參考文獻:極客時間傅健老師的《Netty源碼剖析與實戰》Talk is cheap.show me the code!
什么是粘包和半包
在客戶端發送數據時,實際是把數據寫入到了TCP發送緩存里面的。
半包:顧名思義就是接收到半個包,如果發送的包的大小比TCP發送緩存的容量大,那么這個數據包就會被分成多個包,通過socket多次發送到服務端,服務端第一次從接受緩存里面獲取的數據,實際是整個包的一部分,半包不是說只收到了全包的一半,是說收到了全包的一部分。
粘包:如果發送的包的大小比TCP發送緩存容量小,並且TCP緩存可以存放多個包,那么客戶端和服務端的一次通信就可能傳遞了多個包,這時候服務端從接受緩存就可能一下讀取了多個包
舉個栗子:
我們現在發送兩條消息:“ABC”,“DEF”,那么對方接收到的消息不一定是這種格式的,對方可能一次性就接收到“ABCDEF”,也可能分好幾次接收到“AB”,“CD”,“EF”,或者更為惡劣一次接收到了兩個消息“A”,“BCDEF”。那么這樣一次接收兩個消息的稱為粘包現象,分三次四接收到多個不完整的現象是半包現象。
粘包的主要原因:
發送方每次寫入數據 < 套接字緩沖區大小,接收方讀取套接字緩沖區數據不夠及時。
半包的主要原因:
發送方寫入數據 > 套接字緩沖區大小,還有就是發送的數據大於協議的MTU(Maximum Transmission Unit 最大傳輸單元)的時候必須拆包。(MTU其實就是TCP協議每層的大小)
換個角度:
收發:一個發送可能被多次接收,多次發送可能被一次接收
傳輸:一個發送可能占用多個傳輸包,多個發送可能公用一個傳輸包
根本原因:
TCP是流動協議,消息無邊界。(UDP像郵寄的包裹,雖然一次運輸多個的,但每個包裹都有“界限”,一個一個簽收,所以無粘包、半包問題)
解決問題的根本手段:找出消息的邊界

Netty對三種常用封幀方式的支持(三個類都繼承ByteTomessageDecoder.java)

解讀Netty處理粘包、半包的源碼-------解碼核心工作流程:
先找到ByteToMessageDecoder.java,可以看到它繼承了ChannelInboundHandlerAdapter.java

這個ChannelInboundHandlerAdapter有個核心的入口方法“channelRead()”;

圖中的msg就相當於我們的數據,開始就把數據轉換成data,
ByteBuf data = (ByteBuf) msg;
然后判斷cumulation是否為null,cumulation是數據積累器,用來積累數據。
ByteBuf cumulation;
first = cumulation == null; if (first) { cumulation = data; } else { cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); }
第一次的時候肯定為null 所以first是true,直接把data數據給了cumulation。再接下來就是解碼:
callDecode(ctx, cumulation, out);
進入具體方法

參數中的in就是數據積累器中的數據,也就是我們傳入的數據。往下走又這么個方法調用:
decodeRemovalReentryProtection(ctx, in, out);
需要注意的是:decode中時,不能執行完handler remove清理操作,decode完之后需要清理數據,改方法的名稱長久是標識功能的。點進去可以查看

可以看出有個decodeState = STATE_CALLING_CHILD_DECODE;這個就是處理剛才remove handler 的;接下來就是“decode(ctx, in, out);”;這個decode是個抽象方法

之前說過Netty對三種封幀的支持分別是:“FixedLengthFrameDecoder”,“DelimiterBasedFrameDecoder”,“LengthFieldBasedFrameDecoder”。這里以“FixedLengthFrameDecoder”為例,所以點開FixedLengthFrameDecoder的decode();
@Override protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { Object decoded = decode(ctx, in); if (decoded != null) { out.add(decoded); } }
然后再進去查看Object decoded = decode(ctx, in);
protected Object decode( @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception { if (in.readableBytes() < frameLength) { return null; } else { return in.readRetainedSlice(frameLength); } }
這時候就能發現關鍵點,代碼標粗,這個in還是數據積累器的數據,取得之后進行判斷,如果小於則返回null不能解出,否則解出。這樣一來就完成了一個數據解析的過程。
