Netty系列(四)TCP拆包和粘包

Netty系列(四)TCP拆包和粘包

一、拆包和粘包问题

(1) 一个小的Socket Buffer问题

在基于流的传输里比如 TCP/IP,接收到的数据会先被存储到一个 socket 接收缓冲里。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了 2 个独立的数据包,操作系统也不会作为 2 个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。举个例子,让我们假设操作系统的 TCP/TP 协议栈已经接收了 3 个数据包:

netty5_1.png

由于基于流传输的协议的这种普通的性质,在你的应用程序里读取数据的时候会有很高的可能性被分成下面的片段。

netty5_2.png

因此,一个接收方不管他是客户端还是服务端,都应该把接收到的数据整理成一个或者多个更有意思并且能够让程序的业务逻辑更好理解的数据。在上面的例子中,接收到的数据应该被构造成下面的格式:

测试:

netty5_3.png

  1. 在 client 端向 server 端发送三次数据

    //向服务器发送数据 buf
    f.channel().writeAndFlush(Unpooled.copiedBuffer("ABC".getBytes()));
    f.channel().writeAndFlush(Unpooled.copiedBuffer("DEF".getBytes()));
    f.channel().writeAndFlush(Unpooled.copiedBuffer("GHI".getBytes()));
  2. server 端可能将三次传输的数据当成一次请求,服务器收到的结果如下

    ABCDEFGHI

(2) 解决方案

拆包和粘包问题的解决方案,根据业界主流协议,在有三种方案,前三种 Netty 已经实现:

  1. 消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格。

  2. 在包尾部增加特殊字符进行分割,例如加回车等。

  3. 将消息分为定长消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。通常设计思烙为消息头的第一个字段使用 int32 来表示消息的总长度。

二、定长方案 - FixedLengthFrameDecoder

  1. 在 Server 中添加如下配制:[com.github.binarylei.t1]
childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel sc) throws Exception {
        //定长拆包:5个字符,不足5位则忽略
        sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
        //设置字符串形式的解码
        sc.pipeline().addLast(new StringDecoder());
        sc.pipeline().addLast(new ServerHandler());
    }
})
  1. ServerHandler 中接收请求的数据:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    System.out.println((String)msg);

    //写给客户端
    ChannelFuture f = ctx.writeAndFlush(Unpooled.copiedBuffer(((String)msg).getBytes()));
    //写完成后会自动关闭客户端
    //f.addListener(ChannelFutureListener.CLOSE);
}
  1. Client 发送的数据:
//向服务器发送数据 buf
f.channel().writeAndFlush(Unpooled.copiedBuffer("aaaaabbbbb".getBytes()));
f.channel().writeAndFlush(Unpooled.copiedBuffer("cccccddd".getBytes()));
  1. 结果如下,可以看出5个字符作为一个请求处理,不足5位的忽略:
aaaaa
bbbbb
ccccc

三、固定分隔符方案 - DelimiterBasedFrameDecoder

  1. 在 Server 中添加如下配制:[com.github.binarylei.t2]
childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
        //设置字符串形式的解码
        ch.pipeline().addLast(new StringDecoder());
        ch.pipeline().addLast(new ServerHandler());
    }
})
  1. ServerHandler 中接收请求的数据:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    System.out.println((String)msg);

    //写给客户端
    ChannelFuture f = ctx.writeAndFlush(Unpooled.copiedBuffer("netty$_".getBytes()));
    //写完成后会自动关闭客户端
    f.addListener(ChannelFutureListener.CLOSE);

}

结果如下,可以看出请求是分三次处理的:

ABC
DEF
GHI

自定义协议

Netty自定义协议请参考 这篇文章


每天用心记录一点点。内容也许不重要,但习惯很重要!

猜你喜欢

转载自www.cnblogs.com/binarylei/p/8947167.html