TCP協議是面向流的協議,是流式的,沒有業務上的分段,只會根據當前套接字緩衝區的狀況進行拆包或者粘包:
java
發送端的字節流都會先傳入緩衝區,再經過網絡傳入到接收端的緩衝區中,最終由接收端獲取。緩存
由於TCP會根據緩衝區的實際狀況進行包的劃分,在業務上認爲,有的包被拆分紅多個包進行發送,也可能多個曉小的包封裝成一個大的包發送,這就是TCP的粘包或者拆包。服務器
假設客戶端分別發送了兩個數據包D1和D2給服務端,因爲服務端一次讀取到字節數是不肯定的,故可能存在如下幾種狀況:網絡
當TCP緩存再小一點的話,會把D1和D2分別拆成多個包發送。socket
由於TCP只負責數據發送,並不處理業務上的數據,因此只能在上層應用協議棧解決,目前的解決方案概括:ide
Netty提供了多種默認的編碼器解決粘包和拆包:
oop
基於回車換行符的解碼器,當遇到"n"或者 "rn"結束符時,分爲一組。支持攜帶結束符或者不帶結束符兩種編碼方式,也支持配置單行的最大長度。
LineBasedFrameDecoder與StringDecoder搭配時,至關於按行切換的文本解析器,用來支持TCP的粘包和拆包。
使用例子:編碼
private void start() throws Exception { //建立 EventLoopGroup NioEventLoopGroup group = new NioEventLoopGroup(); NioEventLoopGroup work = new NioEventLoopGroup(); try { //建立 ServerBootstrap ServerBootstrap b = new ServerBootstrap(); b.group(group, work) //指定使用 NIO 的傳輸 Channel .channel(NioServerSocketChannel.class) //設置 socket 地址使用所選的端口 .localAddress(new InetSocketAddress(port)) //添加 EchoServerHandler 到 Channel 的 ChannelPipeline .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new LineBasedFrameDecoder(1024)); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler()); } }); //綁定的服務器;sync 等待服務器關閉 ChannelFuture f = b.bind().sync(); System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress()); //關閉 channel 和 塊,直到它被關閉 f.channel().closeFuture().sync(); } finally { //關機的 EventLoopGroup,釋放全部資源。 group.shutdownGracefully().sync(); } }
注意ChannelPipeline 中ChannelHandler的順序,spa
分隔符解碼器,能夠指定消息結束的分隔符,它能夠自動完成以分隔符做爲碼流結束標識的消息的解碼。回車換行解碼器其實是一種特殊的DelimiterBasedFrameDecoder解碼器。
使用例子(後面的代碼只貼ChannelPipeline部分):.net
ChannelPipeline p = ch.pipeline(); p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes()))); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler());
固定長度解碼器,它可以按照指定的長度對消息進行自動解碼,當制定的長度過大,消息太短時會有資源浪費,可是使用起來簡單。
ChannelPipeline p = ch.pipeline(); p.addLast(new FixedLengthFrameDecoder(1 << 5)); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler());
通用解碼器,通常協議頭中帶有長度字段,經過使用LengthFieldBasedFrameDecoder傳入特定的參數,來解決拆包粘包。
io.netty.handler.codec.LengthFieldBasedFrameDecoder的實例化:
/** * Creates a new instance. * * @param maxFrameLength 最大幀長度。也就是能夠接收的數據的最大長度。若是超過,這次數據會被丟棄。 * @param lengthFieldOffset 長度域偏移。就是說數據開始的幾個字節可能不是表示數據長度,須要後移幾個字節纔是長度域。 * @param lengthFieldLength 長度域字節數。用幾個字節來表示數據長度。 * @param lengthAdjustment 數據長度修正。由於長度域指定的長度能夠是header+body的整個長度,也能夠只是body的長度。若是表示header+body的整個長度,那麼咱們須要修正數據長度。 * @param initialBytesToStrip 跳過的字節數。若是你須要接收header+body的全部數據,此值就是0,若是你只想接收body數據,那麼須要跳過header所佔用的字節數。 * @param failFast 若是爲true,則在解碼器注意到幀的長度將超過maxFrameLength時當即拋出TooLongFrameException,而不論是否已讀取整個幀。 * 若是爲false,則在讀取了超過maxFrameLength的整個幀以後引起TooLongFrameException。 */ public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) { //略 }
最大幀長度。也就是能夠接收的數據的最大長度。若是超過,這次數據會被丟棄。
長度域偏移。就是說數據開始的幾個字節可能不是表示數據長度,須要後移幾個字節纔是長度域。
長度域字節數。用幾個字節來表示數據長度。
數據長度修正。由於長度域指定的長度能夠是header+body的整個長度,也能夠只是body的長度。若是表示header+body的整個長度,那麼咱們須要修正數據長度。
跳過的字節數。若是你須要接收header+body的全部數據,此值就是0,若是你只想接收body數據,那麼須要跳過header所佔用的字節數。
若是爲true,則在解碼器注意到幀的長度將超過maxFrameLength時當即拋出TooLongFrameException,而不論是否已讀取整個幀。
若是爲false,則在讀取了超過maxFrameLength的整個幀以後引起TooLongFrameException。
下面經過Netty源碼中LengthFieldBasedFrameDecoder的註釋幾個例子看一下參數的使用:
本例中的length字段的值是12 (0x0C),它表示「HELLO, WORLD」的長度。默認狀況下,解碼器假定長度字段表示長度字段後面的字節數。
由於咱們能夠經過調用readableBytes()來得到內容的長度,因此可能但願經過指定initialbystrip來刪除長度字段。在本例中,咱們指定2(與length字段的長度相同)來去掉前兩個字節。
在大多數狀況下,length字段僅表示消息體的長度,如前面的示例所示。可是,在一些協議中,長度字段表示整個消息的長度,包括消息頭。在這種狀況下,咱們指定一個非零長度調整。由於這個示例消息中的長度值老是比主體長度大2,因此咱們指定-2做爲補償的長度調整。
下面的消息是第一個示例的簡單變體。一個額外的頭值被預先寫入消息中。長度調整再次爲零,由於譯碼器在計算幀長時老是考慮到預寫數據的長度。
這是一個高級示例,展現了在長度字段和消息正文之間有一個額外頭的狀況。您必須指定一個正的長度調整,以便解碼器將額外的標頭計數到幀長度計算中。
這是上述全部示例的組合。在長度字段以前有預寫的header,在長度字段以後有額外的header。預先設置的header會影響lengthFieldOffset,而額外的leader會影響lengthAdjustment。咱們還指定了一個非零initialBytesToStrip來從幀中去除長度字段和預約的header。若是不想去掉預寫的header,能夠爲initialBytesToSkip指定0。
讓咱們對前面的示例進行另外一個修改。與前一個示例的唯一區別是,length字段表示整個消息的長度,而不是消息正文的長度,就像第三個示例同樣。咱們必須把HDR1的長度和長度計算進長度調整裏。請注意,咱們不須要考慮HDR2的長度,由於length字段已經包含了整個頭的長度。