Netty粘包/拆包之解码器应用

160 阅读6分钟

TCP粘包/拆包介绍

TCP是个"流"协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据 TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

粘包/拆包的四种情况

wecom-temp-9fb9b2cdc49ae4e67d1aa196eabbb0b2.png

  1. 服务端分两次读取到了两个独立的数据包,分别是 D1和 D2,没有粘包和拆包;
  2. 服务端一次接收到了两个数据包,D1和 D2 粘合在一起,被称为 TCP 粘包;
  3. 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包;
  4. 服务端分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1包的剩余内容 D1_2 和 D2 包的整包。如果此时服务端 TCP接收滑窗非常小,而数据包 D1和 D2 比较大,很有可能会发生第五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包。

粘包问题的解决策略

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

  1. 消息定长,例如每个报文的大小为固定长度 200 字节,如果不够,空位补空格;
  2. 在包尾增加回车换行符进行分割,例如 FTP 协议;
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度;
  4. 更复杂的应用层协议。

TCP粘包案例

本次使用的是Netty 5.0版本

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>5.0.0.Alpha2</version>
</dependency>

Server端

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

public class TimeServer {

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

    public void bind(int bindPort){
        //用于网络连接的线程组
        NioEventLoopGroup boss = new NioEventLoopGroup();
        //用户socket读写线程组
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss,worker);

            //设置channel类型(对应jdk的nio)
            serverBootstrap.channel(NioServerSocketChannel.class);

            //服务端接受连接前的缓冲大小(最多有2048个请求等待被accept)
            serverBootstrap.option(ChannelOption.SO_BACKLOG,2048);

            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    pipeline.addLast(new StringDecoder());
                    pipeline.addLast(new TimeServerHandler());
                }
            });


            ChannelFuture future = serverBootstrap.bind(bindPort).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }

    private static class TimeServerHandler extends ChannelHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("The time server receive order : " + body);
            ByteBuf resp = Unpooled.copiedBuffer((String.valueOf(System.currentTimeMillis())).getBytes());
            ctx.write(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }

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

Client端

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

public class TimeClient {

    public static void main(String[] args) {
        new TimeClient().connect("localhost", 8080);
    }

    public void connect(String host, int port) {
        //IO读写线程组
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.TCP_NODELAY, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    pipeline.addLast(new StringDecoder());
                    pipeline.addLast(new TimeClientHandler());
                }
            });
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            group.shutdownGracefully();
        }
    }

    private static class TimeClientHandler extends ChannelHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            for (int i = 0; i < 10; i++) {
                byte[] req = ("hello").getBytes();
                ByteBuf buffer = Unpooled.buffer(req.length);
                buffer.writeBytes(req);
                ctx.writeAndFlush(buffer);
            }
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("Now is : " + body);
        }

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

}

运行结果

Server端:

The time server receive order : hellohellohellohellohellohellohellohellohellohello

Client端:

Now is : 1628217161726

客户端写入了10次数据包,而服务端只接受到了一次(把10个数据包粘成一个包传到了应用层),此时发生了TCP粘包现象。

下面我们利用Netty编解码器来解决粘包问题

对Server端进行改造

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

public class TimeServer {

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

    public void bind(int bindPort){
        //用于网络连接的线程组
        NioEventLoopGroup boss = new NioEventLoopGroup();
        //用户socket读写线程组
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss,worker);

            //设置channel类型(对应jdk的nio)
            serverBootstrap.channel(NioServerSocketChannel.class);

            //服务端接受连接前的缓冲大小(最多有2048个请求等待被accept)
            serverBootstrap.option(ChannelOption.SO_BACKLOG,2048);

            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    //以换行符作为结束读取标准 超过指定的最大大小若还没有读取到结束标志 则抛出异常 同时忽略到之前读取的异常码流
                    pipeline.addLast(new LineBasedFrameDecoder(1024));
                    pipeline.addLast(new StringDecoder());
                    pipeline.addLast(new TimeServerHandler());
                }
            });


            ChannelFuture future = serverBootstrap.bind(bindPort).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }

    private static class TimeServerHandler extends ChannelHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("The time server receive order : " + body);
            ByteBuf resp = Unpooled.copiedBuffer((System.currentTimeMillis() + System.getProperty("line.separator")).getBytes());
            ctx.write(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }

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

XX行管道中增加了LineBasedFrameDecoder解码器用于对换行符来进行粘包拆包的处理。 XX行写出的消息增加换行符。

对Client端进行改造

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

public class TimeClient {

    public static void main(String[] args) {
        new TimeClient().connect("localhost", 8080);
    }

    public void connect(String host, int port) {
        //IO读写线程组
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.TCP_NODELAY, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    pipeline.addLast(new LineBasedFrameDecoder(1024));
                    pipeline.addLast(new StringDecoder());
                    pipeline.addLast(new TimeClientHandler());
                }
            });
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            group.shutdownGracefully();
        }
    }

    private static class TimeClientHandler extends ChannelHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            for (int i = 0; i < 10; i++) {
                byte[] req = ("hello" + System.getProperty("line.separator")).getBytes();
                ByteBuf buffer = Unpooled.buffer(req.length);
                buffer.writeBytes(req);
                ctx.writeAndFlush(buffer);
            }
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("Now is : " + body);
        }

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

}

XX行管道中增加了LineBasedFrameDecoder解码器用于对换行符来进行粘包拆包的处理。

XX行写出的消息增加换行符。

运行结果

Server端:

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

The time server receive order : hello

Client端:

Now is : 1628218042106

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042111

Now is : 1628218042112

可以看到这次没有发生粘包现象,应用层接收到了10次数据包。

下面介绍其他几种常用解码器

Netty之常用解码器

LineBasedFrameDecoder(换行符)

使用示例: pipeline.addLast(new LineBasedFrameDecoder(1024));

LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。

提供了两种构造方式

  1. new LineBasedFrameDecoder(int maxLength, boolean stripDelimiter, boolean failFast);

  2. new LineBasedFrameDecoder(int maxLength){this(maxLength, true, true)};

DelimiterBasedFrameDecoder(分隔符)

使用示例: pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$_".getBytes())));

FixedLengthFrameDecoder(定长)

使用示例: pipeline.addLast(new FixedLengthFrameDecoder(1024));