PostgreSQL通信協議

數年前為了實現高效率的RPC框架,研究了PostgreSQL資料庫的通信協議,此次發布出來。使用資料庫的通信協議作為RPC協議擁有很多好處,比如無需編寫客戶端,且資料庫的客戶端還可以利用上資料庫連接池帶來的若干好處。對此協議的研究最終寫成了MagicRPC框架, github.com/gashero/magi

譯者:

gashero

日期:

2009-08-14

版本:

PostgreSQL-8.3

地址:

Documentation: 8.3: Frontend/Backend Protocol

目錄

  • 1 概覽
    • 1.1 報文概覽
    • 1.2 擴展查詢概覽
    • 1.3 格式與格式代碼
  • 2 報文流
    • 2.1 初始報文
    • 2.2 簡單查詢
    • 2.3 擴展查詢
    • 2.4 函數調用
    • 2.5 COPY操作
    • 2.6 非同步操作
    • 2.7 取消請求
    • 2.8 終止
    • 2.9 SSL會話加密
  • 3 報文數據類型
  • 4 報文格式
  • 5 錯誤與提示信息欄位
  • 6 從Protocol2.0的改變
  • 7 調試經驗
    • 7.1 開頭的SSL確認
    • 7.2 驗證過程
    • 7.3 密碼確認
    • 7.4 錯誤碼
    • 7.5 結果集的定義

PostgreSQL使用基於消息的協議在前端與後端之間通信。協議支持TCP/IP和Unix套接字。埠號5432已經註冊到IANA作為著名協議,實際中可以使用任何埠號。

本文檔描述3.0版的協議,基於PostgreSQL7.4和以後版本的實現。早起版本協議的描述請參考以前的文檔。同一伺服器可以支持多種版本的協議。初始化請求報文可以告知伺服器所用的協議版本,而伺服器可以跟隨可用的協議。

高級的功能依賴於高版本的協議,例如libpq在連接時傳遞正確的環境變數。

為了有效的服務多個客戶端,伺服器啟動一個新的"後端(backend)"進程給一個客戶端。在當前實現一個新的客戶端進程在連接時立即創建。這對協議是透明的,因此,對協議的目的,術語後端和伺服器是可以互換(interchangeable)的;另外前端和客戶端也是可以互換的。

其他參考文獻:

  1. PgPP.pm: cpansearch.perl.org/src 找到了生成md5密碼的方式

1 概覽

postgresql.org/docs/8.3

協議分為初始(startup)階段和正常操作階段。在初始階段,前端打開連接、認證。這可能是一個報文,也可能是多個報文,依賴於認證方法。如果一切正常,伺服器會發送狀態信息到客戶端,然後進入正常操作階段。除了初始階段請求報文外,這部分的協議由伺服器驅動。

在正常操作階段,前端發送請求和其他命令到後端,後端發送返回結果和其他響應。有些情況(例如 NOTIFY),後端會發送未請求(unsolicite)的報文,但是對大多數情況,會話由前端請求驅動。

會話的終止一般由前端選擇,但是也可以由後端強制。在任何情況下,當後端關閉連接,會回滾(rollback)所有未完成的事務。

在正常操作階段,SQL命令會通過兩個子協議執行。在"簡單查詢"協議,前端只需要發送基於文本的查詢字元串,然後立即解析和執行。在"擴展查詢"協議,處理和請求分為多個步驟:解析、綁定參數值、執行。這是為了靈活性和性能而提供的。

正常操作還有些操作有附加子協議,例如 COPY

1.1 報文概覽

所有通信都是報文流。報文的第一個位元組標識了報文類型,隨後的4位元組指定了餘下的報文長度(這個長度包含自身,但是不包含報文類型位元組)。餘下的內容基於報文類型來檢測。出於歷史原因,客戶端發送的第一個報文(初始報文)沒有消息類型位元組。

為了避免在報文流中丟失同步,伺服器和客戶端都會在處理報文內容之前讀入整個報文到緩衝區(依靠位元組數)。這允許在處理髮生錯誤時簡單的恢復。在極端情況(extreme situation)下,例如沒有足夠的內存緩衝整個報文。接收方使用位元組數決定跳過多少以讀到報文。

相反的,伺服器和客戶端都必須小心的確保不能發送不完整的報文。可以通過把整個報文緩衝起來,直到完整後才發送。如果通信中途收發報文失敗,明智的做法是放棄這個連接,哪怕還有一絲報文邊界重新同步的希望。

1.2 擴展查詢概覽

對擴展查詢協議,執行SQL命令分為多個步驟。步驟間的狀態保留由兩種類型的對象描述:prepared statement和portal。一個prepared statement描述了文本查詢字元串的解析、語意分析、(可選的)查詢計劃的結果。一個prepared statement還沒有準備好執行,因為缺少執行所必須的值。一個portal描述了準備好執行或已經部分執行的語句,所有缺少的參數都已經填入了。對於"SELECT"語句,一個portal等同於一個打開的游標,但是我們使用不同的術語因為游標無法處理非SELECT語句。

整個執行周期包含一個解析步驟,會從文本查詢字元串創建prepared statement;一個綁定步驟,創建portal並傳入prepared statement和參數值;一個執行步驟運行portal的查詢。如果查詢返回多行(SELECT、SHOW等等),執行步驟會被告知獲取有限數量的行,以便多個執行查詢步驟可以被需要完成操作。

後端可以保持多個prepared statement和portal(注意只存在於同一會話中,且不會在會話間共享)。已經存在的prepared statement和portal按照名字引用。另外,未命名的prepared statement和portal也是存在的。儘管其行為基本等同於命名對象,他們之上的優化面向只執行一次就丟棄,而命名對象的優化則是多次執行。

1.3 格式與格式代碼

詳細(particular)的數據類型可能以多種格式發送。在PostgreSQL 7.4,只支持"text"和"binary"格式,但是協議為擴展(provision)提供了可能。期望(desire)的格式依賴于格式碼。客戶端可以為每次發送制定格式碼,也可以為每一列指定。文本的格式碼為0,binary格式碼為1,其他格式碼保留作未來使用。

文本描述的值就是I/O轉換時可以接受的值。在傳輸描述中,沒有附帶null符號,如果前端需要用C字元串處理,就要自己加上。(文本格式不允許潛入null)。

二進位描述的整數使用網路位元組序。對其他數據類型參考文檔。記住二進位描述的複雜數據類型可能在不同伺服器版本之間是不同的;文本格式是更容易移植的方案。

2 報文流

Documentation: 8.3: Message Flow

這一節描述了報文流和報文類型的語意(semantics)。每個報文的詳細信息參見後面章節。依賴於連接類型有多種不同的子協議:初始、查詢、函數調用、 COPY 、終止。同時也有非同步支持,包括通知響應和命令取消,在初始報文之後隨時可用。

2.1 初始報文

為了開始會話,前端打開到伺服器的連接,然後發送初始報文。這個報文包含用戶名、資料庫名、協議版本。同時,初始報文也可以包含運行時配置信息。伺服器隨後使用這些信息和自身配置文件(例如 pg_hba.conf )來檢測連接是否可以臨時性的接受,和使用何種後續的認證方式。

伺服器隨後發出對應的認證請求報文,而前端必須回復對應的認證響應報文(例如密碼)。對所有除了GSSAPI和SSPI的認證方法,都是一個請求和一個響應。在一些方法中,前端無需響應,所以也沒有認證請求發生。對於GSSAPI和SSPI,認證需要完成多次通信數據包的交換。

認證過程結束於伺服器拒絕連接嘗試(ErrorResponse),或者發送AuthenticationOk。

這個階段伺服器可能發送的報文包括:

  1. ErrorResponse :連接嘗試被拒絕。服務里立即關閉連接。
  2. AuthenticationOk :認證成功
  3. AuthenticationKerberosV5 :前端必須加入伺服器的Kerberos V5認證對話。如果成功,伺服器響應AuthenticationOk,否則是ErrorResponse
  4. AuthenticationCleartextPassword :前端發送PasswordMessage,包含密碼的clear-text(明文)形式。如果密碼正確,伺服器響應AuthenticationOk,否則是ErrorResponse。
  5. AuthenticationCryptPassword :前端必須發送PasswordMessage,包含通過crypt(3)加密的密碼,AuthenticationCryptPassport報文中提供2字元的salt。
  6. AuthenticationMD5Password :前端必須發送PasswordMessage,包含MD5加密過的密碼,AuthenticationMD5Password報文提供4字元的salt。
  7. AuthenticationSCMCredential :響應只能是local Unix-domain連接到平台支持的SCM憑據報文。前端必須生成SCM憑據報文,然後發送單一數據位元組。數據位元組的內容是無趣的,只用於確保伺服器等待足夠長事件來接受憑據報文。
  8. AuthenticationGSS :前端發起GSSAPI流。前端發送PasswordMessage,包含GSSAPI數據流的第一部分,後續的報文由伺服器的AuthenticationGSSContinue要求。
  9. AuthenticationSSPI :前端發起(initite)SSPI流。前端發送PasswordMessage包含SSPI數據流,後續報文由伺服器的AuthenticationGSSContinue要求。
  10. AuthenticationGSSContinue :這個報文包含GSSAPI和SSPI流的後續響應數據。如果GSSAPI或SSPI數據需要更多數據來完成認證,前端必須發送另一個PasswordMessage。如果GSSAPI或SSPI認證完成了,伺服器會發送AuthenticationOK來表示認證成功,ErrorResponse表示失敗。

如果前端不支持伺服器請求的認證方法,會立即關閉連接。

在收到AuthenticationOk之後,前端必須等待伺服器的另外一個報文。在這個階段後端開始處理,而前端只能做旁觀者。這時仍然有可能初始化失敗(ErrorResponse),但是在大多數情況下是後端發送一些ParameterStatus報文、BackendKeyData和最後的ReadyForQuery。

在這個階段後端會嘗試應用初始報文中給出的運行時參數配置。如果成功,這些值會作為會話的默認值,否則ErrorResponse後退出。

這個階段後端可能發送的報文:

  1. BackendKeyData :這個報文提供了以後前端用來取消請求的密碼鍵數據。前端無需理會此報文,但是必須等待ReadyForQuery報文。
  2. ParameterStatus :這個報文告訴(informs)前端當前配置參數,例如 client_encoding 或者 DataStyle 。前端可以忽略此報文,或者記錄下來供以後使用。前端無需響應此報文,但是應該繼續等待ReadyForQuery報文。
  3. ReadyForQuery :初始化結束,前端可以發送命令了。
  4. ErrorResponse :初始化失敗,發送此報文後連接關閉。
  5. NoticeResponse :一個警告信息。前端應該顯示該消息然後繼續等待ReadyForQuery或ErrorResponse。

在後端執行完命令後,也會發送ReadyForQuery報文。依賴於前端的代碼,應該認為ReadyForQuery作為一個命令周期的開始,或者將ReadyForQuery作為初始化和每個子命令的結束。

2.2 簡單查詢

一個簡單查詢由前端發起,發送一個查詢報文到後端。報文包括SQL命令文本。後端隨後發送一個或多個相應報文,最後是一個ReadyForQuery響應報文。ReadyForQuery告知(inform)前端可以安全的發送新的命令了。(前端並不是必須等待ReadyForQuery,但是前端必須在命令失敗時獲取失敗信息)。

後端可能的相應報文:

  1. CommandComplete :一個SQL命令正常結束
  2. CopyInResponse :後端準備好從前端拷貝數據到表格
  3. CopyOutResponse :後端準備好從表格拷貝數據到前端
  4. RowDescription :表明由SELECT、FETCH等返回的行。報文內容描述了列布局。這個報文跟隨DataRow報文。
  5. DataRow :由SELECT、FETCH返回的行集合
  6. EmptyQueryResponse :檢測到空的查詢字元串
  7. ErrorResponse :發生了錯誤
  8. ReadyForQuery :查詢過程結束了。一個分隔報文告知前端,因為查詢字元串可能包含多個SQL命令。(CommandComplete標記一個SQL命令處理結束,而不是整個字元串)ReadyForQuery總是會發送,無論執行成功還是失敗。
  9. NoticeResponse :表示查詢有相關的警告信息。提示會在其他報文,如後端會繼續執行命令。

SELECT查詢的響應(或其他可以返回結果集的查詢,如EXPLAIN、SHOW)一般包含RowDescription,0個或多個DataRow報文,隨後是CommandComplete。COPY類操作調用特殊的協議。所有其他查詢類型一般只產生一個CommandComplete報文。

因為一個查詢字元串可能包含多個查詢(由分號分開),所有可能有多個響應序列。ReadyForQuery用以標誌整個查詢字元串都處理完成,並可以接受新的查詢字元串了。

如果一個空的查詢字元串,響應就是EmptyQueryResponse,隨後就是ReadyForQuery。

在發生錯誤時,ErrorResponse作響應,隨後是ReadyForQuery。查詢字元串中所有後續處理都會由ErrorResponse中斷。注意這有可能在一系列報文中的一個無效查詢鎖產生。

在簡單查詢模式,取回值的格式總是文本,除非給定命令是FETCH,且已經有BINARY選項。在那種情況下,取回值是二進位格式。格式代碼在RowDescription報文中。

前端必須準備好在準備接受其他報文時接受ErrorResponse和NoticeResponse報文。

建議實現前端時以狀態機的風格,以便接受任意報文類型。

2.3 擴展查詢

@waiting ...

2.4 函數調用

函數調用子協議允許客戶端直接請求已經存在的資料庫pg_proc系統中的功能。客戶端必須有函數的execute許可權。

Note

函數調用子協議是一個遺留(legacy)功能,可能在未來刪除。類似的功能可以通過設置一個prepared statement做"SELECT function($1,...)"。函數調用過程可以被Bind/Execute替換。

@waiting...

2.5 COPY操作

COPY命令允許與伺服器的高速批量數據傳輸。拷貝入和拷貝出操作將協議轉換到一種子協議中,直到操作完成。

@waiting ...

2.6 非同步操作

@waiting ...

2.7 取消請求

@waiting ...

2.8 終止

正常的終止過程是前端發送終止消息,並立即關閉連接。後端收到該消息以後也立即關閉連接。

特殊情況(如管理員強制關閉伺服器),後端會在沒有前端請求時關閉連接。在這種情況下後端會嘗試發送錯誤或通知消息,在關閉連接之前給出關閉理由。

另一種關閉的情況是來自某些失敗,例如核心錯誤,丟失通信連接丟失消息同步等等。前端或後端檢測到沒有預期的連接關閉,就應該清理和終止。前端有選項可以啟動一個新的後端。收到未知類型消息時也可以關閉連接,通常發生在丟失消息同步時。

無論正常還是異常終止,任何打開的事務都要回滾,而不是提交。應該注意當前端執行非SELECT查詢時發生了斷開,後端會在通知斷開前完成查詢。如果查詢不再任何事務塊(BEGIN..COMMIT序列)中,其結果會在斷開前提交。

2.9 SSL會話加密

如果構建PostgreSQL時有SSL支持,那麼前後端通信會使用SSL加密。使得有攻擊者的環境也沒啥問題。對於加密的更多信息參考17.8節。

要初始化SSL加密的連接,前端要發送SSLRequest報文,而不是StartupMessage。伺服器響應一個位元組S或N,表示可以使用和不能使用SSL。如果前端收到的不是預期的,應該立即關閉連接。在S之後,可以執行SSL起始握手(這裡不描述,是SSL規範的一部分)。如果成功,就繼續發送普通的StartupMessage。這時StartupMessage和所有數據都是SSL加密的了。在N之後,發送普通StartupMessage而無加密。

前端也應該準備好接收ErrorMessage,發生於還沒有SSL支持的PostgreSQL。這時連接會關閉,但是前端可以選擇打開一個新的連接而不不使用SSL請求。

一個初始化SSLRequest也可以在已經打開的連接中發送CancelRequest報文。

協議本身不提供強制使用SSL加密,管理員可以配置伺服器拒絕非加密會話,作為認證檢查的備份。

3 報文數據類型

Documentation: 8.3: Message Data Types

這一節描述報文中的基本數據類型。

  1. Intn(i)

    n一個網路位元組序的n bit整數,如果i指定了,則表示實際出現的數字,否則這個值為變數。例如 Int16Int32(42)

  2. Intn[k]

    n一個k個元素的n bit整數的數組,網路位元組序。數組長度k是由報文的前一欄位決定的。

  3. String(s)

    n一個null結尾的C風格字元串。沒有限制長度,如果s指定了,就是實際顯示的值,否則就是變數。例如 StringString("user")注意 :沒有對字元串長度的預定義限制。前端的好編碼策略是使用可擴展緩衝區,所以任何內存可以接受的東西都可以處理。如果不可行,讀取整個字元串,然後丟棄緩衝區裝不下的東西。

  4. Byten(c)

    n嚴格的n位元組。如果欄位寬度n不是常量,就是依據前一個欄位。如果c指定了就是實際值。例如 Byte2Byte1(n)

4 報文格式

postgresql.org/docs/8.3

本節描述每個消息報文的具體格式。報文上標註了發送方:前端(F)、後端(B)、雙方(F&B)。注意,儘管每個報文在開始處都包含一個位元組數,消息格式的結束可以不在位元組數引用中。其目標是合法性檢測。(CopyData報文是個例外,因為他的格式部分與數據流;任何非法CopyData數據都無法被中斷)。

AuthenticationOK (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(0) :制定認證成功了

AuthenticationKerberosV5 (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(2) :指定Kerberos V5認證需求

AuthenticationCleartextPassword (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(3) :指定需要clear-text密碼

AuthenticationCryptPassword (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(10) :內容長度,包含自身
  3. Int32(4) :指定需要crypt()加密過的密碼
  4. Byte2 :加密時所需salt

AuthenticationMD5Password (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(12) :內容長度,包含自身
  3. Int32(5) :指定需要MD5加密的密碼
  4. Byte4 :加密時用到的salt

AuthenticationSCMCredential (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(6) :標誌使用SCM認證

AuthenticationGSS (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(7) :標誌使用GSS API認證

AuthenticationSSPI (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32(8) :內容長度,包含自身
  3. Int32(9) :標誌使用SSPI認證

AuthenticationGSSContinue (B)

  1. Byte1(R) :標誌報文為認證請求
  2. Int32 :內容長度,包含自身
  3. Int32(8) :指定報文包含GSSAPI或SSPI數據
  4. Byten :GSSAPI或SSPI認證數據

BackendKeyData (B)

  1. Byte1(K) :標誌報文為取消關鍵數據,前端必須保存這些值,以便用於之後的CancelRequest報文
  2. Int32(12) :內容長度,包含自身
  3. Int32 :後端processID
  4. Int32 :後端的密鑰

Bind (F)

  1. Byte1(B) :標誌報文為Bind命令
  2. Int32 :內容長度,包含自身
  3. String :目標portal的名字(空字元串表示未命名)
  4. String :源prepared statement的名字(空字元串表示未命名)
  5. Int16 :隨後跟著的參數格式碼(就是下面的C),0表示沒參數或使用文本格式;1表示二進位格式;或者等於參數個數
  6. Int16[C] :參數格式碼,每個都必須取值0(文本)或1(二進位)
  7. Int16 :跟隨的參數值(可以是0)。必須匹配查詢時參數個數

隨後是每個參數的欄位對:

  1. Int32 :參數值的長度(位元組,不包含自身),可以是0,-1表示NULL,在NULL之後沒有跟隨的位元組
  2. Byten :參數值,由之前指定的格式,n是上面的長度

在最後一個參數後的欄位:

  1. Int16 :返回列的格式碼(下面的R),可以是0表示無結果或文本格式,1表示二進位格式,或者等於結果列的數量
  2. Int16[R] :結果列格式碼,每個都必須取值0(文本)或1(二進位)

BindComplete (B)

  1. Byte1(2) :標誌報文為綁定完成
  2. Int32(4) :內容長度,包含自身

CancelRequest (F)

  1. Int32(16) :內容長度,包含自身
  2. Int32(80877102) :取消請求的代碼,值取自1234*0xffff+5678,為了避免混亂,這個代碼必須不能與協議版本號相同
  3. Int32 :目標後端進程ID
  4. Int32 :目標後端密鑰

Close (F)

  1. Byte1(C) :標誌關閉命令報文
  2. Int32 :內容長度,包含自身
  3. Byte1 :S關閉prepared statement;P關閉portal
  4. String :要關閉的prepared statement或portal的名字,空字元串選擇未命名的

CloseComplete (B)

  1. Byte1(3) :標誌關閉完成報文
  2. Int32(4) :內容長度,包含自身

CommandComplete (B)

  1. Byte1(C) :標誌命令完成響應
  2. Int32 :內容長度,包含自身
  3. String :命令標籤(tag)。通常是一個單詞表示剛完成的SQL命令:
    1. 對INSERT命令,標籤是 INSERT oid rows 參數rows是插入行數,oid是當插入1行時的對象ID,否則oid為0
    2. 對DELETE命令,標籤是 DELETE rows ,rows是刪除的行數
    3. 對UPDATE命令,標籤是 UPDATE rows ,rows是更新的行數
    4. 對MOVE命令,標籤是 MOVE rows ,rows是游標移動行數
    5. 對FETCH命令,標籤是 FETCH rows ,rows是獲取的行數
    6. 對COPY命令,標籤是 COPY rows ,rows是拷貝的行數(注意行數僅在PostgreSQL 8.2以後有顯示)
    7. CREATE TABLE

CopyData (F&B)

  1. Byte1(d) :標誌COPY數據報文
  2. Int32 :內容長度,包含自身
  3. Byten :COPY數據流。從後端發來的總是表示單行數據,但是前端發來的可能被無意義的分割

CopyDone (F&B)

  1. Byte1(c) :標誌COPY完成報文
  2. Int32(4) :內容長度,包含自身

CopyFail (F)

  1. Byte1(f) :標誌COPY失敗報文
  2. Int32 :內容長度,包含自身
  3. String :錯誤信息描述失敗原因

CopyInResponse (B)

  1. Byte1(G) :標誌開始拷貝入響應,前端必須立即發送拷貝進入數據(如果沒有準備好,就發送CopyFail報文)
  2. Int32 :內容長度,包含自身
  3. Int8 :0表示全部COPY格式是文本(行以新行分隔,列以分隔符分隔),1表示所有拷貝數據格式是二進位(類似於DataRow格式)
  4. Int16 :拷貝進來的列數量,其數字是下面的N
  5. Int16[N] :每個列的格式碼,每個必須是0(文本)或1(二進位)。如果所有拷貝格式是文本則必須全部是0

CopyOutResponse (B)

  1. Byte1(H) :標誌開始拷貝出響應,這個報文必須跟隨著拷貝出的數據
  2. Int32 :內容長度,包含自身
  3. Int8 :0表示文本格式(行以新行分隔,列以分隔符分隔),1表示二進位格式(類似於DataRow格式),查看COPY命令了解更多
  4. Int16 :列的數量(下面的N)
  5. Int16[N] :每個列的格式碼,必須取值0或1,分別表示文本和二進位

DataRow (B)

  1. Byte1(D) :標誌數據行報文
  2. Int32 :內容長度,包含自身
  3. Int16 :隨後列的數值(可能是0)

接下來的是每一列的欄位對:

  1. Int32 :列值的長度,單位是位元組(不包含自身),可以為0,特殊情況-1表示NULL值,且NULL時沒有跟隨的值位元組串
  2. Byten :列的值,有如格式碼描述的,n是如上長度

Describe (F)

  1. Byte1(D) :標誌描述命令報文
  2. Int32 :內容長度,包含自身
  3. Byte1 :S表示prepared statement;P表示portal
  4. String :prepared statement或portal的名字,空字元串選擇未命名的

EmptyQueryResponse (B)

  1. Byte1(I) :標誌查詢結果為空的響應,作為CommandComplete的替代(substitute)
  2. Int32(4) :內容長度,包含自身

ErrorResponse (B)

  1. Byte1(E) :標誌錯誤信息報文
  2. Int32 :內容長度,包含自身

消息體由一個或多個欄位組成,以null結尾,欄位可以任何順序出現,對每個欄位應該有:

  1. Byte1 :代碼用以標誌欄位類型;如果0則表示消息結束符,後面沒字元串了。描述定義欄位類型鍵45.5節。更多欄位類型可能在未來添加進來,前端應該無聲的忽略不識別的類型
  2. String :欄位值

Execute (F)

  1. Byte1(E) :標誌為執行命令
  2. Int32 :內容長度,包含自身
  3. String :portal的名字,空字元串選擇未命名portal
  4. Int32 :最大允許返回行數量,0表示無限制

Flush (F)

  1. Byte1(H) :標誌清空命令
  2. Int32(4) :內容長度,包含自身

FunctionCall (F)

  1. Byte1(F) :標誌是函數調用報文
  2. Int32 :內容長度,包含自身
  3. Int32 :指定函數的對象ID
  4. Int16 :參數格式碼(下面的C),0表示無參數或文本,1表示二進位,也可以是實際參數個數
  5. Int16[C] :參數格式碼,必須取值0或1,表示文本或二進位
  6. Int16 :指定提供給函數的參數個數

下面是每個參數的欄位對:

  1. Int32 :參數值長度(位元組,不包含自身),可以是0,-1表示NULL,NULL沒有跟隨的位元組
  2. Byten :參數值,按照上面給定格式碼,長度為上面欄位n

在這些參數後,跟隨的欄位:

  1. 函數結果的格式碼,必須取值0或1

FunctionCallResponse (B)

  1. Byte1(V) :標誌報文是函數調用結果
  2. Int32 :內容長度,包含自身
  3. Int32 :函數結果值長度(位元組,不包含自身),可以是0,-1表示NULL,NULL後無跟隨位元組
  4. Byten :函數結果值,按照上面給定的格式碼和長度

NoData (B)

  1. Byte1(n) :標誌無數據
  2. Int32(4) :內容長度,包含自身

NoticeResponse (B)

  1. Byte1(N) :標誌通知報文
  2. Int32 :內容長度,包含自身

消息體由一個或多個欄位構成,跟隨0表示結束符。欄位可以用任何順序顯示,每個欄位為:

  1. Byte1 :代碼表示欄位類型;如果0則為消息結束符。欄位類型定義見45.5節。未來可能添加更多欄位類型,前端應該無聲的忽略不識別的欄位
  2. String :欄位值

NotificationResponse (B)

  1. Byte1(A) :標誌通知響應報文
  2. Int32 :內容長度,包含自身
  3. Int32 :通知的後端進程ID
  4. String :拋出通知的條件(condition)名字
  5. String :通知附加信息(當前尚未實現,所以該欄位總是空字元串)

ParameterDescription (B)

  1. Byte1(t) :標誌參數描述報文
  2. Int32 :內容長度,包含自身
  3. Int16 :語句使用的參數數量(可以是0)

下面對每個參數:

  1. Int32 :指定參數數據類型的對象ID

ParameterStatus (B)

  1. Byte1(S) :標誌運行時參數狀態報告
  2. Int32 :內容長度,包含自身
  3. String :運行時參數名字
  4. String :參數值

Parse (F)

  1. Byte1(P) :標誌Parse命令報文
  2. Int32 :內容長度,包含自身
  3. String :目標prepared statement的名字(空字元串選擇未命名的)
  4. String :要解析的查詢字元串
  5. Int16 :參數數據類型數字(可以是0)。注意這裡不是參數在查詢字元串中的佔位符,只有前端想要預備的類型

然後對每個參數:

  1. Int32 :指定參數數據類型的對象ID。防止0等同於讓類型不指定

ParseComplete (B)

  1. Byte1(1) :標誌解析完成報文
  2. Int32(4) :內容長度,包含自身

PasswordMessage (F)

  1. Byte1(p) :標誌密碼響應,同時用於GSSAPI和SSPI的響應(這是個設計錯誤,因為包含數據不是以null結束的,可以是任意二進位數據)
  2. Int32 :內容產度,包含自身
  3. String :密碼(按照請求不同,可能是加密的)

PortalSuspended (B)

  1. Byte1(s) :標誌portal掛起報文,注意只在Execute報文的行數限制到達時會顯示
  2. Int32(4) :內容長度,包含自身

Query (F)

  1. Byte1(Q) :定義這是個簡單查詢
  2. Int32 :內容長度,包含自身
  3. String :查詢字元串

ReadyForQuery (B)

  1. Byte1(Z) :標誌報文為後端準備好接收新的查詢
  2. Int32(5) :內容長度,包含自身
  3. Byte1 :當前後端事務狀態碼。"I"表示空閑(沒有在事務中)、"T"表示在事務中;"E"表示在失敗的事務中(事務塊結束前查詢都回被駁回)

RowDescription (B)

  1. Byte1(T) :標誌報文為行描述
  2. Int32 :內容長度,包含自身
  3. Int16 :指定欄位數量(可以為0)

然後就是各個欄位了

  1. String :欄位名,注意有結尾的NULL
  2. Int32 :如果欄位是特定表的列,則是表格的對象ID,否則為0
  3. Int16 :如果欄位是特定表的列,則是列的屬性名,否則為0
  4. Int32 :欄位數據類型對象ID,詳見下面的實踐筆記
  5. Int16 :數據類型大小(查看pg_type.typlen),注意負數表示變長類型,-1為varlena,-2為NULL結尾C字元串,其他為固定長度
  6. Int32 :類型修飾符(查看pg_attribute.atttypmod),修飾符是類型相關的
  7. Int16 :欄位的格式碼,當前0為文本,1為二進位。如果是從Describe的語句變數返回的RowDescription,則格式碼未知,總是返回0

SSLRequest (F)

  1. Int32(8) :內容長度,包含自身
  2. Int32(80877103) :SSL請求碼,值來自1234*0xffff+5679。為了避免混亂,這個代碼必須不能與協議版本號相同

StartupMessage (F)

  1. Int32 :內容長度,包含自身
  2. Int32(196608) :協議版本號,最重要的是16bit的主版本號3。隨後是次版本號0。196608=0x00030000。

協議版本號之後就是多個鍵值對,最後一個鍵值對結尾要有null字元,鍵值對參數可以用任何順序出現, user 是必須的,其他都可選。

  1. String :參數名,當前可以被識別的鍵名包括:user(資料庫用戶名)、database(要連接的資料庫,默認是用戶名)、options(後端命令行選項)。
  2. String :參數值

Sync (F)

  1. Byte1(S) :標示同步命令
  2. Int32(4) :內容長度,包含自身

Terminate (F)

  1. Byte1(X) :標誌終止命令
  2. Int32(4) :內容長度,包含自身

5 錯誤與提示信息欄位

Error and Notice Message Fields

本節描述ErrorResponse和NoticeResponse報文中的欄位。每個欄位類型有一個單一位元組記號(token)。注意每個給定欄位至少出現在一個報文中。

S(Severity) :嚴重程度,欄位內容是ERROR、FATAL、PANIC(在錯誤信息中)、WARNING、NOTICE、DEBUG、INFO、LOG(在notice信息中)或本地翻譯。

C(Code) :錯誤的SQLSTATE代碼,非本地化的

M(Message) :人類可讀的錯誤信息,一般很短且只有1行

D(Detail) :可選的第二錯誤信息,包含問題的詳細描述,可能多行

H(Hint) :可選的問題建議,建議不同於detail的是包含建議,可能多行

P(Position) :欄位值的ASCII碼整數,引用錯誤查詢語句的游標位置,第一個字元是1,以字元引用,而不是位元組

p(internal position) :定義類似於P欄位,只不過用於引用內部生成命令的錯誤位置,當這個欄位顯示則q欄位也總是顯示

q(internal query) :內部生成的查詢命令,例如PL/pgSQL函數

W(Where) :一段索引正文,包括調用棧和當前處理語言函數的內部查詢,每行一個

F(File) :發生錯誤的源碼文件名

L(Line) :發生錯誤的源碼行號

R(Routine) :發生錯誤的源碼常規

客戶端選擇如何顯示這些信息,一般至少應該對較長的行來折行,以及分段。

6 從Protocol2.0的改變

Summary of Changes since Protocol 2.0

@waiting...

7 調試經驗

7.1 開頭的SSL確認

實際安裝了PostgreSQL8.3的客戶端和伺服器以後,使用代理伺服器測試發現默認使用的是SSL。所以進入了postgres.conf修改了 ssl=false ,然後重啟(restart)伺服器。才可以以明文方式通信。

實際的協議中,第一個通信報文並不是startup包,而應該算是詢問是否使用SSL的包。前端發來的包是 x00x00x00x08x04xd2x16/ ,其中去除前4位元組長度,剩餘部分應該是用於詢問是否支持SSL通信,如果支持優先使用SSL。

後端對此詢問只需要回復一個位元組,"S"表示支持SSL,我沒有繼續深究。如果回復"N"則不支持SSL。然後就是正常的Startup流程。

7.2 驗證過程

正式收到版本號為 x00x03x00x00 的Startup包以後,後端發送驗證需求。然後前段回復PasswordMessage。對於ClearText類密碼的格式就是一個字元串,當然末尾有null。

驗證通過後後端應該發送AuthenticationOK報文。不過這還沒結束,後端還需要發一系列的報文才能完成整個驗證和初始化,而這期間前端無需發送任何報文。

按照順序,尚未解析的各個報文如下:

  1. Sx00x00x00x19client_encodingx00UTF8x00
  2. Sx00x00x00x17DataStylex00ISO, YMDx00
  3. Sx00x00x00x19integer_datatimesx00onx00
  4. Sx00x00x00x14is_superuserx00onx00
  5. Sx00x00x00x19server_encodingx00UTF8x00
  6. Sx00x00x00x1aserver_versionx008.3.11x00
  7. Sx00x00x00#session_authorizationx00postgresx00
  8. Sx00x00x00$standard_conforming_stringsx00offx00
  9. Sx00x00x00x11TimeZonex00PRCx00
  10. Kx00x00x00x0cx00x00&xefY3>xc1
  11. Zx00x00x00x05I

整體數據是:

Rx00x00x00x08x00x00x00x00

Sx00x00x00x19client_encodingx00UTF8x00

Sx00x00x00x17DateStylex00ISO, YMDx00

Sx00x00x00x19integer_datetimesx00onx00

Sx00x00x00x14is_superuserx00onx00

Sx00x00x00x19server_encodingx00UTF8x00

Sx00x00x00x1aserver_versionx008.3.11x00

Sx00x00x00#session_authorizationx00postgresx00

Sx00x00x00$standard_conforming_stringsx00offx00

Sx00x00x00x11TimeZonex00PRCx00

Kx00x00x00x0cx00x00&xefY3>xc1

Zx00x00x00x05I

先不管那麼多,先一起發過去,客戶端已經算是登錄成功了。可以發送請求了。

7.3 密碼確認

客戶端連接伺服器,伺服器如果要求密碼,則客戶端會不發送任何數據包立刻斷開,等待用戶輸入完密碼以後重新連接。至少psql命令就是如此。

實際的MD5密碼生成方法為:

result=md5+md5sum(md5sum(password+username)+saltstr)

7.4 錯誤碼

一個沒有找到表的基本錯誤信息 Sxe9x94x99xe8xafxafx00C42P01x00Mxe5x85xb3xe7xb3xbb "testtest" xe4xb8x8dxe5xadx98xe5x9cxa8x00Fnamespace.cx00L273x00RRangeVarGetRelidx00x00 。其中分解開就是每一小段以一個字母開頭,隨後是NULL結尾字元串。最後再加一個NULL。這些開頭的字母如上面講解的。

其中C(CODE)欄位存儲的是SQLSTATE,這是個5位元組數組。這5個位元組中可以包含數字和大寫字母,前2個字元代表基本錯誤類,後3個字元表示錯誤子類。成功的SQLSTATE是"00000"。這些都是SQL標準定義的。建議以後都使用SQLSTATE而不是各個資料庫自己的錯誤碼。

一些SQLSTATE示例: postgresql.org/docs/8.3

SQLSTATE與錯誤碼: Documentation: 8.3: Error Handling

幾個常用的:

  1. 00000 :成功
  2. 01000 :警告
  3. 02000 :無數據
  4. 03000 :SQL語句不完整
  5. 08000 :連接異常
  6. 0A000 :不支持的功能
  7. 22000 :數據異常
  8. 42000 :語法錯誤
  9. 54000 :程序範圍限制
  10. 54001 :語句太複雜
  11. 54011 :太多的列
  12. 54023 :太多參數
  13. 58030 :I/O錯誤
  14. P0000 :PLPGSQL ERROR
  15. P0001 :拋出異常
  16. P0002 :沒找到數據
  17. P0003 :太多行
  18. XX000 :內部錯誤
  19. XX001 :數據被誤用
  20. XX002 :索引被誤用

7.5 結果集的定義

使用RowDescription包。

一個結果集定義 x00x01namex00x00x00@x03x00x02x00x00x04x13xffxffx00x00x00Dx00x00 。其中欄位定義如下:

  1. 欄位數16bit:1個
  2. 欄位名: namex00
  3. 特定表的表格對象ID: x00x00@x03
  4. 特定表的列屬性對象ID: x00x02
  5. 欄位數據類型對象ID: x00x00x04x13
  6. 數據類型長度: xffxff
  7. 類型修飾符: x00x00x00D
  8. 欄位格式碼: x00x00

實際這是一個VARCHAR類型的欄位。即類型代碼1043對應varchar類型。具體的類型映射表可以進入PGSQL自己查詢,使用 SELECT typname,oid FROM pg_type; ,我使用8.3.11,其中有269個返回結果。列出幾個常用的:

  1. bool=16
  2. bytea=17
  3. char=18
  4. name=19
  5. int8=20
  6. int4=23
  7. text=25
  8. xml=142
  9. float4=700
  10. float8=701
  11. abstime=702
  12. reltime=703
  13. inet=869
  14. varchar=1043
  15. date=1082
  16. time=1083
  17. timestamp=1114
  18. numeric=1700
  19. uuid=2950
  20. record=2249
  21. cstring=2275
  22. trigger=2279
  23. parameters=11393

推薦閱讀:

TiKV 源碼解析系列 - Lease Read
MySQL中單引號和反引號的區別是什麼?
消息隊列如果持久化到資料庫的話,相對於直接操作資料庫有啥優勢?
時間序列數據的存儲和計算 - 開源時序資料庫解析

TAG:PostgreSQL | 网络协议 | 数据库 |