在網絡傳輸中,粘包和半包應該是最常出現的問題,做爲 Java 中最常使用的 NIO 網絡框架 Netty,它又是如何解決的呢?今天就讓咱們來看看。 git
TCP 傳輸中,客戶端發送數據,實際是把數據寫入到了 TCP 的緩存中,粘包和半包也就會在此時產生。github
客戶端給服務端發送了兩條消息ABC
和DEF
,服務端這邊的接收會有多少種狀況呢?有多是一次性收到了全部的消息ABCDEF
,有多是收到了三條消息AB
、CD
、EF
。緩存
上面所說的一次性收到了全部的消息ABCDEF
,相似於粘包。若是客戶端發送的包的大小比 TCP 的緩存容量小,而且 TCP 緩存能夠存放多個包,那麼客戶端和服務端的一次通訊就可能傳遞了多個包,這時候服務端從 TCP 緩存就可能一下讀取了多個包,這種現象就叫粘包
。網絡
上面說的後面那種收到了三條消息AB
、CD
、EF
,相似於半包。若是客戶端發送的包的大小比 TCP 的緩存容量大,那麼這個數據包就會被分紅多個包,經過 Socket 屢次發送到服務端,服務端第一次從接受緩存裏面獲取的數據,實際是整個包的一部分,這時候就產生了半包
(半包不是說只收到了全包的一半,是說收到了全包的一部分)。框架
其實從上面的定義,咱們就能夠大概知道產生的緣由了。性能
粘包的主要緣由:編碼
半包的主要緣由:code
其實咱們能夠換個角度看待問題:cdn
收發
的角度看,即是一個發送可能被屢次接收,多個發送可能被一次接收。 傳輸
的角度看,即是一個發送可能佔用多個傳輸包,多個發送可能共用一個傳輸包。 根本緣由,實際上是get
TCP 是流式協議,消息無邊界。
(PS : UDP 雖然也能夠一次傳輸多個包或者屢次傳輸一個包,但每一個消息都是有邊界的,所以不會有粘包和半包問題。)
就像上面說的,UDP 之因此不會產生粘包和半包問題,主要是由於消息有邊界,所以,咱們也能夠採起相似的思路。
將 TCP 鏈接改爲短鏈接,一個請求一個短鏈接。這樣的話,創建鏈接到釋放鏈接之間的消息即爲傳輸的信息,消息也就產生了邊界。
這樣的方法就是十分簡單,不須要在咱們的應用中作過多修改。但缺點也就很明顯了,效率低下,TCP 鏈接和斷開都會涉及三次握手以及四次握手,每一個消息都會涉及這些過程,十分浪費性能。
所以,並不推介這種方式。
封裝成幀(Framing),也就是本來發送消息的單位是緩衝大小,如今換成了幀,這樣咱們就能夠自定義邊界了。通常有4種方式:
這種方式下,消息邊界也就是固定長度便可。
優勢就是實現很簡單,缺點就是空間有極大的浪費,若是傳遞的消息中大部分都比較短,這樣就會有不少空間是浪費的。
所以,這種方式通常也是不推介的。
這種方式下,消息邊界也就是分隔符自己。
優勢是空間再也不浪費,實現也比較簡單。缺點是當內容自己出現分割符時須要轉義,因此不管是發送仍是接受,都須要進行整個內容的掃描。
所以,這種方式效率也不是很高,但能夠嘗試使用。
這種方式,就有點相似 Http 請求中的 Content-Length,有一個專門的字段存儲消息的長度。做爲服務端,接受消息時,先解析固定長度的字段(length字段)獲取消息總長度,而後讀取後續內容。
優勢是精肯定位用戶數據,內容也不用轉義。缺點是長度理論上有限制,須要提早限制可能的最大長度從而定義長度佔用字節數。
所以,十分推介用這種方式。
其餘方式就各不相同了,好比 JSON 能夠當作是使用{}
是否成對。這些優缺點就須要你們在各自的場景中進行衡量了。
Netty 支持上文所講的封裝成幀(Framing)中的前三種方式,簡單介紹下:
方式 | 解碼 | 編碼 |
---|---|---|
固定長度 | FixedLengthFrameDecoder | 簡單 |
分割符 | DelimiterBasedFrameDecoder | 簡單 |
專門的 length 字段 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
今天主要介紹了粘包和半包問題、解決思路和 Netty 中的支持,我會在下一篇文章裏重點講述 Netty 中的具體實現,敬請期待。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。