【Netty 核心技术及源码剖析】05 TCP 粘包和拆包 及解决方案

1 TCP 粘包和拆包

  1. TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。
  2. 由于TCP无消息保护边界, 需要在接收端处理消息边界问题,即我们所说的粘包、拆包问题。
  3. TCP粘包、拆包图解:
    在这里插入图片描述
    假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
    1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包。
    2. 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包。
    3. 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容D2_1,第二次读取到了 D2 包的剩余内容 D2 _2,这称之为 TCP 拆包。
    4. 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容 D1_1,第二次读取到了 D1包的剩余部分内容 D1_2和完整的 D2 包。

上层协议为了对消息进行区分,往往采用以下几种方式:

  1. 将回车换行符作为消息结束符,例如 FTP 协议,这种方式在文本协议中应用比较广泛;
  2. 将特殊的分隔符作为泭息的结束标志,回车换行符就是一种特殊的结束分隔符;
  3. 消息长度固定,累计读取到长度总和为定长 LEN 的报文后,就认为读取到了一个完整的消息;将计数器置位,重新开始读取下一个数据报;
  4. 通过在消息头中定义长度字段来标识消息的总长度。

2 TCP 粘包和拆包现象实例

在编写 Netty 程序时,如果没有做处理,就会发生粘包和拆包的问题,查看下面代码:

服务端:

package bin.netty.tcp粘包拆包;

import io.netty.bootstrap.ServerBootstrap;
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;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyServer {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            // 业务处理,读取客户端的消息并显示
                            pipeline.addLast(new MyServerHandler1());
                        }
                    });

            ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
            channelFuture.addListener(cf -> {
                if (cf.isSuccess()) {
                    System.out.println("listen on 9999");
                }
            });

            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

MyServerHandler1:

package bin.netty.tcp粘包拆包;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyServerHandler1 extends ChannelInboundHandlerAdapter {

	@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + ": ");
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;

        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);

        System.out.println("客户端的消息:" + new String(bytes, CharsetUtil.UTF_8));
    }

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

客户端:

package bin.netty.tcp粘包拆包;

import bin.netty.inandoutbound.MyLongMessageDecoder;
import bin.netty.inandoutbound.MyLongMessageEncoder;
import io.netty.bootstrap.Bootstrap;
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.StringEncoder;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new MyClientHandler1());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
            channelFuture.addListener(cf -> {
                if (cf.isSuccess()) {
                    System.out.println("connect to server");
                }
            });

            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

MyClientHandler1:

package bin.netty.tcp粘包拆包;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyClientHandler1 extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 5; i++) {
            ctx.writeAndFlush(Unpooled.copiedBuffer("消息序号:" + i, CharsetUtil.UTF_8));
        }
    }

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

运行结果如下:

/127.0.0.1:51653: 
客户端的消息:消息序号:0消息序号:1消息序号:2消息序号:3消息序号:4
/127.0.0.1:51686: 
客户端的消息:消息序号:0
客户端的消息:消息序号:1
客户端的消息:消息序号:2
客户端的消息:消息序号:3
客户端的消息:消息序号:4

运行两个客户端,但是接收到的消息顺序不一样的。

3 TCP 粘包和拆包解决方案

3.1 LineBasedFrameDecoder 解决 TCP 粘包问题

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

服务端:

package bin.netty.tcp粘包拆包.lineframedecoder;

import io.netty.bootstrap.ServerBootstrap;
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;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

/**
 * LineBasedFrameDecoder + StringDecoder 解决 Tcp 粘包
 *
 * @author liyibin
 * @date 2021-07-10
 */
public class TimeServer {

    public static void main(String[] args) throws Exception {
        int port = 9999;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ignore) {
            }
        }

        // 启动服务器
        new TimeServer().start(port);
    }

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

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new StringEncoder());

                            pipeline.addLast(new LineBasedFrameDecoder(1024));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TimeServerHandler());
                        }
                    });

            ChannelFuture cf = serverBootstrap.bind(port).sync();
            System.out.println("listen on " + port);

            // 等待服务器监听窗口关闭
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class TimeServerHandler extends SimpleChannelInboundHandler<String> {

        private int counter;

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("接收到客户端请命令:" + msg + ", counter=" + ++counter);

            String content = "NOW".equalsIgnoreCase(msg) ? new Date().toString() : "BAD COMMAND";

            // 字符串加上换行符表示结束
            ctx.writeAndFlush(content + System.getProperty("line.separator"));
        }

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

客户端:

package bin.netty.tcp粘包拆包.lineframedecoder;

import io.netty.bootstrap.Bootstrap;
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;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

/**
 * @author liyibin
 * @date 2021-07-10
 */
public class TimeClient {

    public static void main(String[] args) throws Exception {
        int port = 9999;

        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ignore){
            }
        }

        new TimeClient().connect("127.0.0.1", port);
    }

    public void connect(String host, int port) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new StringEncoder());

                            pipeline.addLast(new LineBasedFrameDecoder(1024));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TimeClientHandler());
                        }
                    });

            ChannelFuture ch = bootstrap.connect(new InetSocketAddress(host, port)).sync();
            System.out.println("connect to server");

            ch.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    private static class TimeClientHandler extends SimpleChannelInboundHandler<String> {

        private String timeCmd = "NOW" + System.getProperty("line.separator");

        private int counter;

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            for (int i = 0; i < 100; i++) {
                // 请求资源
                ctx.writeAndFlush(timeCmd);
            }
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("Now is: " + msg + ", counter=" + ++counter);
        }

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

3.2 DelimiterBasedFrameDecoder 分隔符

DelimiterBasedFrameDecoder 可以自动完成以分隔符作为码流结束标识的消息的解码。

服务端:

package bin.netty.tcp粘包拆包.自定义分隔符;

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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

/**
 * DelimiterBasedFrameDecoder + StringDecoder 解决 Tcp 粘包
 *
 * @author liyibin
 * @date 2021-07-10
 */
public class TimeServer {

    public static void main(String[] args) throws Exception {
        int port = 9999;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ignore) {
            }
        }

        // 启动服务器
        new TimeServer().start(port);
    }

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

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new StringEncoder());

                            // 已 $ 作为分隔符
                            pipeline.addLast(new DelimiterBasedFrameDecoder(
                                    1024, Unpooled.copiedBuffer("$".getBytes())));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TimeServerHandler());
                        }
                    });

            ChannelFuture cf = serverBootstrap.bind(port).sync();
            System.out.println("listen on " + port);

            // 等待服务器监听窗口关闭
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class TimeServerHandler extends SimpleChannelInboundHandler<String> {

        private int counter;

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("接收到客户端请命令:" + msg + ", counter=" + ++counter);

            String content = "NOW".equalsIgnoreCase(msg) ? new Date().toString() : "BAD COMMAND";

            // 字符串加上分隔符$表示结束
            ctx.writeAndFlush(content + "$");
        }

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

客户端:

package bin.netty.tcp粘包拆包.自定义分隔符;

import io.netty.bootstrap.Bootstrap;
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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

/**
 * @author liyibin
 * @date 2021-07-10
 */
public class TimeClient {

    public static void main(String[] args) throws Exception {
        int port = 9999;

        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ignore){
            }
        }

        new TimeClient().connect("127.0.0.1", port);
    }

    public void connect(String host, int port) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new StringEncoder());

                            // 已 $ 作为分隔符
                            pipeline.addLast(new DelimiterBasedFrameDecoder(
                                    1024, Unpooled.copiedBuffer("$".getBytes())));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TimeClientHandler());
                        }
                    });

            ChannelFuture ch = bootstrap.connect(new InetSocketAddress(host, port)).sync();
            System.out.println("connect to server");

            ch.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    private static class TimeClientHandler extends SimpleChannelInboundHandler<String> {
        private int counter;

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            for (int i = 0; i < 100; i++) {
                // 请求资源
                ctx.writeAndFlush("NOW$");
            }
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("Now is: " + msg + ", counter=" + ++counter);
        }

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

3.3 FixedLengthFrameDecoder 固定长度

FixedLengthFrameDecoder 是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑 TCP 的粘包/拆包问题。

服务端:

package bin.netty.tcp粘包拆包.固定长度;

import io.netty.bootstrap.ServerBootstrap;
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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

/**
 * FixedLengthFrameDecoder + StringDecoder 解决 Tcp 粘包
 *
 * @author liyibin
 * @date 2021-07-10
 */
public class TimeServer {

    public static void main(String[] args) throws Exception {
        int port = 9999;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ignore) {
            }
        }

        // 启动服务器
        new TimeServer().start(port);
    }

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

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new StringEncoder());

                            // 固定长度 3
                            pipeline.addLast(new FixedLengthFrameDecoder(3));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TimeServerHandler());
                        }
                    });

            ChannelFuture cf = serverBootstrap.bind(port).sync();
            System.out.println("listen on " + port);

            // 等待服务器监听窗口关闭
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class TimeServerHandler extends SimpleChannelInboundHandler<String> {

        private int counter;

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("接收到客户端请命令:" + msg + ", counter=" + ++counter);

            String content = "now".equalsIgnoreCase(msg) ? new Date().toString() : "BAD COMMAND";

            // 字符串加上分隔符$表示结束
            ctx.writeAndFlush(content);
        }

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

客户端:使用 Telnet 进行测试
telnet host ip
在这里插入图片描述
服务器回显内容
在这里插入图片描述

3.4 自定义协议 + 编解码器

  1. 使用自定义协议 + 编解码器来解决。
  2. 关键就是要解决服务器端每次读取数据长度的问题。这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的 TCP 粘包,拆包 。
  3. 看下面的代码示例:
    要求客户端发送 5 个 Message 对象, 客户端每次发送一个 Message 对象。
    服务器端每次接收一个 Message, 分 5 次进行解码, 每读取到 一个 Message , 会回复一个Message 对象 给客户端。

MyMessageEncoder:

package bin.netty.protocoltcp;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.ReplayingDecoder;
import io.netty.util.CharsetUtil;

import java.util.List;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyMessageEncoder extends MessageToByteEncoder<MyMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception {
        // 写入数据长度
        out.writeInt(msg.getLen());

        // 写入数据内容
        out.writeBytes(msg.getContent().getBytes(CharsetUtil.UTF_8));
    }
}

MyMessageDecoder:

package bin.netty.protocoltcp;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import io.netty.util.CharsetUtil;

import java.util.List;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyMessageDecoder extends ReplayingDecoder<Void> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyMessageDecoder decode 方法被调用");

        // 数据长度
        int dataLen = in.readInt();

        // 数据
        byte[] data = new byte[dataLen];
        in.readBytes(data);

        MyMessage myMessage = new MyMessage();
        myMessage.setLen(dataLen);
        myMessage.setContent(new String(data, CharsetUtil.UTF_8));

        // 给下一个 handler处理
        out.add(myMessage);
    }
}

服务端:

package bin.netty.protocoltcp;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * 使用 自定义协议+编解码器 解决 tcp 粘包拆包
 *
 * @author liyibin
 * @date 2021-06-27
 */
public class MyServer {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            // 编码器
                            pipeline.addLast(new MyMessageEncoder());

                            // 解码器
                            pipeline.addLast(new MyMessageDecoder());

                            // 业务处理,读取客户端的消息并显示
                            pipeline.addLast(new MyServerHandler());
                        }
                    });

            ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
            channelFuture.addListener(cf -> {
                if (cf.isSuccess()) {
                    System.out.println("listen on 9999");
                }
            });

            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

MyServerHandler:

package bin.netty.protocoltcp;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

import java.nio.charset.Charset;
import java.util.UUID;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyServerHandler extends SimpleChannelInboundHandler<MyMessage> {

    private int count;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) throws Exception {
        //接收到数据,并处理
        System.out.println("服务器接收到信息如下:len=" + msg.getLen() + ", content=" + msg.getContent());
        System.out.println("服务器接收到消息包数量=" + (++this.count));

        //回复消息

        String responseContent = UUID.randomUUID().toString();

        //构建一个协议包
        MyMessage myMessage = new MyMessage();
        myMessage.setLen(responseContent.getBytes(CharsetUtil.UTF_8).length);
        myMessage.setContent(responseContent);

        ctx.writeAndFlush(myMessage);
    }
}

客户端:

package bin.netty.protocoltcp;

import bin.netty.tcp粘包拆包.MyClientHandler1;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            // 编码器
                            pipeline.addLast(new MyMessageEncoder());

                            // 解码器
                            pipeline.addLast(new MyMessageDecoder());

                            pipeline.addLast(new MyClientHandler());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
            channelFuture.addListener(cf -> {
                if (cf.isSuccess()) {
                    System.out.println("connect to server");
                }
            });

            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

MyClientHandler:

package bin.netty.protocoltcp;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

/**
 * @author liyibin
 * @date 2021-06-27
 */
public class MyClientHandler extends SimpleChannelInboundHandler<MyMessage> {

    private int count;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) throws Exception {
        int len = msg.getLen();
        String content = msg.getContent();

        System.out.println("客户端接收到消息:长度=" + len + ", content =" + content);

        System.out.println("客户端接收消息数量=" + (++this.count));
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 5; i++) {
            String content = "消息序号:" + i;

            MyMessage myMessage = new MyMessage();
            myMessage.setLen(content.getBytes(CharsetUtil.UTF_8).length);
            myMessage.setContent(content);

            // 写入消息
            ctx.writeAndFlush(myMessage);
        }
    }

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

运行结果如下:

MyMessageDecoder decode 方法被调用
客户端接收到消息:长度=36, content =731347db-4154-4c7d-9fc1-67db6057019b
客户端接收消息数量=1
MyMessageDecoder decode 方法被调用
客户端接收到消息:长度=36, content =9179b4f5-f3f8-43a6-8352-1ca32fdce7a6
客户端接收消息数量=2
MyMessageDecoder decode 方法被调用
客户端接收到消息:长度=36, content =7a3c34d9-3fd5-448b-9ff8-b2e88d3c5ef6
客户端接收消息数量=3
MyMessageDecoder decode 方法被调用
客户端接收到消息:长度=36, content =93c7d873-5c83-4ad4-be23-e9b007bbaac0
客户端接收消息数量=4
MyMessageDecoder decode 方法被调用
客户端接收到消息:长度=36, content =50221cac-5bae-4667-8a45-9bdf5d18f1d1
客户端接收消息数量=5

可以看到没有出现粘包和拆包的情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值