基于Netty解决TCP的粘包拆包问题

TCP是一个流协议,即TCP的数据时没有界限的一串数据。而这样的数据方式必然会导致数据粘包。为了解析TCP数据,我们相对应的也要对数据进行拆包。
粘包的原因:
1. 应用程序write的字节大于套接口发送缓冲区大小;
2. 进行MSS大小的TCP分段;
3. 以太网帧的payload大于MTU进行IP分片;

未考虑粘包问题的异常代码

服务器端:
TimeServer

public class TimeServer {

    public void bind(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChildChannelHandler());
            //绑定端口, 同步等待成功;
            ChannelFuture future = bootstrap.bind(port).sync();
            //等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } finally {
            //优雅关闭 线程组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast("timeServerHandler",new TimeServerHandler());
        }
    }
    public static void main(String[] args) throws Exception {
        int port = 443;
        new TimeServer().bind(port);
    }
}

TimeServerHandler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private int count;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("Server start read");
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
// String body = (String) msg;
        System.out.println("The time server receive order : " + body + "; the count is : " +  ++count);
        String currentTime = "Query Time Order".equalsIgnoreCase(body) ? new java.util.Date(
                System.currentTimeMillis()).toString() : "Bad Order";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
// ctx.writeAndFlush(resp);
        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();
    }
}

客户端:
TimeClient

public class TimeClient {
    public void connect(int port, String host) throws Exception{
        //配置客户端NIO 线程组
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap client = new Bootstrap();
        try {
            client.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast("timeServerHandler",new TimeClientHandler());
                        }
                    });

            //绑定端口, 异步连接操作
            ChannelFuture future = client.connect(host, port).sync();

            //等待客户端连接端口关闭
            future.channel().closeFuture().sync();
        } finally {
            //优雅关闭 线程组
            group.shutdownGracefully();
        }
    }
    public static void main(String[] args) {
        int port = 443;
        TimeClient client = new TimeClient();
        try {
            client.connect(port, "127.0.0.1");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TimeClientHandler

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    private int count;
    private byte[] req;

    public TimeClientHandler() {
        req = ("QUERY TIME ORDER" + System.getProperty("line.separator"))
                .getBytes();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf message = null;
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8");
        // String body = (String) msg;
        System.out.println("NOW is: " + body + "; the counter is " + ++count);

    }

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

服务端输出:

Server start read The time server receive order : QUERY TIME ORDER QUERY TIME ORDER 。。。 QUERY TIME ORDER QUERY TIME ORD; the count is : 1
Server start read The time server receive order : QUERY TIME ORDER 。。。 QUERY TIME ORDER QUERY TIME ORDER; the count is : 2

客户端输出:

NOW is: Bad Order
Bad Order
; the counter is 1

很明显,由于粘包拆包导致半包读写问题,致使得到的结果不是目标结果。Netty提供多种编码器用于处理半包问题,接下来使用LineBasedFrameDecoder来解决半包读写问题。

使用LineBasedFrameDecoder的改善方案

服务端:
TimeServer类需要修改initChannel方法,加入LineBasedFrameDecoder解码器,修改后方法如下:

protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast("timeServerHandler",new TimeServerHandler());
        }

TimeServerHandler类需要修改channelRead方法,由于使用解码器之后,获取的msg已经解码成字符串了,具体代码如下:

public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
//      System.out.println("Server start read");
//      ByteBuf buf = (ByteBuf) msg;
//      byte[] req = new byte[buf.readableBytes()];
//      buf.readBytes(req);
//      String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
        String body = (String) msg;
        System.out.println("The time server receive order : " + body + "; the count is : " +  ++count);
        String currentTime = "Query Time Order".equalsIgnoreCase(body) ? new java.util.Date(
                System.currentTimeMillis()).toString() : "Bad Order";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
//      ctx.write(resp);
    }

客户端代码:
TimeClient与服务端对应修改initChannel方法,加入LineBasedFrameDecoder解码器,修改后代码如下:

protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast("timeServerHandler",new TimeClientHandler());
                        }

TimeClientHandler类需要修改channelRead方法,修改如下:

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// ByteBuf buf = (ByteBuf)msg;
// byte[] req = new byte[buf.readableBytes()];
// buf.readBytes(req);
// String body = new String(req, "UTF-8");
      String body = (String) msg;
      System.out.println("NOW is: " + body + "; the counter is " + ++count);

  }

运行之后,服务端输出为:

The time server receive order : QUERY TIME ORDER; the count is : 1
The time server receive order : QUERY TIME ORDER; the count is : 2
The time server receive order : QUERY TIME ORDER; the count is : 3
The time server receive order : QUERY TIME ORDER; the count is : 4
The time server receive order : QUERY TIME ORDER; the count is : 5
The time server receive order : QUERY TIME ORDER; the count is : 6
The time server receive order : QUERY TIME ORDER; the count is : 7
。。。。。。
The time server receive order : QUERY TIME ORDER; the count is : 98
The time server receive order : QUERY TIME ORDER; the count is : 99
The time server receive order : QUERY TIME ORDER; the count is : 100

客户端输出为:

NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 1
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 2
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 3
。。。。。。
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 94
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 95
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 96
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 97
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 98
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 99
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 100

程序运行结果完全符合预期,说明使用LineBasedFrameDecoder和StringDecoder可以解决TCP粘包导致的读半包问题,不需要写额外代码,使用起来比较方便。

原理分析

LineBasedFrameDecoder是依次遍历ByteBuf中的可读字节,判断看是否有”\n”或者”\r\n”,如果有,则此位置为结束位置,从可读索引到结束位置区间的字节组成一行。它是以换行符为结束标志的解码器。
StringDecoder的功能非常简单,就是接收到的对象转换为字符串,然后调用后面的Handler。
使用LineBasedFrameDecoder+StringDecoder组合就是按照行切换的文本解码器,它被设计用来支持TCP的粘包和拆包!!

你可能感兴趣的:(netty,粘包,拆包,文本解码器)