如何自定義協議

語言: CN / TW / HK

前言

何為自定義協議,其實是相對標準協議來說的,這裡主要針對的是應用層協議;常見的標準的應用層協議如http、ftp、smtp等,如果我們在網路通訊的過程中不去使用這些標準協議,那就需要自定義協議,比如我們常用的RPC框架(dubbo,thrift),分散式快取(redis,memcached)等都是自定義協議;本文就來講講如何去自定義私有協議,在此之前我們先考慮一下為什麼要自定義協議。

為什麼要自定義協議

直接使用標準的協議好處是顯而易見的,我個人理解的幾點優點:

  • 既然是標準協議說明已經成為了標準,這樣很多系統就可以直接對接,無縫整合;
  • 協議最重要的一點就是編碼解碼,標準協議往往有現成的編碼解碼包,直接拿來使用,減少開發時間;
  • 有很多圍繞標準協議的第三方測試工具,可以很方便的進行測試;

既然有這麼多優點那我們為什麼還要去自定義協議,大致出於以下幾點考慮:

  • 既然是標準協議,往往兼顧的東西比較多,導致協議資料相對來說比較大,這樣可能在一些追求效能,流量的系統中不能容忍;
  • 標準協議有很多,沒有哪一種協議可以適用任何場景中,所以如果在某個場景中還沒有既定的標準協議,這時候會有各種私有協議;
  • 自定義協議只要雙方約定好資料結構就行,不具有通用性,理論上來說會更加安全一點,當然現在很多標準協議都有安全版本,比如https,sftp等等;

以上只是個人的一點理解,歡迎大家補充;關於如何去自定義協議,其實可以去多參考一些主流的標準協議或者私有協議,其實有很多共同點可以去借鑑;下面先簡單看看那些主流的協議;

主流協議

下面分別看看一些主流的標準協議或者私有協議都是如何去定義自己的資料結構的,對我們有非常好的借鑑意義;

http協議

http協議大家最熟悉不過了,全稱叫超文字傳輸協議,整個請求報文可以分為三個部分分別是:請求行,請求報頭,請求正文;

  • 請求行
GET /test.html HTTP/1.1 (CRLF換行)
  • 請求報頭
Accept-Encoding: gzip, deflate
Content-Length: 38 
Content-Encoding: gzip
...

請求包頭有很多,每一個代表了各自的含義,這邊就不一一列出,我們這裡更加關注整個報文的結構;

  • 請求正文

這個只有在POST請求的時候才有正文,裡面存放業務資料,比如常見的json文字串;具體正文的長度可以根據訊息頭中的Content-Length來決定;

dubbo協議

dubbo協議格式可以直接參考官網提供的如下圖片:
image
看上圖其實整個協議資料包也大致分為兩個部分:固定部分和可變部分,或者叫訊息頭和訊息體;
固定部分一共是4+8+4=16個位元組,具體如下所示:

header{
    Magic High       = 8bit;  //魔數高位
    Magic Low        = 8bit;  //魔數低位
    Req/Res          = 1bit;  //標識是請求或響應
    2 Way            = 1bit;  //標記是否期望從伺服器返回值
    Event            = 1bit;  //標識是否是事件訊息
    Serialization ID = 5bit;  //標識序列化型別
    Status           = 8bit;  //標識響應的狀態
    Request ID       = 64bit; //標識唯一請求
    Data Length      = 32bit; //序列化後的內容長度
}

可變部分根據固定部分中的Data Length來確定長度;

redis協議

Redis的客戶端與服務端採用叫做 RESP(Redis Serialization Protocol)的網路通訊協議交換資料,相對來說還是比較簡單的,以下是這個協議的一般形式:

*< 引數數量 > CR LF
$< 引數 1 的位元組數量 > CR LF
< 引數 1 的資料 > CR LF
...
$< 引數 N 的位元組數量 > CR LF
< 引數 N 的資料 > CR LF

以上大致介紹了三種比較有代表性的協議,雖然說每種協議都有各自的使用場景,但是如果我們自己去定義協議,還是有一些相通的東西;

如何自定義協議

下面我們重點看看去自定義協議有哪些需要我們關注的點,以下是本人根據自己的理解整理了如下關注點:

  • 完整的資料包
  • 協議號
  • 訊息頭標識
  • 業務資料
  • 預留欄位

下面分別逐一詳細介紹:

完整的資料包

我們平時經常講資料包,但是TCP其實只有流的概念,並沒有資料包的概念;那很重要的一點就是我們的程式怎麼知道現在的業務資料已經接受全部接收完了,可以作為一個完整的資料包去處理了,如果不去做處理的話就會出現我們常說的半包和粘包問題;主流的的處理方式大致有這麼兩種:

  • 在訊息頭部加上資料包長度描述,比如在http協議和dubbo協議中出現的dataLength欄位;
  • 用特殊的字串作為資料包的結尾,這樣我們在接受資料的時候接受到預定的特殊字串就表示資料包完整了;

協議號

可能不同的協議有不同的叫法,我這裡把它叫做協議號,個人理解就是根據這個協議號,伺服器端知道去執行什麼邏輯;比如http協議請求行中的/test.html,dubbo協議中的服務名+版本號,redis中的具體要執行什麼key;

訊息頭標識

這個是否需要還是要看各自的場景,比如redis協議足夠簡單,無需任何標識,所有的東西都是雙端約定好的;但是其他很多協議還是有一些需要的,除了上面說到的可以在訊息頭中指定dataLength,其實還有很多其他的東西可以指定比如:

  • 業務資料格式:文字格式,json格式,html格式等等;
  • 壓縮格式:可能為了追求流量包大小對資料包進行壓縮,gzip、deflater、snappy等;
  • 加密演算法:可能需要對我的業務資料進行加密處理,保證業務資料的安全性AES、DES等;

業務資料

業務資料往往在整個資料包中是最大的,同時也是大小可變的部分;我們上面所做的這些其實都是在為業務資料服務,業務資料需要在網路傳輸,最重要的一點就是序列化,一般就以下兩種方式:

  • 文字方式:序列化文字文件text,或者json串,xml格式等;
  • 二進位制方式:常見的比如protobuf,thrift,kyro等;

預留欄位

是否需要預留欄位這個得看情況,比如http協議整個訊息頭是可變的,每一行一個標識,知道讀取到空行,表示訊息頭結束下面就是正文了,可以理解為http使用了兩種方式來保證完整包,訊息頭使用特殊字元結尾,正文使用在訊息頭中指定dataLength;這種方式其實它的整個擴充套件性是非常好的;
另外一種像dubbo這樣,其實它的頭部相當於已經固定好了16個位元組,這種情況下是否可以預留幾個位元組防止後面的變更;

總結

自定義協議其實在我們真正的工作中還是很少能接觸到的,更多的其實還是去實現業務,但是我們系統無時無刻不在和各種應用層協議打交道,如果我們瞭解了各種協議,在系統出現問題時可以做抓包分析;另外像我們常用的資料庫中介軟體、快取中介軟體等,都需要對協議都充分的瞭解,然後去實現代理。

感謝關注

可以關注微信公眾號「 回滾吧程式碼」,第一時間閱讀,文章持續更新;專注Java原始碼、架構、演算法和麵試