Netty 权威指南笔记(三):TCP 粘包和拆包

Netty 权威指南笔记(三):TCP 粘包和拆包

什么是 TCP 粘包和拆包?

TCP 是一个“流”协议,所谓“流”就是没有界限的一串数据。大家可以想像河流里的水,期间并没有分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分。所以,在业务上,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包,封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

为什么会发生粘包和拆包?

主要是应用程序写入数据大小、缓冲区大小、TCP/IP 最大报文大小、以太网帧 payload 大小不一致造成。

  1. 应用程序一次写入的字节数大于发送缓冲区大小。
  2. 进行 TSS 大小的 TCP 分段。
  3. 以太网帧的 payload 大于 MTU 进行 IP 分片。
  4. 应用程序多次写入少量数据,导致粘包。

解决策略

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。根据业界的主流协议的解决方案,归纳如下:

  1. 消息定长。
  2. 包尾增加分隔符,比如 FTP 协议使用回车换行符进行分割。
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度的字段,这正是 TCP/UDP/IP 报文采用的方案。
  4. 更复杂的应用层协议设计。

在 Netty 中,有处理定长消息的 FixedLengthFrameDecoder、回车换行符分隔的 LineBasedFrameDecoder、特殊分隔符的 DelimiterBasedFrameDecoder,以及处理特殊协议的 Decoder,比如处理 HTTP 协议的 HttpRequestDecoder、HttpResponseDecoder 等。

下面,我们以定长消息解码器 FixedLengthFrameDecoder 为例,分析源码,学习一下其工作原理。

FixedLengthFrameDecoder 源码分析

从下面的类图中可以看出来,这些 Decoder 都继承自 ByteToMessageDecoder、ChannelHandlerAdapter,实现了 ChannelHandler 接口。回顾之前使用 Netty 开发的 TimeServer 程序中,TimeServerHandler 也是继承自 ChannelHandlerAdapter。

Netty Decoder 类图.png

ChannelHandler 接口中,与读取数据相关的主要是 channelRead 方法。

public interface ChannelHandler {
    // 读取数据
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
    // 读取数据完成
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
}

下面我们看一下 ByteToMessageDecoder 中实现的 channelRead 方法:

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 只处理 ByteBuf 类型数据
        if (msg instanceof ByteBuf) {
            // RecyclableArrayList 是一个可循环使用的 ArrayList,使用它是为了减少 GC
            RecyclableArrayList out = RecyclableArrayList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                // cumulation 是上次处理数据后遗留的半包数据
                first = cumulation == null;
                if (first) {
                    cumulation = data;
                } else {
                    // 上次遗留数据和本次数据进行合并
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                // 对数据进行解码,解码成功的数据存入 out,半包数据赋值给 cumulation
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Throwable t) {
                throw new DecoderException(t);
            } finally {
                if (cumulation != null && !cumulation.isReadable()) {
                    cumulation.release();
                    cumulation = null;
                }
                int size = out.size();

                // 如果 out.size 大于 0,表示有解码成功的数据,发送到下一个 ChannelAdapter 进行处理
                for (int i = 0; i < size; i ++) {
                    ctx.fireChannelRead(out.get(i));
                }
                out.recycle();
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                int outSize = out.size();
                int oldInputLength = in.readableBytes();
                // 解析数据,并存入 out 中
                decode(ctx, in, out);

                // Check if this handler was removed before continuing the loop.
                // If it was removed, it is not safe to continue to operate on the buffer.
                if (ctx.isRemoved()) {
                    break;
                }

                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                        continue;
                    }
                }
                // 如果成功解码出数据,但是 ByteBuf 中数据长度不变,可能会导致死循环
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
                }

                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable cause) {
            throw new DecoderException(cause);
        }
    }

    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
  1. 首先判断输入数据是否是 ByteBuf 类型,是则处理,否则传递下去。
  2. 然后初始化一个 RecyclableArrayList 用来保存解析成功的数据片。
  3. 将上次解析遗留的数据 cumulation 和本次到来的数据进行合并。
  4. 调用 decode 方法循环解析合并后的数据,存入列表 out。
  5. 如果列表 out 中有解析成功的数据,则调用 fireChannelRead 方法发送给下一个 ChannelHandler 处理。

decode 方法是一个抽象方法,由子类 FixedLengthFrameDecoder 负责实现,具体源码如下所示。其原理是,如果输入数据 ByteBuf 长度超过 frameLength,则截取前 frameLength 字节数据为一个新的 ByteBuf 数据分片,存入列表 out 中。

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) {
        if (frameLength <= 0) {
            throw new IllegalArgumentException(
                    "frameLength must be a positive integer: " + frameLength);
        }
        this.frameLength = frameLength;
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            // 读取固定长度 frameLength 的数据分片,存入列表 out 中
            out.add(decoded);
        }
    }
    protected Object decode(@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // 如果小于长度 frameLength,则返回 null
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            // 否则,读取长度为 frameLength 的数据,为一个 ByteBuf 数据分片
            return in.readSlice(frameLength).retain();
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容