基於定長消息的java nio半包粘包處理

什麼是tcp半包粘包?
簡單來講就是接收到的tcp包並不一定是一個完整的包。
它可能是1個包的一部分,也可能是多個完整包加上1個包的一部分。
爲什麼?
因爲tcp的定義是面向字節流的傳輸協議,所以操作系統實現這個協議的時候,只保證字節的正確傳輸,而至於字節的應用層語義(可能這個字節是個分隔符,也可能這個字節和周圍3個字節組成一個int,代表類的某個字段),操作系統是不管的。
比如下面這個例子(基於java):

public class Account {
    private int accountnum;
    private double balance;
    private int num;
}

要傳輸這個Account對象,實際上就是傳輸它的三個字段accountnum,balance和num,它們的大小分別是4,8,4個字節。當傳輸時,實現tcp協議的系統只負責把這16個字節傳輸到接收方,而不知道這些字節的含義。比如前4個字節是accountnum,然而這是應用層的語義,實現tcp協議的系統並不知道,也不需要知道,因爲tcp規範裏就沒規定需要知道上層的語義。
那麼接收方如何接收呢?
對於這個例子實在是太簡單了,接收方只需要每次都接收16個字節就能保證每一次都能得到一個完整的Account對象,連分隔符都不需要。在已知確定傳輸對象長度(字節數目)的時候:

即使接收到的tcp包並不一定是一個完整的包。
接收方只需要等待,直到讀到確定數量的字節,然後處理即可。
比如現在只傳輸了4個字節,我們知道16個字節才能組成完整的Account對象,那麼再讀12個字節後進行處理即可。

這個例子有什麼意義?
根據這個例子受到啓發,只要傳輸對象的長度是確定的,那麼接收端很容易就能夠對傳輸對象進行解析(就是處理tcp粘包半包)。
然而對象的長度是確定的嗎?往往都不是,比如一個上面的對象現在加一個String類型的成員字段,這個String字段變成字節的時候長度就是未知的,但這並不影響我們把它變成定長的對象。
HOW?
設置最大傳輸長度,每次都接收最大傳輸長度的字節流。而這個字節流的前4個字節用於表示對象的長度,接下來的字節就是傳輸的對象的字節流,最後不夠最大長度的用任意字節進行填充即可。
比如:

public class Account {
private int accountnum;
private double balance;
private int num;
private String extra; 
}

對於增加了String類型字段extra的新Account類來說,它的一個對象長度是不確定的,現在要傳輸它該怎麼辦?設置最大的長度爲400字節,前4個字節存儲實例對象的長度x,之後的x個字節爲對象,最後沒用到的位置用0x0(也就是0)來填充。比如下圖所示:

圖片描述

需要注意的是,前四個字節只是字節,並不是x,需要把這4個字節轉成int類型的變量,然後這個int變量對應的10進制數是x。
這個方式看起來具有很明顯的侷限性?
長度是有限制的?比如一次只能傳輸最多400-4=396個字節的對象?
但是可以把超大的對象再次分開,每一次都只傳輸最大包(400)長度,然後再拼接即可解決。
比如現在設計這400個字節的存儲格式是這樣的:
前4個字節存儲這個對象總共被分成幾個最大傳輸的包,接着的4個字節存儲這是第幾個,然後是長度,然後是內容,最後是填充。
圖片描述

這樣看起來最大長度就解決了。
然而。。。基於java nio的傳輸適合傳輸大文件(巨長的字節流)嗎?
nio是什麼原理?
是I/O多路複用,簡單來講就是我有一個叫做選擇器(Selector)的類不斷輪詢不同的連接(Socket)的i/o事件,發現了i/o事件就處理,處理時可以用Selector所在的線程,也可以另開啓一個線程。如果要用Selector的線程處理i/o事件,那麼i/o的操作時間必須很短,否則可能會丟失消息,而如果開啓一個線程,i/o的時間也應該很短。why?因爲如果i/o時間很長,並且線程很多,那麼就退化成了bio的模型。。。那麼就沒必要用nio了。。。
歸結起來就是nio就不適合多用戶傳輸大文件,否則必然退化爲bio模型。
所以實際上不要考慮這種大文件的傳輸,如果要傳輸大文件還是用bio模型比較好,並且在bio的傳輸模式下java提供了對象的序列化和反序列化,這樣都不需要我們定義長度字段了。

具體的代碼
參考:https://github.com/ItCrazyer/...

說明一點的是,這個例子裏傳輸的對象是不定長的字符串,不是一個定義的類(不是像上面的Account這種),並且使用了SelectionKey對象的attachment方法,來暫存數據,暫存數據存儲在TempData這個類的對象裏,爲什麼?
因爲雖然我們知道確定的長度(比如是600),並且據此處理定長的數據,但是一次傳來的數據很可能是好幾個定長的數據包,而且每一次我們都必須讀完,比如傳來了1300字節的數據,就必須1300字節都讀完,不能這一次i/o事件我只讀600,然後下一次i/o事件在讀接下來的700個字節,那是沒辦法做到的,因爲i/o響應的是這一次可讀,並不響應你還有數據,所以這次不讀完下次就沒有了。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章