上一篇介绍了粘包和半包及其通用的解决方案,今天重点来看一下 Netty 是如何实现封装成帧(Framing)方案的。

解码核心流程

之前介绍过三种解码器FixedLengthFrameDecoderDelimiterBasedFrameDecoderLengthFieldBasedFrameDecoder,它们都继承自ByteToMessageDecoder,而ByteToMessageDecoder继承自ChannelInboundHandlerAdapter,其核心方法为channelRead。因此,我们来看看ByteToMessageDecoderchannelRead方法:

    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
// 将传入的消息转化为data
ByteBuf data = (ByteBuf) msg;
// 最终实现的目标是将数据全部放进cumulation中
first = cumulation == null;
// 第一笔数据直接放入
if (first) {
cumulation = data;
} else {
// 不是第一笔数据就进行追加
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
// 解码
callDecode(ctx, cumulation, out);
}
// 以下代码省略,因为不属于解码过程
}

再来看看callDecode方法:

    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size(); if (outSize > 0) {
// 以下代码省略,因为初始状态时,outSize 只可能是0,不可能进入这里
} int oldInputLength = in.readableBytes();
// 在进行 decode 时,不执行handler的remove操作。
// 只有当 decode 执行完之后,开始清理数据。
decodeRemovalReentryProtection(ctx, in, out); // 省略以下代码,因为后面的内容也不是解码的过程

再来看看decodeRemovalReentryProtection方法:

    final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
// 设置当前状态为正在解码
decodeState = STATE_CALLING_CHILD_DECODE;
try {
// 解码
decode(ctx, in, out);
} finally {
// 执行hander的remove操作
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {
handlerRemoved(ctx);
}
}
} // 子类都重写了该方法,每种实现都会有自己特殊的解码方式
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

从上面的过程可以总结出,在解码之前,需要先将数据写入cumulation,当解码结束后,需要通过 handler 进行移除。

具体解码过程

刚刚说到decode方法在子类中都有实现,那针对我们说的三种解码方式,一一看其实现。

FixedLengthFrameDecoder

其源码为:

    @Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
} protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// 收集到的数据是否小于固定长度,小于就代表无法解析
if (in.readableBytes() < frameLength) {
return null;
} else {
return in.readRetainedSlice(frameLength);
}
}

就和这个类的名字一样简单,就是固定长度进行解码,因此,在设置该解码器的时候,需要在构造方式里传入frameLength

DelimiterBasedFrameDecoder

其源码为:

    @Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
} protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
// 当前的分割符是否是换行分割符(\n或者\r\n)
if (lineBasedDecoder != null) {
return lineBasedDecoder.decode(ctx, buffer);
}
// Try all delimiters and choose the delimiter which yields the shortest frame.
int minFrameLength = Integer.MAX_VALUE;
ByteBuf minDelim = null;
// 其他分割符进行一次切分
for (ByteBuf delim: delimiters) {
int frameLength = indexOf(buffer, delim);
if (frameLength >= 0 && frameLength < minFrameLength) {
minFrameLength = frameLength;
minDelim = delim;
}
}
// 以下代码省略

根据它的名字可以知道,分隔符才是它的核心。它将分割符分成两类,只有换行分割符(\n或者\r\n)其他。因此,需要注意的是,你可以定义多种分割符,它都是支持的。

LengthFieldBasedFrameDecoder

该类比较复杂,如果直接看方法容易把自己看混乱,因此我准备结合类上的解释,先看看其私有变量。

2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message

Let's give another twist to the previous example. The only difference from the previous example is that the length field represents the length of the whole message instead of the message body, just like the third example. We have to count the length of HDR1 and Length into lengthAdjustment. Please note that we don't need to take the length of HDR2 into account because the length field already includes the whole header length.

 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
* +------+--------+------+----------------+ +------+----------------+
* | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
* | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
* +------+--------+------+----------------+ +------+----------------+

lengthFieldOffset : 该字段代表 Length 字段是从第几个字节开始的。上面的例子里,Length 字段是从第1个字节开始(HDR1 是第0个字节),因此该值即为0。

lengthFieldLength : 该字段代表 Length 字段所占用的字节数。上面的例子里,Length 字段占用2个字节,因此该值为2。

lengthAdjustment : 该字段代表 Length 字段结束位置到真正的内容开始位置的距离。上面例子里,因为 Length 字段的含义是整个消息(包括 HDR1、Length、HDR2、Actual Content,一般 Length 指的只是 Actual Content),所以 Length 末尾到真正的内容开始位置(HDR1的开始处),相当于减少3个字节,所以是-3。

initialBytesToStrip : 展示时需要从 Length 字段末尾开始跳过几个字节。上面例子里,因为真正的内容是从 HDR1 开始的,最终展示的内容是从 HDR2 开始的,所以中间差了3个字节,所以该值是3。

该类的解码方法比较复杂,有兴趣的同学可以试着自己分析一下。

总结

这一篇主要是结合 Netty 里的源代码讲解了 Netty 中封装成帧(Framing)的三种方式,相信你一定有了不一样的理解。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

https://death00.github.io/

Netty - 粘包和半包(下)的更多相关文章

  1. Netty - 粘包和半包(上)

    在网络传输中,粘包和半包应该是最常出现的问题,作为 Java 中最常使用的 NIO 网络框架 Netty,它又是如何解决的呢?今天就让我们来看看. 定义 TCP 传输中,客户端发送数据,实际是把数据写 ...

  2. TCP的粘包、半包和Netty的处理

    参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! 什么是粘包和半包 在客户端发送数据时,实际是把数据写入到了TCP发送 ...

  3. C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

    介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解 ...

  4. 关于TCP封包、粘包、半包

    关于Tcp封包 很多朋友已经对此作了不少研究,也花费不少心血编写了实现代码和blog文档.当然也充斥着一些各式的评论,自己看了一下,总结一些心得. 首先我们学习一下这些朋友的心得,他们是: http: ...

  5. socket编程 TCP 粘包和半包 的问题及解决办法

    一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系. 粘包 为x.5个包 半包 为0.5个包 由于网络原因 一次 ...

  6. 详说tcp粘包和半包

    tcp服务端和客户端建立连接后会长时间维持这个连接,用于互相传递数据,tcp是以流的方式传输数据的,就像一个水管里的水一样,从一头不断的流向另一头. 理想情况下,发送的数据包都是独立的, 现实要复杂一 ...

  7. c# socket 解决粘包,半包

    处理原理: 半包:即一条消息底层分几次发送,先有个头包读取整条消息的长度,当不满足长度时,将消息临时缓存起来,直到满足长度再解码 粘包:两条完整/不完整消息粘在一起,一般是解码完上一条消息,然后再判断 ...

  8. TCP的组包、半包、粘包与分包

    一.概念 1)组包.简单的说就是tcp协议把过大的数据包分成了几个小的包传输,接收方要把同一组的数据包重新组合成一个完整的数据包. 2)半包.指接受方没有接受到一个完整的包,只接受了部分,这种情况主要 ...

  9. c# Socket通讯中关于粘包,半包的处理,加分割符

    using System; using System.Collections.Generic; using System.Text; using System.Net.Sockets; using S ...

随机推荐

  1. windows10 安装win10和ubuntu 16.04双系统

    安装教程如下 亲测可用 https://www.cnblogs.com/masbay/p/10844857.html 镜像路径如下 http://releases.ubuntu.com/16.04/ ...

  2. 深挖计算机基础:趣谈Linux操作系统学习笔记

    参考极客时间专栏<趣谈Linux操作系统>学习笔记 核心原理篇:内存管理 趣谈Linux操作系统学习笔记:第二十讲 趣谈Linux操作系统学习笔记:第二十一讲 趣谈Linux操作系统学习笔 ...

  3. [译]Vulkan教程(08)逻辑设备和队列

    [译]Vulkan教程(08)逻辑设备和队列 Introduction 入门 After selecting a physical device to use we need to set up a  ...

  4. ansible执行带有环境变量的脚本不生效

    1背景 jenkins发布时,使用ansible执行远程主机上的启动tomcat脚本发现不生效,启动tomcat的脚本中有环境变量. ansible主机为:172.16.35.8 tomcat服务器为 ...

  5. opencv在VS2017上的环境搭建

    最近开始做一个图像识别的小项目,需要安装opencv,VS里报的错迷的一批,网上教程好多,找了好长时间,终于找的两个解决了问题,在这儿记录一下. 安装很简单,在opencv官网(https://ope ...

  6. jQuery-文件上传问题解决

    后端要求文件上传需传参数为二进制流,用form-data方式传递,如下图所示: 为了满足该输入参数要求,上传代码如下: <input type="file" id=" ...

  7. 04-Uwsgi配置启动Nginx虚拟主机配置

    一.虚拟环境的安装 1.安装虚拟环境 sudo pip3 install virtualenv 2.安装virtualenvwrapper sudo pip3 install virtualenvwr ...

  8. (转)python中用logging实现日志滚动和过期日志删除

    转自:https://blog.csdn.net/ashi198866/article/details/46725813 logging库提供了两个可以用于日志滚动的class(可以参考https:/ ...

  9. Taro自定义Modal对话框组件|taro仿微信、android弹窗

    基于Taro多端实践TaroPop:自定义模态框|dialog对话框|msg消息框|Toast提示 taro自定义弹出框支持编译到多端H5/小程序/ReactNative,还可以自定义弹窗类型/弹窗样 ...

  10. CSS学习笔记-背景属性

    一.背景尺寸属性:    1.含义:        背景尺寸属性是CSS3中新增的一个属性,专门用于设置背景图片大小 2.格式:        1.1具体像素:             backgro ...