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


深入理解Netty编解码、粘包拆包、心跳机制文章插图
本文作者:何建辉(公众号:org_yijiaoqian)
前言Netty系列文章:

  • BIO 、NIO 、AIO 总结
  • Unix网络编程中的五种IO模型
  • 深入理解IO多路复用实现机制
  • Netty核心功能与线程模型
前面我们讲了 BIO、NIO、AIO 等一些基础知识和Netty核心功能与线程模型 , 本篇重点来理解Netty的编解码、粘包拆包、心跳机制等实现原理进行讲解 。
Netty编解码Netty 涉及到编解码的组件有 Channel 、 ChannelHandler 、 ChannelPipe 等 , 我们先大概了解下这几个组件的作用 。
ChannelHandlerChannelHandler 充当来处理入站和出站数据的应用程序逻辑容器 。 例如 , 实现 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdapter) , 你就可以接收入站事件和数据 , 这些数据随后会被你的应用程序的业务逻辑处理 。 当你要给连接的客户端发送响应时 , 也可以从 ChannelInboundHandler 刷数据 。 你的业务逻辑通常下在一个或者多个 ChannelInboundHandler 中 。
ChannelOutboundHandler 原理一样 , 只不过它是用来处理出站数据的 。
ChannelPipelineChannelPipeline 提供了 ChannelHandler 链的容器 。 以客户端应用程序为例 , 如果有事件的运动方向是从客户端到服务端 , 那么我们称这些事件为出站的 , 即客户端发送给服务端的数据会通过 pipeline 中的一系列 ChannelOutboundHandler (ChannelOutboundHandler 调用是从 tail 到 head 方向逐个调用每个 handler 的逻辑) , 并被这些 Hadnler 处理 , 反之称为入站的 , 入站只调用 pipeline 里的 ChannelInboundHandler 逻辑(ChannelInboundHandler 调用是从 head 到 tail 方向 逐个调用每个 handler 的逻辑 。 )
深入理解Netty编解码、粘包拆包、心跳机制文章插图
编解码器当你通过Netty发送或者接受一个消息的时候 , 就将会发生一次数据转换 。 入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息 , 它会被编码成字节 。
Netty提供了一系列实用的编码解码器 , 它们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口 。 在这些类中 ,channelRead方法已经被重写了 。
以入站为例 , 对于每个从入站Channel读取的消息 , 这个方法会被调用 。 随后 , 它将调用由已知解码器所提供的decode()方法进行解码 , 并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler 。
Netty提供了很多编解码器 , 比如编解码字符串的StringEncoder和StringDecoder , 编解码对象的ObjectEncoder和ObjectDecoder 等 。
当然也可以通过集成ByteToMessageDecoder自定义编解码器 。
示例代码完整代码在 Github :
对应的包 com.niuh.netty.codec
Netty粘包拆包TCP 粘包拆包是指发送方发送的若干包数据到接收方接收时粘成一包或某个数据包被拆开接收 。 如下图所示 , client 发送了两个数据包 D1 和 D2 , 但是 server 端可能会收到如下几种情况的数据 。
深入理解Netty编解码、粘包拆包、心跳机制文章插图
程序演示首先准备客户端负责发送消息 , 连续发送5次消息 , 代码如下:
public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 1; i <= 5; i++) {ByteBuf byteBuf = Unpooled.copiedBuffer("msg No" + i + " ", Charset.forName("utf-8"));ctx.writeAndFlush(byteBuf);}}然后服务端作为接收方 , 接收并且打印结果:
// count 变量 , 用于计数private int count;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("服务器读取线程 " + Thread.currentThread().getName());ByteBuf buf = (ByteBuf) msg;byte[] bytes = new byte[buf.readableBytes()];// 把ByteBuf的数据读到bytes数组中buf.readBytes(bytes);String message = new String(bytes, Charset.forName("utf-8"));System.out.println("服务器接收到数据:" + message);// 打印接收的次数System.out.println("接收到的数据量是:" + (++this.count));}启动服务端 , 再启动两个客户端发送消息,服务端的控制台可以看到这样:
深入理解Netty编解码、粘包拆包、心跳机制文章插图
粘包的问题其实是随机的 , 所以每次结果都不太一样 。
完整代码在 Github :
对应的包 com.niuh.splitpacket0
为什么出现粘包现象?TCP 是面向连接的 , 面向流的 , 提供高可靠性服务 。 收发两端(客户端和服务器端)都要有成对的 socket , 因此 , 发送端为了将多个发送给接收端的包 , 更有效的发送给对方 , 使用了优化方法(Nagle算法) , 将多次间隔较少且数据量小的数据 , 合并成一个大的数据块 , 然后进行封包 , 这样做虽然提供了效率 , 但是接收端就难以分辨出完整的数据包了 , 因为面向流的通信是无消息保护边界的 。