Netty—TCP的粘包和拆包問題

一.前言

雖然TCP協議是可靠性傳輸協議,可是對於TCP長鏈接而言,對於消息發送仍然可能會發生粘貼的情形。主要是由於TCP是一種二進制流的傳輸協議,它會根據TCP緩衝對包進行劃分。有可能將一個大數據包拆分紅多個小的數據包,也有可能將多個小的數據包合併成一個數據包。併發

本篇文章將對TCP粘包和拆包進行介紹:框架

  • TCP粘包拆包問題及現象
  • 解決方式


二.TCP粘包拆包問題及現象

假設Client端發送兩個數據包給Server端,以下圖:tcp

可是Server端實際接收到的數據包形式可能存在以上三種形式:ide

  1. 第一種形式是接收到一個數據包,其中客戶端發送的兩個數據包粘貼在一塊兒。如:client端分別發送兩個數據包都爲Hello World,可是Server端只收到一個數據包爲Hello WorldHello World。這就屬於TCP粘包
  2. 第二種形式是先接受到了第一個數據包的一部分,而後又接收到了另一部分和第二個數據包粘貼。一樣以上例爲準,Server端先接收到了Hell,而後又接收到了o WorldHello World。這屬於TCP拆包
  3. 第三種形式也是接收到兩個數據包,可是是先接收到了第一個數據包和第二個數據包的一部分的粘貼,而後又接收到第二個數據包的另一部分。這就屬於TCP粘包拆包

不管是以上哪一種狀況,從應用層的角度而言,Server端都將處理錯誤。首先以沒有考慮TCP拆包和粘包的場景爲例,分析下TCP拆包粘包將形成什麼樣的現象:oop

1.客戶端編碼:

public static class EchoClientHandler extends ChannelHandlerAdapter {

    static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty.";

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 發送100次消息至server端
        for (int i = 0; i < 100; i++) {
            System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]");
            ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
        }
    }

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

這裏關於Client的啓動代碼省略,重點關注業務Handler。其中Echo總共發送了100次消息給Server,若是按照正確的情形,Server端應該接受到100次,而後分別進行處理。可是實際的情形並非這樣。大數據

2.服務端編碼

public static class EchoServerHandler extends ChannelHandlerAdapter {

    /**
     * 原子計數器,統計接受到的次數
     */
    private AtomicInteger counter = new AtomicInteger(0);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 接受到消息打印
        String body = (String) msg;
        System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]");
    }

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

服務端中使用原子計數器統計接收到的包的次數並打印接受到的消息。下面運行下實例,客戶端輸出以下:編碼

This is 1 times send server: [Hi, huaijin.Welcome to Netty.]
This is 2 times send server: [Hi, huaijin.Welcome to Netty.]
This is 3 times send server: [Hi, huaijin.Welcome to Netty.]
This is 4 times send server: [Hi, huaijin.Welcome to Netty.]
.... 中間部分省略
This is 97 times send server: [Hi, huaijin.Welcome to Netty.]
This is 98 times send server: [Hi, huaijin.Welcome to Netty.]
This is 99 times send server: [Hi, huaijin.Welcome to Netty.]
This is 100 times send server: [Hi, huaijin.Welcome to Netty.]

從中能夠看出,Client端總共發送了100條消息至Server,可是Server端接收狀況以下:設計

This is 1 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 2 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 3 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 4 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 5 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 69 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 70 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.Hi, hu]
This is 71 times receive client: [aijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 72 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 84 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 93 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 94 times receive client: [Hi, huaijin.Welcome to Netty.]

因爲發生了粘包致使Server端只接收到94次,其中有兩條消息粘合在一塊兒。netty

有以上的情形能夠看出,當應用使用長鏈接併發發送請求時,會形成Server端接收到的請求數據發生混亂,從而處理錯誤。code


三.解決方式

關於TCP拆包粘包的解決方式有不少,目前的主流解決方式有如下幾種:

  1. 使用定長消息,Client和Server雙方約定報文長度,Server端接受到報文後,按指定長度解析;
  2. 使用特定分隔符,好比在消息尾部增長分隔符。Server端接收到報文後,按照特定的分割符分割消息後,再解析;
  3. 將消息分割爲消息頭和消息體兩部分,消息頭中指定消息或者消息體的長度,一般設計中使用消息頭第一個字段int32表示消息體的總長度;

固然netty做爲成熟框架,提供了多種方式解決TCP的拆包粘包問題,一般稱做爲半包解碼器。

netty中提供了基於分隔符實現的半包解碼器和定長的半包解碼器:

  • LineBasedFrameDecoder使用"\n"和"\r\n"做爲分割符的解碼器
  • DelimiterBasedFrameDecoder使用自定義的分割符的解碼器
  • FixedLengthFrameDecoder定長解碼器

這裏仍然以上例爲主,使用DelimiterBasedFrameDecoder做爲半包解碼器。

1.客戶端編碼

public static class EchoClientHandler extends ChannelHandlerAdapter {

    /**
     * 消息使用"$_"分割
     */
    static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty.$_";

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]");
            ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
        }
    }

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

客戶端代碼改動較小,只是每條消息後使用分割符"$_"分割,而後發送消息。

2.服務端編碼

服務端須要使用分割符解碼器,利用其對粘包消息進行拆分:

/**
 * netty實現echo server
 *
 * @author huaijin
 */
public class EchoServer {

    public void bind(int port) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 使用分隔符"$_"的半包解碼器
                            ByteBuf byteBuf = Unpooled.copiedBuffer("$_".getBytes());
                            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new EchoServer().bind(8080);
    }


    public static class EchoServerHandler extends ChannelHandlerAdapter {

        /**
         * 原子計數器,統計接受到的次數
         */
        private AtomicInteger counter = new AtomicInteger(0);

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // 接受到消息打印
            String body = (String) msg;
            System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]");
        }

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

當再次運行客戶端和服務端代碼時,服務端表現正常,接收到了100次:

This is 1 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 2 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 3 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 98 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 99 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 100 times receive client: [Hi, huaijin.Welcome to Netty.]


四.總結

本篇文章主要介紹了什麼是TCP的拆包和粘包,並展現了拆包和粘包帶來的現象。並經過netty提供的方案,是如何解決TCP拆包和粘包問題。

相關文章
相關標籤/搜索