本節描述訊息流程和每種訊息類型的語意。(每種訊息的確切表示形式的詳細資訊,請參閱第 53.7 節。)根據連線的狀態,有多種不同的子協定:啟動、查詢、函數呼叫、COPY
和終止。還有一些特殊條款用於非同步操作(包括通知回應和命令取消),這些操作可以在啟動階段之後的任何時間發生。
要開始一個工作階段,前端會開啟與伺服器的連線並傳送啟動訊息。此訊息包含使用者和使用者想要連線的資料庫的名稱;它還識別要使用的特定協定版本。(可選地,啟動訊息可以包含執行時間參數的其他設定。)然後,伺服器使用此資訊及其組態檔案(例如 pg_hba.conf
)的內容來確定連線是否可以臨時接受,以及需要哪些額外的身份驗證(如果有的話)。
然後,伺服器會傳送適當的身份驗證請求訊息,前端必須使用適當的身份驗證回應訊息(例如密碼)進行回覆。對於除 GSSAPI、SSPI 和 SASL 之外的所有身份驗證方法,最多只有一個請求和一個回應。在某些方法中,前端根本不需要任何回應,因此不會發生身份驗證請求。對於 GSSAPI、SSPI 和 SASL,可能需要多次交換封包才能完成身份驗證。
身份驗證週期以伺服器拒絕連線嘗試 (ErrorResponse) 或傳送 AuthenticationOk 結束。
在此階段,伺服器可能傳送的訊息有
連線嘗試已被拒絕。然後,伺服器會立即關閉連線。
身份驗證交換已成功完成。
前端現在必須參與與伺服器的 Kerberos V5 身份驗證對話(此處未描述,是 Kerberos 規格的一部分)。如果成功,伺服器會回應 AuthenticationOk,否則會回應 ErrorResponse。此功能已不再支援。
前端現在必須傳送 PasswordMessage,其中包含明文形式的密碼。如果密碼正確,伺服器會回應 AuthenticationOk,否則會回應 ErrorResponse。
前端現在必須傳送 PasswordMessage,其中包含透過 MD5 加密的密碼(帶有使用者名稱),然後使用 AuthenticationMD5Password 訊息中指定的 4 位元組隨機 salt 再次加密。如果密碼正確,伺服器會回應 AuthenticationOk,否則會回應 ErrorResponse。實際的 PasswordMessage 可以在 SQL 中計算為 concat('md5', md5(concat(md5(concat(password, username)), random-salt)))
。(請記住,md5()
函數會將其結果以十六進位字串形式傳回。)
前端現在必須啟動 GSSAPI 協商。前端將傳送 GSSResponse 訊息,其中包含 GSSAPI 資料流的第一部分,以回應此訊息。如果需要更多訊息,伺服器將回應 AuthenticationGSSContinue。
前端現在必須啟動 SSPI 協商。前端將傳送 GSSResponse,其中包含 SSPI 資料流的第一部分,以回應此訊息。如果需要更多訊息,伺服器將回應 AuthenticationGSSContinue。
此訊息包含來自 GSSAPI 或 SSPI 協商前一步驟的回應資料 (AuthenticationGSS、AuthenticationSSPI 或先前的 AuthenticationGSSContinue)。如果此訊息中的 GSSAPI 或 SSPI 資料表示需要更多資料才能完成驗證,前端必須將該資料作為另一個 GSSResponse 訊息傳送。如果 GSSAPI 或 SSPI 驗證由此訊息完成,伺服器接下來會傳送 AuthenticationOk 以表示驗證成功,或傳送 ErrorResponse 以表示失敗。
前端現在必須啟動 SASL 協商,使用訊息中列出的其中一種 SASL 機制。前端將傳送一個 SASLInitialResponse,其中包含所選機制的名稱,以及回應此訊息的 SASL 資料流的第一部分。如果需要更多訊息,伺服器將以 AuthenticationSASLContinue 回應。詳情請參閱第 53.3 節。
此訊息包含來自 SASL 協商前一步驟的挑戰資料 (AuthenticationSASL 或先前的 AuthenticationSASLContinue)。前端必須以 SASLResponse 訊息回應。
SASL 驗證已完成,並包含客戶端的其他機制特定資料。伺服器接下來會傳送 AuthenticationOk 以表示驗證成功,或傳送 ErrorResponse 以表示失敗。只有在 SASL 機制指定在完成時從伺服器傳送到客戶端的額外資料時,才會傳送此訊息。
伺服器不支援客戶端請求的次要協定版本,但支援協定的早期版本;此訊息表示支援的最高次要版本。如果客戶端在啟動封包中請求了不支援的協定選項(即以 _pq_.
開頭),也會傳送此訊息。此訊息之後將跟著 ErrorResponse 或表示驗證成功或失敗的訊息。
如果前端不支援伺服器請求的驗證方法,則應立即關閉連線。
收到 AuthenticationOk 後,前端必須等待來自伺服器的其他訊息。在此階段,正在啟動後端處理程序,前端只是一個感興趣的旁觀者。啟動嘗試仍然有可能失敗 (ErrorResponse) 或伺服器拒絕支援請求的次要協定版本 (NegotiateProtocolVersion),但在正常情況下,後端會傳送一些 ParameterStatus 訊息、BackendKeyData,最後是 ReadyForQuery。
在此階段,後端將嘗試應用啟動訊息中提供的任何額外執行階段參數設定。如果成功,這些值將成為工作階段預設值。錯誤會導致 ErrorResponse 並退出。
此階段中來自後端的可能訊息包括
此訊息提供密鑰資料,如果前端希望稍後能夠發出取消請求,則必須儲存該資料。前端不應回應此訊息,而應繼續監聽 ReadyForQuery 訊息。
此訊息通知前端後端參數的目前(初始)設定,例如 client_encoding 或 DateStyle。前端可以忽略此訊息,或記錄設定以供將來使用;請參閱第 53.2.7 節,了解更多詳細資訊。前端不應回應此訊息,而應繼續監聽 ReadyForQuery 訊息。
啟動已完成。前端現在可以發出指令。
啟動失敗。傳送此訊息後,連線將關閉。
已發出警告訊息。前端應顯示該訊息,但繼續監聽 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 訊息與後端在每個指令週期後發出的訊息相同。根據前端的編碼需求,可以將 ReadyForQuery 視為啟動指令週期,或將 ReadyForQuery 視為結束啟動階段和每個後續指令週期,都是合理的。
簡單查詢週期由前端傳送 Query 訊息到後端啟動。該訊息包含以文字字串表示的 SQL 指令(或多個指令)。然後,後端會根據查詢指令字串的內容傳送一個或多個回應訊息,最後傳送 ReadyForQuery 回應訊息。ReadyForQuery 通知前端,它可以安全地傳送新指令。(前端實際上並不需要在發出另一個指令之前等待 ReadyForQuery,但前端必須自行負責弄清楚如果先前的指令失敗且已發出的後續指令成功會發生什麼情況。)
來自後端的可能回應訊息包括
SQL 指令正常完成。
後端已準備好將資料從前端複製到資料表;請參閱第 53.2.6 節。
後端已準備好將資料從資料表複製到前端;請參閱第 53.2.6 節。
表示即將傳回 rows 以回應 SELECT
、FETCH
等查詢。此訊息的內容描述了 rows 的欄位配置。之後,將為每個傳回前端的 row 傳送一個 DataRow 訊息。
由 SELECT
、FETCH
等查詢傳回的一組 rows 中的一個 row。
已識別出一個空的查詢字串。
發生錯誤。
查詢字串的處理已完成。系統會傳送一個單獨的訊息來表示這一點,因為查詢字串可能包含多個 SQL 指令。(CommandComplete 標記處理一個 SQL 指令的結束,而不是整個字串。)無論處理成功或出現錯誤,都始終會傳送 ReadyForQuery。
已發出與查詢相關的警告訊息。通知是其他回應之外的,也就是說,後端將繼續處理該指令。
對 SELECT
查詢的回應(或其他傳回 row 集合的查詢,例如 EXPLAIN
或 SHOW
)通常包含 RowDescription、零個或多個 DataRow 訊息,然後是 CommandComplete。COPY
到前端或從前端複製會調用特殊的協定,如第 53.2.6 節中所述。所有其他查詢類型通常只產生一個 CommandComplete 訊息。
由於查詢字串可能包含多個查詢(以分號分隔),因此在後端完成處理查詢字串之前,可能會有多個此類回應序列。當整個字串已處理完畢且後端已準備好接受新的查詢字串時,會發出 ReadyForQuery。
如果收到一個完全空的(除了空格之外沒有任何內容)查詢字串,則回應為 EmptyQueryResponse,然後是 ReadyForQuery。
如果發生錯誤,會先發出 ErrorResponse,然後發出 ReadyForQuery。所有後續的查詢字串處理都會被 ErrorResponse 中止 (即使其中還有更多的查詢)。請注意,這可能發生在個別查詢產生的訊息序列的中途。
在簡易查詢模式中,檢索值的格式始終是文字,除非給定的指令是從使用 BINARY
選項宣告的游標進行 FETCH
。 在這種情況下,檢索值是二進位格式。 RowDescription 訊息中提供的格式代碼會說明正在使用的格式。
前端必須準備好在預期任何其他類型訊息時,接受 ErrorResponse 和 NoticeResponse 訊息。另請參閱第 53.2.7 節,了解後端可能因外部事件產生的訊息。
建議的做法是以狀態機樣式編寫前端,使其能夠在任何可能有意義的時間接受任何訊息類型,而不是硬性規定訊息的確切順序。
當簡易 Query 訊息包含多個 SQL 語句(以分號分隔)時,這些語句會以單一交易執行,除非包含明確的交易控制指令來強制不同的行為。 例如,如果訊息包含
INSERT INTO mytable VALUES(1); SELECT 1/0; INSERT INTO mytable VALUES(2);
那麼 SELECT
中的除以零錯誤將會強制回滾第一個 INSERT
。 此外,由於訊息的執行在第一個錯誤時被放棄,因此根本不會嘗試第二個 INSERT
。
如果訊息包含
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELECT 1/0;
那麼第一個 INSERT
會被明確的 COMMIT
指令提交。 第二個 INSERT
和 SELECT
仍然被視為單一交易,因此除以零的錯誤將會回滾第二個 INSERT
,但不會回滾第一個。
此行為的實現方式是在隱含交易區塊中執行多語句 Query 訊息中的語句,除非存在一些明確的交易區塊供它們在其中執行。 隱含交易區塊和常規交易區塊之間的主要區別在於,隱含區塊會在 Query 訊息結束時自動關閉,如果沒有錯誤則隱含提交,如果存在錯誤則隱含回滾。 這類似於單獨執行語句時發生的隱含提交或回滾(當不在交易區塊中時)。
如果會話已經處於交易區塊中,這是由於先前訊息中的 BEGIN
導致的,那麼 Query 訊息只會繼續該交易區塊,無論訊息包含一個語句還是多個語句。 但是,如果 Query 訊息包含關閉現有交易區塊的 COMMIT
或 ROLLBACK
,則任何後續語句都會在隱含交易區塊中執行。 相反,如果 BEGIN
出現在多語句 Query 訊息中,則它會啟動一個常規交易區塊,該區塊只會由明確的 COMMIT
或 ROLLBACK
終止,無論它是否出現在此 Query 訊息或稍後的訊息中。 如果 BEGIN
位於一些已作為隱含交易區塊執行的語句之後,則這些語句不會立即提交; 實際上,它們會被追溯包含到新的常規交易區塊中。
出現在隱含交易區塊中的 COMMIT
或 ROLLBACK
會像往常一樣執行,關閉隱含區塊; 但是,由於沒有先前的 BEGIN
的 COMMIT
或 ROLLBACK
可能代表錯誤,因此會發出警告。 如果後面還有更多語句,則會為它們啟動一個新的隱含交易區塊。
隱含交易區塊中不允許使用保存點,因為它們會與在任何錯誤時自動關閉區塊的行為衝突。
請記住,無論可能存在任何交易控制指令,Query 訊息的執行都會在第一個錯誤時停止。 因此,例如給定
BEGIN; SELECT 1/0; ROLLBACK;
在單個 Query 訊息中,會話將被留在失敗的常規交易區塊中,因為在除以零錯誤之後無法達到 ROLLBACK
。 需要另一個 ROLLBACK
才能將會話恢復到可用狀態。
另一個值得注意的行為是,在執行任何查詢字串之前,會對整個查詢字串進行初始詞法和語法分析。 因此,稍後語句中的簡單錯誤(例如拼寫錯誤的關鍵字)可能會阻止執行任何語句。 這通常對用戶是不可見的,因為當作為隱含交易區塊完成時,所有語句都會回滾。 但是,當嘗試在多語句 Query 中執行多個交易時,它可能是可見的。 例如,如果拼寫錯誤將我們之前的示例變成了
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELCT 1/0;
那麼所有語句都不會運行,導致第一個 INSERT
未提交的明顯差異。 在語義分析或之後檢測到的錯誤(例如拼寫錯誤的表或列名)不會產生此效果。
擴展查詢協議將上述簡單查詢協議分解為多個步驟。 準備步驟的結果可以重複使用多次,以提高效率。 此外,還提供了其他功能,例如可以將資料值作為單獨的參數提供,而無需直接將它們插入到查詢字串中。
在擴展協議中,前端首先發送 Parse 訊息,其中包含文字查詢字串,可選地包含有關參數佔位符資料類型的一些資訊,以及目標預處理語句物件的名稱(空字串選擇未命名的預處理語句)。 響應是 ParseComplete 或 ErrorResponse。 參數資料類型可以由 OID 指定; 如果未給出,則解析器會嘗試以與對待未類型化文字字串常數相同的方式推斷資料類型。
參數資料型別可以透過將其設為零,或使參數型別 OID 的陣列短於查詢字串中使用的參數符號數量($
n
)來保持未指定。另一種特殊情況是,參數的型別可以指定為 void
(也就是 void
偽型別的 OID)。這是為了允許參數符號用於實際上是 OUT 參數的函式參數。通常,在任何情況下都無法使用 void
參數,但如果此類參數符號出現在函式的參數列表中,則它實際上會被忽略。例如,如果 $3
和 $4
被指定為 void
型別,則像 foo($1,$2,$3,$4)
這樣的函式呼叫可以匹配具有兩個 IN 和兩個 OUT 引數的函式。
Parse 訊息中包含的查詢字串不能包含多個 SQL 陳述式;否則會報告語法錯誤。簡單查詢協定中不存在此限制,但擴充協定中存在此限制,因為允許預先處理的陳述式或入口包含多個命令會使協定過於複雜。
如果成功建立,具名預先處理的陳述式物件將持續到目前會期結束,除非明確銷毀。未命名的預先處理陳述式僅持續到下一個指定未命名陳述式作為目標的 Parse 陳述式發出為止。(請注意,簡單的 Query 訊息也會銷毀未命名的陳述式。)具名的預先處理陳述式必須先明確關閉,然後才能被另一個 Parse 訊息重新定義,但未命名的陳述式則不需要。具名的預先處理陳述式也可以在 SQL 命令層級建立和存取,使用 PREPARE
和 EXECUTE
。
一旦預先處理的陳述式存在,就可以使用 Bind 訊息準備好執行。Bind 訊息提供來源預先處理陳述式的名稱(空字串表示未命名的預先處理陳述式)、目標入口的名稱(空字串表示未命名的入口),以及用於預先處理陳述式中存在的任何參數預留位置的值。提供的參數集必須與預先處理的陳述式所需的值相符。(如果您在 Parse 訊息中宣告了任何 void
參數,請在 Bind 訊息中傳遞 NULL 值。)Bind 還指定用於查詢傳回的任何資料的格式;可以整體指定格式,也可以針對每個欄位指定格式。回應是 BindComplete 或 ErrorResponse。
文字和二進位輸出之間的選擇由 Bind 中給定的格式代碼決定,無論涉及的 SQL 命令如何。使用擴充查詢協定時,游標宣告中的 BINARY
屬性無關緊要。
查詢計畫通常在處理 Bind 訊息時發生。如果預先處理的陳述式沒有參數,或者被重複執行,伺服器可能會儲存建立的計畫,並在後續針對相同預先處理陳述式的 Bind 訊息期間重複使用它。但是,只有當它發現可以建立一個通用的計畫,其效率不會比依賴於提供的特定參數值的計畫差很多時,它才會這樣做。就協定而言,這會透明地發生。
如果成功建立,具名入口物件將持續到目前交易結束,除非明確銷毀。未命名的入口會在交易結束時,或在下一個指定未命名入口作為目標的 Bind 陳述式發出後立即銷毀。(請注意,簡單的 Query 訊息也會銷毀未命名的入口。)具名入口必須先明確關閉,然後才能被另一個 Bind 訊息重新定義,但未命名的入口則不需要。具名入口也可以在 SQL 命令層級建立和存取,使用 DECLARE CURSOR
和 FETCH
。
一旦入口存在,就可以使用 Execute 訊息執行它。Execute 訊息指定入口名稱(空字串表示未命名的入口)和最大結果列計數(零表示“提取所有列”)。結果列計數僅對包含傳回列集的命令的入口有意義;在其他情況下,命令始終執行到完成,並且忽略列計數。Execute 的可能回應與上面針對透過簡單查詢協定發出的查詢所描述的回應相同,但 Execute 不會導致發出 ReadyForQuery 或 RowDescription。
如果 Execute 在完成入口的執行之前終止(由於達到非零結果列計數),它將傳送 PortalSuspended 訊息;此訊息的出現告訴前端應該針對相同的入口發出另一個 Execute 以完成操作。指示來源 SQL 命令完成的 CommandComplete 訊息要等到入口的執行完成後才會傳送。因此,Execute 階段始終以以下訊息之一的出現而終止:CommandComplete、EmptyQueryResponse(如果入口是從空查詢字串建立的)、ErrorResponse 或 PortalSuspended。
在每個擴充查詢訊息序列完成時,前端應發出 Sync 訊息。此無參數訊息會導致後端關閉當前交易(如果它不在 BEGIN
/COMMIT
交易區塊內)(“關閉”表示如果沒有錯誤則提交,如果出錯則回滾)。然後發出 ReadyForQuery 回應。Sync 的目的是為錯誤恢復提供一個重新同步點。當在處理任何擴充查詢訊息時檢測到錯誤時,後端會發出 ErrorResponse,然後讀取並丟棄訊息,直到到達 Sync,然後發出 ReadyForQuery 並恢復正常的訊息處理。(但請注意,如果在處理 Sync 時檢測到錯誤,則不會發生跳過——這確保了每個 Sync 只傳送一個 ReadyForQuery。)
Sync 不會導致使用 BEGIN
開啟的交易區塊關閉。可以檢測到這種情況,因為 ReadyForQuery 訊息包含交易狀態資訊。
除了這些基本、必需的操作之外,還有幾個可以與擴充查詢協定一起使用的可選操作。
Describe 訊息(入口變體)指定現有入口的名稱(或未命名入口的空字串)。回應是 RowDescription 訊息,描述了執行入口將傳回的列;如果入口不包含將傳回列的查詢,則為 NoData 訊息;如果沒有這樣的入口,則為 ErrorResponse。
Describe 訊息(陳述式變體)指定現有預先處理的陳述式的名稱(或未命名預先處理陳述式的空字串)。回應是 ParameterDescription 訊息,描述了陳述式所需的參數,後跟 RowDescription 訊息,描述了最終執行陳述式時將傳回的列(如果陳述式不會傳回列,則為 NoData 訊息)。如果沒有這樣的預先處理陳述式,則發出 ErrorResponse。請注意,由於尚未發出 Bind,後端尚不知道用於傳回欄位的格式;在這種情況下,RowDescription 訊息中的格式代碼欄位將為零。
在大多數情況下,前端應在發出 Execute 指令之前,先發出 Describe 的其中一種變體,以確保它知道如何解讀將收到的結果。
Close 訊息會關閉現有的預備語句或入口,並釋放資源。針對不存在的語句或入口名稱發出 Close 並不會產生錯誤。回應通常是 CloseComplete,但如果在釋放資源時遇到困難,則可能是 ErrorResponse。請注意,關閉預備語句會隱含地關閉從該語句建構的任何已開啟的入口。
Flush 訊息不會產生任何特定輸出,但會強制後端傳遞其輸出緩衝區中任何待處理的資料。如果前端希望在發出更多指令之前檢查該指令的結果,則在除了 Sync 之外的任何擴展查詢指令之後,都必須發送 Flush。如果沒有 Flush,後端傳回的訊息將組合到最小數量的封包中,以最大限度地減少網路開銷。
簡單的 Query 訊息大致相當於一系列 Parse、Bind、入口 Describe、Execute、Close、Sync,使用未命名的預備語句和入口物件,且沒有參數。一個區別是它將接受查詢字串中的多個 SQL 語句,自動連續地對每個語句執行 bind/describe/execute 序列。另一個區別是它不會傳回 ParseComplete、BindComplete、CloseComplete 或 NoData 訊息。
使用擴展查詢協定允許管線化,這表示發送一系列查詢,而無需等待先前的查詢完成。 這減少了完成給定系列操作所需的網路往返次數。 但是,如果其中一個步驟失敗,使用者必須仔細考慮所需的行為,因為稍後的查詢已經在傳輸到伺服器的過程中。
處理這種情況的一種方法是將整個查詢系列設為單一交易,也就是將其包裝在 BEGIN
... COMMIT
中。 但是,如果希望某些指令獨立於其他指令進行提交,則這無濟於事。
擴展查詢協定提供了另一種管理此問題的方法,即省略在相依步驟之間發送 Sync 訊息。 因為在發生錯誤後,後端會跳過指令訊息,直到找到 Sync 為止,這允許在先前的指令失敗時自動跳過管線中的後續指令,而無需客戶端使用 BEGIN
和 COMMIT
顯式管理它。 管線中可獨立提交的部分可以用 Sync 訊息分隔。
如果客戶端沒有發出顯式的 BEGIN
,那麼如果前面的步驟成功,則每個 Sync 通常會導致隱式的 COMMIT
,如果它們失敗,則會導致隱式的 ROLLBACK
。 但是,有一些 DDL 指令 (例如 CREATE DATABASE
) 無法在交易區塊中執行。 如果在管線中執行其中一個指令,除非它是管線中的第一個指令,否則它將失敗。 此外,成功後,它將強制立即提交以保持資料庫的一致性。 因此,緊隨這些指令之一之後的 Sync 除了回應 ReadyForQuery 之外,沒有任何作用。
使用此方法時,管線的完成必須通過計算 ReadyForQuery 訊息並等待其達到已發送的 Sync 數量來確定。 計算指令完成回應是不可靠的,因為某些指令可能會被跳過,因此不會產生完成訊息。
函數呼叫子協定允許客戶端請求直接呼叫資料庫 pg_proc
系統目錄中存在的任何函數。 客戶端必須具有函數的執行權限。
函數呼叫子協定是一個舊版功能,最好在新程式碼中避免使用。 通過設定執行 SELECT function($1, ...)
的預備語句,可以完成類似的結果。 然後,函數呼叫週期可以用 Bind/Execute 替換。
函數呼叫週期由前端將 FunctionCall 訊息發送到後端來啟動。 然後,後端會根據函數呼叫的結果發送一個或多個回應訊息,最後發送 ReadyForQuery 回應訊息。 ReadyForQuery 通知前端它可以安全地發送新的查詢或函數呼叫。
來自後端的可能回應訊息包括
發生錯誤。
函數呼叫已完成,並傳回訊息中給出的結果。 (請注意,函數呼叫協定只能處理單個標量結果,而不是列型別或結果集。)
函數呼叫的處理已完成。無論處理成功終止還是發生錯誤,都將始終發送 ReadyForQuery。
已發出與函數呼叫相關的警告訊息。通知是對其他回應的補充,即後端將繼續處理該指令。
COPY
指令允許高速批量資料傳輸到伺服器或從伺服器傳輸。 複製輸入和複製輸出操作都會將連線切換到不同的子協定中,該協定會持續到操作完成為止。
當後端執行 COPY FROM STDIN
SQL 語句時,會啟動複製輸入模式(將資料傳輸到伺服器)。 後端會將 CopyInResponse 訊息發送到前端。 然後,前端應發送零個或多個 CopyData 訊息,形成輸入資料流。 (訊息邊界不需要與列邊界有任何關係,儘管這通常是一個合理的選擇。) 前端可以通過發送 CopyDone 訊息(允許成功終止)或 CopyFail 訊息(這將導致 COPY
SQL 語句失敗並出現錯誤)來終止複製輸入模式。 然後,後端恢復到 COPY
啟動之前的指令處理模式,這將是簡單查詢協定或擴展查詢協定。 接下來,它將發送 CommandComplete(如果成功)或 ErrorResponse(如果沒有成功)。
如果在複製輸入模式期間發生後端偵測到的錯誤(包括收到 CopyFail 訊息),後端將發出 ErrorResponse 訊息。 如果 COPY
指令是通過擴展查詢訊息發出的,則後端現在將丟棄前端訊息,直到收到 Sync 訊息為止,然後它將發出 ReadyForQuery 並返回正常處理。 如果 COPY
指令是在簡單 Query 訊息中發出的,則該訊息的其餘部分將被丟棄並發出 ReadyForQuery。 在任何一種情況下,前端發出的任何後續 CopyData、CopyDone 或 CopyFail 訊息都將被簡單地丟棄。
後端將忽略在複製輸入模式期間收到的 Flush 和 Sync 訊息。 收到任何其他非複製訊息型別構成一個錯誤,該錯誤將中止如上所述的複製輸入狀態。 (Flush 和 Sync 的例外是為了方便客戶端程式庫,它們總是在 Execute 訊息之後發送 Flush 或 Sync,而不檢查要執行的指令是否為 COPY FROM STDIN
。)
當後端執行 COPY TO STDOUT
SQL 指令時,會啟動複製輸出模式(從伺服器傳輸資料)。後端會發送一個 CopyOutResponse 訊息到前端,接著是零或多個 CopyData 訊息(每一列總是傳送一個),最後是 CopyDone。然後,後端會回復到 COPY
開始前的指令處理模式,並發送 CommandComplete。前端無法中止傳輸(除非關閉連線或發出取消請求),但可以捨棄不需要的 CopyData 和 CopyDone 訊息。
如果後端在複製輸出模式期間偵測到錯誤,後端會發出一個 ErrorResponse 訊息並回復到正常處理。前端應將收到 ErrorResponse 視為複製輸出模式的終止。
NoticeResponse 和 ParameterStatus 訊息可能會穿插在 CopyData 訊息之間;前端必須處理這些情況,並且應該準備好處理其他非同步訊息類型(請參閱第 53.2.7 節)。否則,除了 CopyData 或 CopyDone 之外的任何訊息類型都可能被視為複製輸出模式的終止。
還有另一種與複製相關的模式稱為 copy-both,它允許高速批量資料傳輸到伺服器以及從伺服器傳輸。當 walsender 模式下的後端執行 START_REPLICATION
指令時,會啟動 copy-both 模式。後端會發送一個 CopyBothResponse 訊息到前端。然後,後端和前端都可以發送 CopyData 訊息,直到任何一方發送 CopyDone 訊息。在客戶端發送 CopyDone 訊息後,連線會從 copy-both 模式變為複製輸出模式,並且客戶端不得再發送任何 CopyData 訊息。同樣地,當伺服器發送 CopyDone 訊息時,連線會進入複製輸入模式,並且伺服器不得再發送任何 CopyData 訊息。在雙方都發送 CopyDone 訊息後,複製模式會終止,並且後端會回復到指令處理模式。如果後端在 copy-both 模式期間偵測到錯誤,後端會發出一個 ErrorResponse 訊息,捨棄前端訊息直到收到 Sync 訊息,然後發出 ReadyForQuery 並回復到正常處理。前端應將收到 ErrorResponse 視為終止雙向複製;在這種情況下,不應發送 CopyDone。有關透過 copy-both 模式傳輸的子協定的更多資訊,請參閱第 53.4 節。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 訊息包含一些欄位,這些欄位會告知前端每列的欄位數量以及每個欄位使用的格式代碼。(截至目前實作,給定的 COPY
操作中的所有欄位都將使用相同的格式,但訊息設計並未假設如此。)
在某些情況下,後端會發送並非由前端的指令流明確提示的訊息。前端必須準備好隨時處理這些訊息,即使沒有參與查詢。至少,應該在開始讀取查詢回應之前檢查這些情況。
由於外部活動,可能會產生 NoticeResponse 訊息;例如,如果資料庫管理員命令進行「快速」資料庫關閉,後端會在關閉連線之前發送一個 NoticeResponse 訊息來指示這個事實。因此,前端應始終準備好接受和顯示 NoticeResponse 訊息,即使連線名義上處於閒置狀態。
每當後端認為前端應該知道的任何參數的有效值發生變更時,都會產生 ParameterStatus 訊息。最常見的情況是,這是回應前端執行的 SET
SQL 指令而發生的,並且這種情況實際上是同步的 — 但參數狀態變更也可能發生,因為管理員變更了設定檔,然後將 SIGHUP 信號發送到伺服器。此外,如果 SET
指令被回滾,將會產生一個適當的 ParameterStatus 訊息來報告當前的有效值。
目前,有一組硬編碼的參數會產生 ParameterStatus。它們是
application_name |
is_superuser |
client_encoding |
scram_iterations |
DateStyle |
server_encoding |
default_transaction_read_only |
server_version |
in_hot_standby |
session_authorization |
integer_datetimes |
standard_conforming_strings |
IntervalStyle |
TimeZone |
(default_transaction_read_only
和 in_hot_standby
在 14 之前的版本中未報告;scram_iterations
在 16 之前的版本中未報告。)請注意,server_version
、server_encoding
和 integer_datetimes
是偽參數,在啟動後無法變更。此集合將來可能會變更,甚至可能變成可配置的。因此,前端應簡單地忽略它不理解或不關心的參數的 ParameterStatus。
如果前端發出一個 LISTEN
指令,那麼每當對同一個通道名稱執行 NOTIFY
指令時,後端都會發送一個 NotificationResponse 訊息(不要與 NoticeResponse 混淆!)。
目前,NotificationResponse 只能在交易之外發送,因此它不會發生在指令回應序列的中間,儘管它可能剛好發生在 ReadyForQuery 之前。然而,設計假設這種情況的前端邏輯是不明智的。良好的做法是在協定的任何時候都能夠接受 NotificationResponse。
在處理查詢期間,前端可能會請求取消查詢。出於實作效率的考量,取消請求不會直接在到後端的開啟連線上發送:我們不希望後端在查詢處理期間不斷檢查來自前端的新輸入。取消請求應該相對不頻繁,因此我們使其稍微繁瑣,以避免在正常情況下造成效能損失。
若要發出取消請求,前端會開啟一個到伺服器的新連線並發送一個 CancelRequest 訊息,而不是通常透過新連線發送的 StartupMessage 訊息。伺服器將處理此請求,然後關閉連線。出於安全考量,不會對取消請求訊息做出直接回應。
CancelRequest 訊息將被忽略,除非它包含在連線啟動期間傳遞到前端的相同金鑰資料(PID 和密鑰)。如果請求符合目前正在執行的後端的 PID 和密鑰,則會中止目前查詢的處理。(在現有的實作中,這是透過向處理查詢的後端程序發送一個特殊信號來完成的。)
取消訊號可能會或可能不會有任何效果 — 例如,如果它在後端完成查詢處理後才送達,那麼它將不會有任何效果。如果取消有效,它會導致目前指令提早終止,並顯示錯誤訊息。
總而言之,由於安全性和效率的考量,前端沒有直接的方式可以判斷取消請求是否成功。它必須繼續等待後端回應查詢。發出取消指令僅僅是提高了目前查詢提早完成的機率,並且提高了它會因錯誤訊息而失敗而不是成功的機率。
由於取消請求是透過與伺服器的新連線發送,而不是透過常規的前端/後端通訊連結,因此取消請求可以由任何程序發出,而不僅僅是要取消查詢的前端。這可能會在建構多程序應用程式時提供額外的彈性。它也引入了一項安全風險,即未經授權的人員可能會嘗試取消查詢。透過要求在取消請求中提供動態產生的金鑰來解決安全風險。
正常的、優雅的終止程序是前端發送一個 Terminate 訊息,並立即關閉連線。收到此訊息後,後端會關閉連線並終止。
在極少數情況下(例如管理員指令的資料庫關閉),後端可能會在沒有任何前端請求的情況下斷開連線。在這種情況下,後端會在關閉連線之前嘗試發送錯誤或通知訊息,說明斷開連線的原因。
其他終止情況源自於各種故障情況,例如一端或另一端的核心傾印、通訊連結丟失、訊息邊界同步丟失等。如果前端或後端看到意外的連線關閉,它應該清理並終止。如果前端不想自行終止,它可以選擇重新連線伺服器來啟動新的後端。如果收到無法識別的訊息類型,也建議關閉連線,因為這可能表示訊息邊界同步丟失。
對於正常或異常終止,任何未完成的交易都會回滾,而不是提交。但應該注意的是,如果在處理非 SELECT
查詢時前端斷開連線,後端可能會在注意到斷開連線之前完成查詢。如果查詢位於任何交易區塊 (BEGIN
... COMMIT
序列) 之外,那麼它的結果可能會在識別出斷開連線之前被提交。
如果 PostgreSQL 是使用SSL支援建置的,可以使用SSL來加密前端/後端通訊。這在攻擊者可能能夠捕獲連線流量的環境中提供了通訊安全性。有關使用SSL加密 PostgreSQL 連線的更多資訊,請參閱 第 18.9 節。
要啟動一個SSL-加密的連線,前端最初發送一個 SSLRequest 訊息,而不是 StartupMessage。然後,伺服器會以單一位元組回應,其中包含 S
或 N
,表示它願意或不願意執行SSL。如果前端對回應不滿意,可能會在此時關閉連線。要在 S
之後繼續,請執行與伺服器的SSL啟動協商(此處未描述,是SSL規範的一部分)。如果成功,請繼續發送常規的 StartupMessage。在這種情況下,StartupMessage 和所有後續資料都將被SSL-加密。要在 N
之後繼續,請發送常規的 StartupMessage 並在不加密的情況下繼續。(或者,在收到 N
回應後發送 GSSENCRequest 訊息來嘗試使用GSSAPI加密來代替SSL.)
也是允許的。)前端也應該準備好處理伺服器對 SSLRequest 的 ErrorMessage 回應。前端不應向使用者/應用程式顯示此錯誤訊息,因為伺服器尚未經過身份驗證 (CVE-2024-10977)。在這種情況下,必須關閉連線,但前端可以選擇開啟一個新的連線並繼續,而不請求SSL.
當SSL加密可以執行時,伺服器預計只發送單個 S
位元組,然後等待前端啟動SSL協商。如果在這一點上有額外的位元組可以讀取,這很可能意味著中間人試圖執行緩衝區填充攻擊 (CVE-2021-23222)。前端應該編碼為在將套接字交給其 SSL 函式庫之前,從套接字中讀取正好一個位元組,或者如果發現他們讀取了額外的位元組,則將其視為協定違規。
同樣,伺服器期望客戶端在收到伺服器對SSL請求的單個位元組回應之前,不要開始SSL協商。如果客戶端立即開始SSL協商,而不等待收到伺服器回應,則可以將連線延遲減少一次往返。然而,這會導致無法處理伺服器對SSL請求發送負面回應的情況。在這種情況下,伺服器將不會繼續使用 GSSAPI 或未加密的連線或協議錯誤,而是直接斷開連線。
初始的 SSLRequest 也可以用於開啟連線來發送 CancelRequest 訊息。
第二種啟動SSL加密的替代方法是可用的。伺服器將識別立即開始SSL協商而沒有任何先前的 SSLRequest 封包的連線。一旦建立SSL連線,伺服器將期望一個正常的 startup-request 封包,並透過加密通道繼續協商。在這種情況下,任何其他加密請求都將被拒絕。這種方法不適用於通用工具,因為它無法協商最佳的連線加密或處理未加密的連線。然而,它對於伺服器和客戶端都在一起控制的環境很有用。在這種情況下,它可以避免一次往返的延遲,並允許使用依賴標準SSL連線的網路工具。當使用這種風格的SSL連線時,客戶端需要使用 RFC 7301 定義的 ALPN 擴充功能,以防止協議混淆攻擊。PostgreSQL 協議是 "postgresql",已在 IANA TLS ALPN 協議 ID 註冊表中註冊。
雖然協議本身沒有提供伺服器強制SSL加密的方式,但管理員可以將伺服器配置為拒絕未加密的連線,作為身份驗證檢查的副產品。
如果 PostgreSQL 是使用GSSAPI支援建置的,可以使用GSSAPI來加密前端/後端通訊。這在攻擊者可能能夠捕獲連線流量的環境中提供了通訊安全性。有關使用GSSAPI,請參閱 第 18.10 節。
要啟動GSSAPI-加密的連線,前端最初發送一個 GSSENCRequest 訊息,而不是 StartupMessage。然後,伺服器會以單一位元組回應,其中包含 G
或 N
,表示它願意或不願意執行GSSAPI加密。如果前端對回應不滿意,可能會在此時關閉連線。在 G
之後繼續,使用如 RFC 2744 中討論的 GSSAPI C bindings 或等效元件,執行GSSAPI初始化,方法是在迴圈中呼叫 gss_init_sec_context()
,並將結果傳送給伺服器,從空的輸入開始,然後使用伺服器的每個結果,直到它不傳回任何輸出為止。將 gss_init_sec_context()
的結果傳送給伺服器時,將訊息的長度作為網路位元組順序的四位元組整數加在前面。要在 N
之後繼續,請傳送通常的 StartupMessage 並繼續而不加密。(或者,允許在 N
回應之後發出 SSLRequest 訊息,嘗試使用SSL加密來代替GSSAPI.)
前端也應該準備好處理來自伺服器的 GSSENCRequest 的 ErrorMessage 回應。前端不應向使用者/應用程式顯示此錯誤訊息,因為伺服器尚未經過驗證 (CVE-2024-10977)。在這種情況下,必須關閉連線,但前端可能會選擇開啟一個新的連線並繼續而不要求GSSAPI加密。
當GSSAPI加密可以執行時,伺服器應該只傳送單個 G
位元組,然後等待前端啟動GSSAPI握手。如果在這一點有額外的位元組可供讀取,這很可能意味著中間人正在嘗試執行緩衝區填充攻擊 (CVE-2021-23222)。前端的編碼方式應為:要么從socket中準確讀取一個位元組,然後將socket轉交給其GSSAPI庫;要么將其視為協定違規,如果發現已讀取額外的位元組。
初始 GSSENCRequest 也可以用於正在開啟以傳送 CancelRequest 訊息的連線中。
一旦GSSAPI加密已成功建立,請使用 gss_wrap()
來加密通常的 StartupMessage 和所有後續資料,並將 gss_wrap()
結果的長度作為網路位元組順序的四位元組整數加在實際的加密酬載之前。請注意,伺服器只會接受來自小於 16kB 的客戶端的加密封包;客戶端應使用 gss_wrap_size_limit()
來確定未加密訊息的大小,該訊息將適合此限制,並且較大的訊息應分解為多個 gss_wrap()
呼叫。典型的分段是 8kB 的未加密資料,導致略大於 8kB 的加密封包,但遠低於 16kB 的上限。伺服器預計不會向客戶端傳送大於 16kB 的加密封包。
雖然協議本身沒有提供伺服器強制GSSAPI加密的方式,但管理員可以將伺服器配置為拒絕未加密的連線,作為身份驗證檢查的副產品。
如果您在文件中發現任何不正確、與特定功能的經驗不符或需要進一步澄清的地方,請使用此表格來回報文件問題。