Netty系列(四)TCP拆包和粘包
一、拆包和粘包问题
(1) 一个小的Socket Buffer问题
在基于流的传输里比如 TCP/IP,接收到的数据会先被存储到一个 socket 接收缓冲里。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了 2 个独立的数据包,操作系统也不会作为 2 个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。举个例子,让我们假设操作系统的 TCP/TP 协议栈已经接收了 3 个数据包:
由于基于流传输的协议的这种普通的性质,在你的应用程序里读取数据的时候会有很高的可能性被分成下面的片段。
因此,一个接收方不管他是客户端还是服务端,都应该把接收到的数据整理成一个或者多个更有意思并且能够让程序的业务逻辑更好理解的数据。在上面的例子中,接收到的数据应该被构造成下面的格式:
测试:
- 在 client 端向 server 端发送三次数据
//向服务器发送数据 buf f.channel().writeAndFlush(Unpooled.copiedBuffer("ABC".getBytes())); f.channel().writeAndFlush(Unpooled.copiedBuffer("DEF".getBytes())); f.channel().writeAndFlush(Unpooled.copiedBuffer("GHI".getBytes()));
- server 端可能将三次传输的数据当成一次请求,服务器收到的结果如下
ABCDEFGHI
(2) 解决方案
拆包和粘包问题的解决方案,根据业界主流协议,在有三种方案,前三种 Netty 已经实现:
- 消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格。
- 在包尾部增加特殊字符进行分割,例如加回车等。
- 将消息分为定长消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。通常设计思烙为消息头的第一个字段使用 int32 来表示消息的总长度。
二、定长方案 - FixedLengthFrameDecoder
FixedLengthFrameDecoder 是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑 TCP 的粘包/拆包问题,非常实用。注意:长度不够的忽略。
StringDecoder 的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的 Handler。 FixedLengthFrameDecoder + StringDecoder 组合就是按固定长度的文本解码。
- 在 Server 中添加如下配制:
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()); } })
- 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); }
- Client 发送的数据:
//向服务器发送数据 buf f.channel().writeAndFlush(Unpooled.copiedBuffer("aaaaabbbbb".getBytes())); f.channel().writeAndFlush(Unpooled.copiedBuffer("cccccddd".getBytes()));
- 结果如下,可以看出5个字符作为一个请求处理,不足5位的忽略:
aaaaa bbbbb ccccc
三、固定分隔符方案 - DelimiterBasedFrameDecoder
LineBasedFrameDecoder 的工作原理是它依次遍历 Bytebuf 中的可读字节,判判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
DelimiterBasedFrameDecoder 自动完成以分隔符作为码流结束标识的消息的解码。
- 在 Server 中添加如下配制:
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 LineBasedFrameDecoder(1024, buf)); //设置字符串形式的解码 ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new ServerHandler()); } })
- 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自定义协议请参考 这篇文章
参考:
《Netty 解决 TCP 拆包粘包问题》: http://ifeve.com/netty5-user-guide/#%E6%B5%81%E6%95%B0%E6%8D%AE%E7%9A%84%E4%BC%A0%E8%BE%93%E5%A4%84%E7%90%86
每天用心记录一点点。内容也许不重要,但习惯很重要!
原文地址:https://www.cnblogs.com/binarylei/p/8947167.html