Netty之七TCP粘包和拆包及解決方案

個人專題目錄


1. TCP 粘包和拆包及解決方案

1.1 TCP 粘包和拆包基本介紹

  1. TCP是面向連接的,面向流的,提供高可靠性服務(wù)。收發(fā)兩端(客戶端和服務(wù)器端)都要有一一成對的socket,因此,發(fā)送端為了將多個發(fā)給接收端的包,更有效的發(fā)給對方,使用了優(yōu)化方法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個大的數(shù)據(jù)塊,然后進行封包。這樣做雖然提高了效率,但是接收端就難于分辨出完整的數(shù)據(jù)包了,因為面向流的通信是無消息保護邊界
  2. 由于TCP無消息保護邊界, 需要在接收端處理消息邊界問題,也就是我們所說的粘包、拆包問題

TCP粘包是指發(fā)送方發(fā)送的若干包數(shù)據(jù)到接收方接收時粘成一包,從接收緩沖區(qū)看,后一包數(shù)據(jù)的頭緊接著前一包數(shù)據(jù)的尾。

出現(xiàn)粘包現(xiàn)象的原因是多方面的,它既可能由發(fā)送方造成,也可能由接收方造成。發(fā)送方引起的粘包是由TCP協(xié)議本身造成的,TCP為提高傳輸效率,發(fā)送方往往要收集到足夠多的數(shù)據(jù)后才發(fā)送一包數(shù)據(jù)。若連續(xù)幾次發(fā)送的數(shù)據(jù)都很少,通常TCP會根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一包后一次發(fā)送出去,這樣接收方就收到了粘包數(shù)據(jù)。接收方引起的粘包是由于接收方用戶進程不及時接收數(shù)據(jù),從而導(dǎo)致粘包現(xiàn)象。這是因為接收方先把收到的數(shù)據(jù)放在系統(tǒng)接收緩沖區(qū),用戶進程從該緩沖區(qū)取數(shù)據(jù),若下一包數(shù)據(jù)到達時前一包數(shù)據(jù)尚未被用戶進程取走,則下一包數(shù)據(jù)放到系統(tǒng)接收緩沖區(qū)時就接到前一包數(shù)據(jù)之后,而用戶進程根據(jù)預(yù)先設(shè)定的緩沖區(qū)大小從系統(tǒng)接收緩沖區(qū)取數(shù)據(jù),這樣就一次取到了多包數(shù)據(jù)。

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

  • 服務(wù)端分兩次讀取到了兩個獨立的數(shù)據(jù)包,分別是D1和D2,沒有粘包和拆包

  • 服務(wù)端一次接受到了兩個數(shù)據(jù)包,D1和D2粘合在一起,稱之為TCP粘包

  • 服務(wù)端分兩次讀取到了數(shù)據(jù)包,第一次讀取到了完整的D1包和D2包的部分內(nèi)容,第二次讀取到了D2包的剩余內(nèi)容,這稱之為TCP拆包

  • 服務(wù)端分兩次讀取到了數(shù)據(jù)包,第一次讀取到了D1包的部分內(nèi)容D1_1,第二次讀取到了D1包的剩余部分內(nèi)容D1_2和完整的D2包。

1.2 TCP 粘包和拆包解決方案

粘包情況有兩種:

  • 粘在一起的包都是完整的數(shù)據(jù)包;
  • 粘在一起的包有不完整的包。

解決粘連包的方法大致分為如下三種:

  1. 發(fā)送方開啟TCP_NODELAY,但是會加重網(wǎng)絡(luò)負擔(dān)
  2. 接收方簡化或者優(yōu)化流程盡可能快的接收數(shù)據(jù),治標不治本
  3. 人為強制分包每次只讀一個完整的包。使用自定義協(xié)議+ 編解碼器來解決,關(guān)鍵就是要解決服務(wù)器端每次讀取數(shù)據(jù)長度的問題, 這個問題解決,就不會出現(xiàn)服務(wù)器多讀或少讀數(shù)據(jù)的問題,從而避免的TCP 粘包、拆包。
    1. 每次都只讀取一個完整的包,不如不足一個完整的包,就等下次再接收,如果緩沖區(qū)有N個包要接受,那么需要分N次才能接收完成;
    2. 有多少接收多少,將接収的數(shù)據(jù)緩存在一個臨時的緩存中,交由后續(xù)的專門解碼的線程/進程處理。
    3. 以上兩種分包方式,如果強制關(guān)閉程序,數(shù)據(jù)會存在丟失,第一種數(shù)據(jù)丟失在接收緩沖區(qū);第二種丟失在程序自身緩存。

Netty自帶的幾種粘連包解決方案:

  1. DelimiterBasedFrameDecoder
    1. FixedLengthFrameDecoder
    2. LengthFieldBasedFrameDecoder

在基于流的傳輸里比如TCP/IP,接收到的數(shù)據(jù)會先被存儲到一個socket接收緩沖里。不幸的是,基于流的傳輸并不是一個數(shù)據(jù)包隊列,而是一個字節(jié)隊列。即使你發(fā)送了2個獨立的數(shù)據(jù)包,操作系統(tǒng)也不會作為2個消息處理而僅僅是作為一連串的字節(jié)而言。因此這是不能保證你遠程寫入的數(shù)據(jù)就會準確地讀取。所以一個接收方不管他是客戶端還是服務(wù)端,都應(yīng)該把接收到的數(shù)據(jù)整理成一個或者多個更有意思并且能夠讓程序的業(yè)務(wù)邏輯更好理解的數(shù)據(jù)。

netty使用tcp/ip協(xié)議傳輸數(shù)據(jù)。而tcp/ip協(xié)議是類似水流一樣的數(shù)據(jù)傳輸方式。多次訪問的時候有可能出現(xiàn)數(shù)據(jù)粘包的問題,解決這種問題的方式如下:

定長數(shù)據(jù)流

客戶端和服務(wù)器,提前協(xié)調(diào)好,每個消息長度固定。(如:長度10)。如果客戶端或服務(wù)器寫出的數(shù)據(jù)不足10,則使用空白字符補足(如:使用空格)。每個完整請求數(shù)據(jù)長度為8字節(jié)等。(FixedLengthFrameDecoder)

特殊結(jié)束符

客戶端和服務(wù)器,協(xié)商定義一個特殊的分隔符號,分隔符號長度自定義。如:‘#’、‘_’、‘AA@’。在通訊的時候,只要沒有發(fā)送分隔符號,則代表一條數(shù)據(jù)沒有結(jié)束。 每個完整請求數(shù)據(jù)末尾使用’\0’作為數(shù)據(jù)結(jié)束標記。(DelimiterBasedFrameDecoder)

協(xié)議

相對最成熟的數(shù)據(jù)傳遞方式。有服務(wù)器的開發(fā)者提供一個固定格式的協(xié)議標準。客戶端和服務(wù)器發(fā)送數(shù)據(jù)和接受數(shù)據(jù)的時候,都依據(jù)協(xié)議制定和解析消息。http協(xié)議格式等。

使用POJO來替代傳遞的流數(shù)據(jù),如:每個完整的請求數(shù)據(jù)都是一個RequestMessage對象,在Java語言中,使用POJO更符合語種特性,推薦使用。

1.3 具體實例

/**
 * 協(xié)議包
 *
 * @author Administrator
 */
public class MessageProtocol {
    private int len;
    private byte[] content;

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {

    private int count;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客戶端發(fā)送10條數(shù)據(jù) "今天天氣冷,吃火鍋" 編號

        for (int i = 0; i < 5; i++) {
            String mes = "今天天氣冷,吃火鍋";
            byte[] content = mes.getBytes(StandardCharsets.UTF_8);
            int length = mes.getBytes(StandardCharsets.UTF_8).length;

            //創(chuàng)建協(xié)議包對象
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLen(length);
            messageProtocol.setContent(content);
            ctx.writeAndFlush(messageProtocol);
        }

    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {

        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println("客戶端接收到消息如下");
        System.out.println("長度=" + len);
        System.out.println("內(nèi)容=" + new String(content, StandardCharsets.UTF_8));

        System.out.println("客戶端接收消息數(shù)量=" + (++this.count));

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("異常消息=" + cause.getMessage());
        ctx.close();
    }
}

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被調(diào)用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}
public class MyMessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyMessageDecoder decode 被調(diào)用");
        //需要將得到二進制字節(jié)碼-> MessageProtocol 數(shù)據(jù)包(對象)
        int length = in.readInt();

        byte[] content = new byte[length];
        in.readBytes(content);

        //封裝成 MessageProtocol 對象,放入 out, 傳遞下一個handler業(yè)務(wù)處理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);

        out.add(messageProtocol);

    }
}
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    private int count;

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

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {

        //接收到數(shù)據(jù),并處理
        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("服務(wù)器接收到信息如下");
        System.out.println("長度=" + len);
        System.out.println("內(nèi)容=" + new String(content, StandardCharsets.UTF_8));

        System.out.println("服務(wù)器接收到消息包數(shù)量=" + (++this.count));

        //回復(fù)消息
        String responseContent = UUID.randomUUID().toString();
        int responseLen = responseContent.getBytes(StandardCharsets.UTF_8).length;
        byte[] responseContent2 = responseContent.getBytes(StandardCharsets.UTF_8);
        //構(gòu)建一個協(xié)議包
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(responseLen);
        messageProtocol.setContent(responseContent2);

        ctx.writeAndFlush(messageProtocol);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容