Netty(三)TCP粘包/拆包

????TCP是個(gè)“流”協(xié)議,所謂流,就是沒有界限的一串?dāng)?shù)據(jù).TCP底層并不了解上層業(yè)務(wù)數(shù)據(jù)的具體含義,它會(huì)根據(jù)TCP緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分,所以在業(yè)務(wù)上任務(wù),一個(gè)完整的包可能會(huì)被TCP拆分成多個(gè)包進(jìn)行發(fā)送,也有可能把多個(gè)小的包封裝成一個(gè)大的數(shù)據(jù)包發(fā)送,這就是所謂的TCP粘包和拆包問題

TCP粘包/拆包現(xiàn)象

????通過圖解對TCP粘包和拆包問題說明:


image.png

????假設(shè)客戶端分別發(fā)送了兩個(gè)數(shù)據(jù)包D1和D2給服務(wù)端,有序服務(wù)端一次讀取到的字節(jié)數(shù)是不確定的,故可能存在以下4種情況:

  1. 服務(wù)端分兩次讀取到兩個(gè)獨(dú)立的數(shù)據(jù)包,分別是D1和D2,沒有粘包和拆包;
  2. 服務(wù)端一次接收到了兩個(gè)數(shù)據(jù)包,D1和D2粘合在一起,被稱為TCP粘包;
  3. 服務(wù)端分兩次讀取到了兩個(gè)數(shù)據(jù)包,第一次讀取到了完整的D1包和D2包的部分內(nèi)容,第二次讀取到了D2包的剩余內(nèi)容,這被稱為TCP拆包;
  4. 服務(wù)端兩次讀取到了兩個(gè)數(shù)據(jù)包,第一次讀取到了D1包的部分內(nèi)容D1_1,第二次讀取到了D1包的剩余內(nèi)容D1_2和D2包的整包.
    如果此時(shí)服務(wù)端TCP接收滑窗非常小,而數(shù)據(jù)包D1和D2比較大,很有可能會(huì)發(fā)生第5種可能,即服務(wù)端分多次才能將D1和D2包接收完全,期間發(fā)生多次拆包.

TCP粘包/拆包發(fā)生的原因

  1. 應(yīng)用程序write寫入的字節(jié)大小大于套接口發(fā)送緩沖區(qū)大小

  2. 進(jìn)行MSS大小的TCP分段

  3. 以太網(wǎng)幀payload大于MTU進(jìn)行IP分片.


    image.png

粘包解決

由于底層的TCP無法理解上層的業(yè)務(wù)數(shù)據(jù),所以在底層是無法保證數(shù)據(jù)包不被拆分和重組的,這個(gè)問題只能通過上層的應(yīng)用協(xié)議棧設(shè)計(jì)來解決,根據(jù)業(yè)界的主流協(xié)議的解決方案,可歸納如下:

  1. 消息定長,例如每個(gè)豹紋的大小為固定長度200字節(jié),如果不夠,空位補(bǔ)空格;
  2. 在包尾增加回車換行符進(jìn)行分割,例如FTP協(xié)議
  3. 將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段,通常設(shè)計(jì)思路為消息頭的第一個(gè)字段使用int32來表示消息的總長度;

異常案例

繼Netty(一)中的代碼,修改NettyClientHandler:

/**
 * @ClassName NettyClientHandler
 * @Description TODO
 * @Author liuzetian
 * @Date 2019/5/6 9:22 AM
 * @Version 1.0
 **/
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("客戶端收到消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 修改 循環(huán)發(fā)送100消息 預(yù)期服務(wù)端接收到100條消息
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf byteBuf = null;
        String req = "Hello World\n";
        for(int i= 0;i<100;i++){
            byteBuf = Unpooled.copiedBuffer(req, CharsetUtil.UTF_8);
            ctx.writeAndFlush(byteBuf);
        }

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

服務(wù)端代碼不變:

/**
 * @ClassName Netty
 * @Description TODO
 * @Author liuzetian
 * @Date 2019/5/6 9:02 AM
 * @Version 1.0
 **/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println( "服務(wù)器接收到消息:" + in.toString(CharsetUtil.UTF_8));
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

打印結(jié)果:
服務(wù)端:

服務(wù)器接收到消息:Hello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldH
服務(wù)器接收到消息:ello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello World

客戶端

客戶端收到消息:Hello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldH
客戶端收到消息:ello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello World

有打印結(jié)果看服務(wù)端只收到了2條消息,服務(wù)端也發(fā)送了兩條應(yīng)答;即發(fā)生了粘包

解決

需要添加解碼器,后面會(huì)介紹;如有錯(cuò)誤,歡迎指正

以上內(nèi)容均摘自,《Netty權(quán)威指南》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容