netty学习笔记14 - TCP粘包和拆包

TCP粘包与拆包基本介绍

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

下面我们来编写一个程序,如果没有做处理,就会发生粘包和拆包问题

客户端程序:

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new NettyClientInitialize());

            System.out.println("客户端启动。。。");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}


/////////////////////////////////////
public class NettyClientInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new NettyClientHandler());
    }
}

/////////////////////////////////////////
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 通道就绪就会触发此方法, 发送10条数据给服务器
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            String msg = "Hello,服务器,我是消息"  +  i;
            ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到服务器发来消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

服务端程序

public class NettyServer {

    public static void main(String[] args) throws InterruptedException {
        // 初始化2个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 引导程序
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new NettyServerInitialize());

            System.out.println("服务器启动。。。");
            ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

//////////////////////////////////////////////////////////////////////
public class NettyServerInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new NettyServerHandler());
    }
}

////////////////////////////////////////////////////////////////////
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    private int count;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务器ChannelRead方法被调用"+ (++count) + "次数");
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到客户端发来消息:"+ byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 发送消息给客户端
        String msg = "Hello,我是服务器  ";
        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
    }
}

我没来运行2个客户端,来看看测试结果

服务器启动。。。
服务器ChannelRead方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0Hello,服务器,我是消息1Hello,服务器,我是消息2Hello,服务器,我是消息3Hello,服务器,我是消息4Hello,服务器,我是消息5Hello,服务器,我是消息6Hello,服务器,我是消息7Hello,服务器,我是消息8Hello,服务器,我是消息9
服务器ChannelRead方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
服务器ChannelRead方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
服务器ChannelRead方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2Hello,服务器,我是消息3Hello,服务器,我是消息4
服务器ChannelRead方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息5Hello,服务器,我是消息6
服务器ChannelRead方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息7
服务器ChannelRead方法被调用6次数
收到客户端发来消息:Hello,服务器,我是消息8Hello,服务器,我是消息9
客户端启动。。。
收到服务器发来消息:Hello,我是服务器  Hello,我是服务器  Hello,我是服务器  Hello,我是服务器  Hello,我是服务器  Hello,我是服务器 

从运行结果 我们可以分析得出:

  1. 当我们从客户端连续发送10次消息到服务器,服务器这边接收不一定分10次接收,
  2. 服务器接收客户端发来的消息可以使一次性接收所有消息,也有可能是2次,3次或者9次等。这就是TCP的粘包和拆包问题了
  3. 服务器无法分辨出客户端发送消息的边界,因此我们需要来解决TCP粘包和拆包问题

TCP粘包和拆包解决 方案

  1. 使用自定义协议 + 编解码器来解决
  2. 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或者少读 的问题,从而避免 了TCP粘包,拆包。

代码示例(自定义协议 + 编解码器):

  • 要求客户端发送 5 个 Message 对象, 客户端每次发送一个 Message 对象
  • 服务器端每次接收一个Message, 分5次进行解码, 每读取到 一个Message , 会回复一个Message 对象 给客户端.

客户端程序

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new NettyClientInitialize());

            System.out.println("客户端启动。。。");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

/////////////////////////////////public class NettyClientInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 加入编码器
        pipeline.addLast(new MessageEncoder());
        // 加入解码器
        pipeline.addLast(new  MessageDecoder());
        pipeline.addLast(new NettyClientHandler());
    }
}

客户端处理器

public class NettyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    /**
     * 通道就绪就会触发此方法, 发送10条数据给服务器
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 5; i++) {
            String msg = "Hello,服务器,我是消息"  +  i;
            byte[] content = msg.getBytes(CharsetUtil.UTF_8);
            int length = msg.getBytes(CharsetUtil.UTF_8).length;

            // 封装消息
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLength(length);
            messageProtocol.setContent(content);

            ctx.writeAndFlush(messageProtocol);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol) throws Exception {
        System.out.println("收到服务器发来消息:" + new String(messageProtocol.getContent()));
    }
}

协议类(关键)

public class MessageProtocol {
    // 表示发送数据的长度
    private int length;
    // 发送数据的内容
    private byte[] content;
    }

解码器和编码器(关键)

/**
 * 编码器
 */
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol, ByteBuf byteBuf) throws Exception {
        System.out.println("编码器被调用");
        byteBuf.writeInt(messageProtocol.getLength());
        byteBuf.writeBytes(messageProtocol.getContent());
    }
}
/**
 * 解码器
 */
public class MessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        System.out.println("解码器被调用");
        // 得到消息长度和内容
        int length = byteBuf.readInt();
        byte[] content = new byte[length];
        byteBuf.readBytes(content);

        // 将消息封装成MessagePool,放入 list, 传递下一个handler业务处理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLength(length);
        messageProtocol.setContent(content);

        list.add(messageProtocol);
    }
}

服务端程序

public class NettyServer {

    public static void main(String[] args) throws InterruptedException {
        // 初始化2个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 引导程序
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new NettyServerInitialize());

            System.out.println("服务器启动。。。");
            ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class NettyServerInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 加入编解码器
        pipeline.addLast(new MessageEncoder());
        pipeline.addLast(new MessageDecoder());
        pipeline.addLast(new NettyServerHandler());
    }
}

服务端处理器

public class NettyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    private int count;

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol) throws Exception {
        System.out.println("服务器channelRead0方法被调用"+ (++count) + "次数");
        byte[] content = messageProtocol.getContent();
        System.out.println("收到客户端发来消息:"+ new String(content));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 发送消息给客户端
        String msg = "Hello,我是服务器  ";
        byte[] content = msg.getBytes(CharsetUtil.UTF_8);
        int length = msg.getBytes(CharsetUtil.UTF_8).length;
        // 封装对象
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLength(length);
        messageProtocol.setContent(content);
        ctx.writeAndFlush(messageProtocol);
    }
}

运行结果(启动多个客户多)

服务器启动。。。
解码器被调用
服务器channelRead0方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
解码器被调用
服务器channelRead0方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
解码器被调用
服务器channelRead0方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2
解码器被调用
服务器channelRead0方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息3
解码器被调用
服务器channelRead0方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息4
编码器被调用
解码器被调用
服务器channelRead0方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
编码器被调用
解码器被调用
服务器channelRead0方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
编码器被调用
解码器被调用
服务器channelRead0方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2
解码器被调用
服务器channelRead0方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息3
编码器被调用
解码器被调用
服务器channelRead0方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息4

从结果我们可得知,客户端发送5次信息,客户端根据协议接收5次消息,启用多个客户端测试结果还是一样,这样就完美结果了TCP粘包和拆包问题,

解决TCP粘包和拆包的关键在于定义发送消息的协议和编解码器,让消息根据协议和编解码器发送,从而结果了TCP拆包和粘问题。

发布了83 篇原创文章 · 获赞 3 · 访问量 9836

猜你喜欢

转载自blog.csdn.net/fyj13925475957/article/details/104425772