Netty粘包与拆包问题

先看一下下面的例子:
服务端代码为:

public class TimeServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try{
            ServerBootstrap b=new ServerBootstrap();
            b.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) {
                            channel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            ChannelFuture f=b.bind(8081).sync();
            f.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
class TimeServerHandler extends SimpleChannelInboundHandler {
    private int counter;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf=(ByteBuf)msg;
        byte[] req=new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body=new String(req, StandardCharsets.UTF_8);
        System.out.println(body+",counter:"+ (++counter));
    }
}

客户端代码为:

public class TimeClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup group=new NioEventLoopGroup();
        try{
            Bootstrap b=new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel){
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });
            ChannelFuture f=b.connect("127.0.0.1",8081).sync();
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        byte[] req= ("send msg to server\r\n").getBytes(StandardCharsets.UTF_8);
        for (int i = 0; i < 3; i++) {
            ByteBuf msg= Unpooled.buffer(req.length);
            msg.writeBytes(req);
            ctx.writeAndFlush(msg);
        }
    }
}

上面的代码中,客户端会连续发三条信息给服务端,服务端接收信息并进行计数。
或许你会认为服务端会收到三条信息,但事实上服务端可能只会收到一条,结果为:

send msg to server\send msg to server\send msg to server,counter:1

这是因为发生了粘包。粘包指的是多个小的数据包可能被封装成一个大的数据包发送。与之相关的是拆包,拆包指的是一个完整的数据包可能会被 TCP 拆分成多个包进行发送。

出现粘包/拆包的原因

出现粘包/拆包的原因有三点:

  1. 应用发送的字节数超过 Socket 发送缓冲区的大小。每个 TCP Socket 连接在内核中都有一个发送缓冲区和接收缓冲区,在接收数据时,会把数据存到接收缓冲区,等待用户读取。
  2. 对超过 TCP 最大报文段长度(Max Segment Size,MSS)进行 TCP 分段。
  3. 对以太网帧超过最大传输单元(Maximum Transmission Unit,MTU)进行 IP 分片。因为数据链路层传输的帧大小是有限制的,MTU 指的就是以太网和 IEEE 802.3 对数据帧的长度限制。

如果 IP 需要发送一个数据报,并且这个数据报比链路层 MTU 大,则 IP 会通过分片将数据报分解成较小的部分,使每个分片都小于 MTU。 当两台主机之间跨越多个网络通信时,每条链路可能有不同大小的 MTU。在包含所有链路的整个网络路径上,最小的 MTU 称为路径 MTU。

粘包问题的解决办法

因为底层的 TCP 无法区分上层的应用数据,所以只能依赖上层来解决,主要有四种办法:

  1. 固定消息的长度
  2. 在包尾增加回车或换行符以进行分割
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度 (或者消息体长度) 的字段。
  4. 使用更复杂的应用层协议。

Netty 提供了 LineBasedFrameDecoderStringDecoder 解决粘包问题。

public class TimeServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try{
            ServerBootstrap b=new ServerBootstrap();
            b.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) { //添加LineBasedFrameDecoder和StringDecoder
                            channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            channel.pipeline().addLast(new StringDecoder());
                            channel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            ChannelFuture f=b.bind(8081).sync();
            f.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
class TimeServerHandler extends SimpleChannelInboundHandler {
    private int counter;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
        String body=(String) msg; //接收到的是String类型
        System.out.println(body+",counter:"+ (++counter));
    }
}

LineBasedFrameDecoder 的工作原理是依次遍历 ByteBuf 中的可读字节,判断是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器。
StringDecoder 则会将接收到的对象转换成字符串。

参考:

  1. 《Netty 权威指南》
  2. 《TCP/IP 详解卷 1:协议》
  3. TCP的发送缓冲区和接收缓冲区 - 取经路上的白龙马C - 博客园
  4. MTU TCP-MSS详解 - 知乎

你可能感兴趣的:(java,jetty)