深入理解Netty编解码、粘包拆包、心跳机制( 二 )


如何理解TCP是面向字节流的

  1. 应用程序和 TCP 的交互是一次一个数据块(大小不等) , 但 TCP 把应用程序交下来的数据仅仅看成是一连串的无结构的字节流 。 TCP 并不知道所传送的字节流的含义;
  2. 因此 TCP 不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系(例如 , 发送方应用程序交给发送方的 TCP 共 10 个数据块 , 但接收方的 TCP 可能只用了 4 个就把收到的字节流交付上层的应用程序);
  3. 同时 , TCP 不关心应用进程一次把多长的报文发送到 TCP 的缓存中 , 而是根据对方给出的窗口值和当前网络阻塞的程度来决定一个报文段应包含多少个字节(UDP 发送的报文长度是应用进程给出的) 。 如果应用进程传送到 TCP 缓存的数据块太长 , TCP 就可以把它划分短一点再传送 。 如果应用程序一次只发来一个字节 , TCP 也可以等待积累有足够多的字节后再构成报文段发送出去 。
TCP发送报文一般是 3 个时机
  1. 缓冲区数据达到 , 最大报文长度 MSS;
  2. 由发送端的应用进程指明要求发送报文段 , 即 TCP 支持的推送(push)操作;
  3. 当发送方的一个计时器期限到了 , 即使长度不超过 MSS , 也发送 。
解决方案一般解决粘包拆包问题有 4 中办法
  1. 在数据的末尾添加特殊的符号标识数据包的边界 。 通常会加\n、\r、\t或者其他的符号
学习 HTTP、FTP 等 , 使用回车换行符号;
  1. 在数据的头部声明数据的长度 , 按长度获取数据
将消息分为 head 和 body , head 中包含 body 长度的字段 , 一般 head 的第一个字段使用 int 值来表示 body 长度;
  1. 规定报文的长度 , 不足则补空位 。 读取时按规定好的长度来读取 。 比如 100 字节 , 如果不够就补空格;
  2. 使用更复杂的应用层协议 。
使用LineBasedFrameDecoderLineBasedFrameDecoder 是Netty内置的一个解码器 , 对应的编码器是 LineEncoder 。
原理是上面讲的第一种思路 , 在数据末尾加上特殊符号以标识边界 。 默认是使用换行符\n 。
用法很简单 , 发送方加上编码器:
@Overrideprotected void initChannel(SocketChannel ch) throws Exception { //添加编码器 , 使用默认的符号\n , 字符集是UTF-8ch.pipeline().addLast(new LineEncoder(LineSeparator.DEFAULT, CharsetUtil.UTF_8));ch.pipeline().addLast(new TcpClientHandler());}接收方加上解码器:
@Overrideprotected void initChannel(SocketChannel ch) throws Exception { //解码器需要设置数据的最大长度 , 我这里设置成1024 ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); //给pipeline管道设置业务处理器 ch.pipeline().addLast(new TcpServerHandler());}然后在发送方 , 发送消息时在末尾加上标识符:
@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {for (int i = 1; i <= 5; i++) {//在末尾加上默认的标识符\nByteBuf byteBuf = Unpooled.copiedBuffer("msg No" + i + StringUtil.LINE_FEED, Charset.forName("utf-8"));ctx.writeAndFlush(byteBuf); }}于是我们再次启动服务端和客户端 , 在服务端的控制台可以看到:
深入理解Netty编解码、粘包拆包、心跳机制文章插图
在数据的末尾添加特殊的符号标识数据包的边界 , 粘包、拆包的问题就得到解决了 。
注意:数据末尾一定是分隔符 , 分隔符后面不要再加上数据 , 否则会当做下一条数据的开始部分 。 下面是错误演示:
@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {for (int i = 1; i <= 5; i++) {//在末尾加上默认的标识符\nByteBuf byteBuf = Unpooled.copiedBuffer("msg No" + i + StringUtil.LINE_FEED + "[我是分隔符后面的字符串]", Charset.forName("utf-8"));ctx.writeAndFlush(byteBuf); }}服务端的控制台就会看到这样的打印信息:
深入理解Netty编解码、粘包拆包、心跳机制文章插图
使用自定义长度帧解码器使用这个解码器解决粘包问题的原理是上面讲的第二种 , 在数据的头部声明数据的长度 , 按长度获取数据 。 这个解码器构造器需要定义5个参数 , 相对较为复杂一点 , 先看参数的解释: