粘包拆包問題是處於網絡比較底層的問題,在數據鏈路層、網絡層以及傳輸層都有可能發生。咱們平常的網絡應用開發大都在傳輸層進行,因爲UDP有消息保護邊界,不會發生這個問題,所以這篇文章只討論發生在傳輸層的TCP粘包拆包問題。java
什麼是粘包、拆包?服務器
對於什麼是粘包、拆包問題,我想先舉兩個簡單的應用場景:網絡
客戶端和服務器創建一個鏈接,客戶端發送一條消息,客戶端關閉與服務端的鏈接。性能
客戶端和服務器簡歷一個鏈接,客戶端連續發送兩條消息,客戶端關閉與服務端的鏈接。spa
對於第一種狀況,服務端的處理流程能夠是這樣的:當客戶端與服務端的鏈接創建成功以後,服務端不斷讀取客戶端發送過來的數據,當客戶端與服務端鏈接斷開以後,服務端知道已經讀完了一條消息,而後進行解碼和後續處理...。對於第二種狀況,若是按照上面相同的處理邏輯來處理,那就有問題了,咱們來看看第二種狀況下客戶端發送的兩條消息遞交到服務端有可能出現的狀況:.net
第一種狀況:
code
服務端一共讀到兩個數據包,第一個包包含客戶端發出的第一條消息的完整信息,第二個包包含客戶端發出的第二條消息,那這種狀況比較好處理,服務器只須要簡單的從網絡緩衝區去讀就行了,第一次讀到第一條消息的完整信息,消費完再從網絡緩衝區將第二條完整消息讀出來消費。對象
沒有發生粘包、拆包示意圖
blog
第二種狀況:接口
服務端一共就讀到一個數據包,這個數據包包含客戶端發出的兩條消息的完整信息,這個時候基於以前邏輯實現的服務端就蒙了,由於服務端不知道第一條消息從哪兒結束和第二條消息從哪兒開始,這種狀況實際上是發生了TCP粘包。
TCP粘包示意圖
第三種狀況:
服務端一共收到了兩個數據包,第一個數據包只包含了第一條消息的一部分,第一條消息的後半部分和第二條消息都在第二個數據包中,或者是第一個數據包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數據包包含了第二條消息的剩下部分,這種狀況實際上是發送了TCP拆,由於發生了一條消息被拆分在兩個包裏面發送了,一樣上面的服務器邏輯對於這種狀況是很差處理的。
TCP拆包示意圖
爲何會發生TCP粘包、拆包呢?
發生TCP粘包、拆包主要是因爲下面一些緣由:
應用程序寫入的數據大於套接字緩衝區大小,這將會發生拆包。
應用程序寫入數據小於套接字緩衝區大小,網卡將應用屢次寫入的數據發送到網絡上,這將會發生粘包。
進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包。
接收方法不及時讀取套接字緩衝區數據,這將發生粘包。
……
如何處理粘包、拆包問題?
知道了粘包、拆包問題及根源,那麼如何處理粘包、拆包問題呢?TCP自己是面向流的,做爲網絡服務器,如何從這源源不斷涌來的數據流中拆分出或者合併出有意義的信息呢?一般會有如下一些經常使用的方法:
使用帶消息頭的協議、消息頭存儲消息開始標識及消息長度信息,服務端獲取消息頭的時候解析出消息長度,而後向後讀取該長度的內容。
設置定長消息,服務端每次讀取既定長度的內容做爲一條完整消息。
設置消息邊界,服務端從網絡流中按消息編輯分離出消息內容。
……
如何基於Netty處理粘包、拆包問題?
個人上一篇文章的ChannelPipeline部分大概講了Netty網絡層數據的流向以及ChannelHandler組件對網絡數據的處理,這一小節也會涉及到相關重要組件:
ByteToMessageDecoder
MessageToMessageDecoder
這兩個組件都實現了ChannelInboundHandler接口,這說明這兩個組件都是用來解碼網絡上過來的數據的。而他們的順序通常是ByteToMessageDecoder位於head channel handler的後面,MessageToMessageDecoder位於ByteToMessageDecoder的後面。Netty中,涉及到粘包、拆包的邏輯主要在ByteToMessageDecoder及其實現中。
ByteToMessageDecoder
顧名思義、ByteToMessageDecoder是用來將從網絡緩衝區讀取的字節轉換成有意義的消息對象的,對於源碼層面指的說明的一段是下面這部分:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { try { while (in.isReadable()) { int outSize = out.size(); if (outSize > 0) { fireChannelRead(ctx, out, outSize); out.clear(); if (ctx.isRemoved()) { break; } outSize = 0; } int oldInputLength = in.readableBytes(); decode(ctx, in, out); if (ctx.isRemoved()) { break; } if (outSize == out.size()) { if (oldInputLength == in.readableBytes()) { break; } else { continue; } } if (oldInputLength == in.readableBytes()) { throw new DecoderException( StringUtil.simpleClassName(getClass()) + ".decode() did not read anything but decoded a message."); } if (isSingleDecode()) { break; } } } catch (DecoderException e) { throw e; } catch (Throwable cause) { throw new DecoderException(cause); } }
爲了節省篇幅,我把註釋刪除掉了,當上面一個channel handler傳入的ByteBuf有數據的時候,這裏咱們能夠把in參數當作網絡流,這裏有不斷的數據流入,而咱們要作的就是從這個byte流中分離出message,而後把message添加給out。分開將一下代碼邏輯:
當out中有Message的時候,直接將out中的內容交給後面的channel handler去處理。
當用戶邏輯把當前channel handler移除的時候,當即中止對網絡數據的處理。
記錄當前in中可讀字節數。
decode是抽象方法,交給子類具體實現。
一樣判斷當前channel handler移除的時候,當即中止對網絡數據的處理。
若是子類實現沒有分理出任何message的時候,且子類實現也沒有動bytebuf中的數據的時候,這裏直接跳出,等待後續有數據來了再進行處理。
若是子類實現沒有分理出任何message的時候,且子類實現動了bytebuf中的數據,則繼續循環,直到解析出message或者不在對bytebuf中數據進行處理爲止。
若是子類實現解析出了message可是又沒有動bytebuf中的數據,那麼是有問題的,拋出異常。
若是標誌位只解碼一次,則退出。
能夠知道,若是要實現具備處理粘包、拆包功能的子類,及decode實現,必需要遵照上面的規則,咱們以實現處理第一部分的第二種粘包狀況和第三種狀況拆包狀況的服務器邏輯來舉例:
對於粘包狀況的decode須要實現的邏輯對應於將客戶端發送的兩條消息都解析出來分爲兩個message加入out,這樣的話callDecode只須要調用一次decode便可。
對於拆包狀況的decode須要實現的邏輯主要對應於處理第一個數據包的時候第一次調用decode的時候out的size不變,從continue跳出而且因爲不知足繼續可讀而退出循環,處理第二個數據包的時候,對於decode的調用將會產生兩個message放入out,其中兩次進入callDecode上下文中的數據流將會合併爲一個bytebuf和當前channel handler實例關聯,兩次處理完畢即清空這個bytebuf。
固然,儘管介紹了ByteToMessageDecoder,用戶本身去實現處理粘包、拆包的邏輯仍是有必定難度的,Netty已經提供了一些基於不一樣處理粘包、拆包規則的實現:如DelimiterBasedFrameDecoder、FixedLengthFrameDecoder、LengthFieldBasedFrameDecoder和LineBasedFrameDecoder等等。其中:
DelimiterBasedFrameDecoder是基於消息邊界方式進行粘包拆包處理的。
FixedLengthFrameDecoder是基於固定長度消息進行粘包拆包處理的。
LengthFieldBasedFrameDecoder是基於消息頭指定消息長度進行粘包拆包處理的。
LineBasedFrameDecoder是基於行來進行消息粘包拆包處理的。
用戶能夠自行選擇規則而後使用Netty提供的對應的Decoder來進行具備粘包、拆包處理功能的網絡應用開發。
最後
在一般的高性能網絡應用中,客戶端一般以長鏈接的方式和服務端相連,由於每次創建網絡鏈接是一個很耗時的操做。好比在RPC調用中,若是一個客戶端遠程調用的過程當中,連續發起了屢次調用,而若是這些調用對應於同一個鏈接的時候,那麼就會出現服務器須要對於這些屢次調用消息的粘包拆包問題的處理。若是是你,你會選擇哪一種策略呢?
本文基於Netty4.1主分支代碼,若有問題,還請多多指教。