Netty——解决TCP粘包、拆包

一、TCP 粘包和拆包基本介绍

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。

TCP粘包、拆包图解

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

  • 1、服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包。
  • 2、服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包。
  • 3、服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包。
  • 4、服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。

二、解决TCP粘包、拆包问题

解决问题的根本手段就是找出消息的边界

Netty提供了以下三种方式解决TCP粘包和拆包问题:

  • DelimiterBasedFrameDecoder:分隔符。
  • LineBasedFrameDecoder:结束符\n。
  • FixedLengthFrameDecoder:固定长度。
  • LengthFieldBasedFrameDecoder+LengthFieldPrepender:自定义消息长度。
  • ReplayingDecoder:自定义协议。

2.1、DelimiterBasedFrameDecoder分隔符

DelimiterBasedFrameDecoder是通过发送方每条报文结束都添加特殊符号($_) 作 为 报 文 分 隔 符,接收方通过特殊符号($_)对报文进行切割。

发送方需要自行编码,添加分隔符,编码如下:

ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + "$_").getBytes())); // 以$_结尾

接收方的解码如下:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1 << 10, Unpooled.copiedBuffer("$_".getBytes())));

缺点:发送的内容本身可能会出现分隔符,需要对发送的内容进行扫描并转义,接收到的内容也要进行反转义。

一种解决策略是,发送方对需要发送的内容预先进行base64编码,由于base64编码只包含64个字符:0-9、a-z、A-Z、+、/,我们可以选择这64个字符之外的特殊字符作为分隔符。

DelimiterBasedFrameDecoder提供了多个构造方法,最终调用的都是以下构造方法:

public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters)

参数说明:

  • maxLength:表示一行最大的长度,如果超过这个长度依然没有检测到分隔符,将会抛出TooLongFrameException。
  • failFast:与maxLength联合使用,表示超过maxLength后,抛出TooLongFrameException的时机。如果为true,则超出maxLength后立即抛出TooLongFrameException,不继续进行解码;如果为false,则等到完整的消息被解码后,再抛出TooLongFrameException异常。
  • stripDelimiter:解码后的消息是否去除分隔符。
  • delimiters:分隔符。我们需要先将分割符,写入到ByteBuf中,然后当做参数传入。

2.2、LineBasedFrameDecoder结束符\n

LineBasedFrameDecoder可以当成是一种特殊的DelimiterBasedFrameDecoder,其分隔符为\n或者\r\n。

发送方的编码如下:

ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + "\n").getBytes())); // 以\n结尾

接收方的解码如下:

ch.pipeline().addLast(new LineBasedFrameDecoder(1 << 10));

2.3、FixedLengthFrameDecoder固定长度

FixedLengthFrameDecoder是通过发送方固定每条报文长度均为n个字节,接收方也通过n个字节长度切分报文。

发送方需要自行补齐长度,编码如下:

ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + "          ").getBytes())); // 补齐长度为16

接收方的解码如下:

ch.pipeline().addLast(new FixedLengthFrameDecoder(16));

缺点:如果发送的内容比较小,需要补齐长度,空间浪费,如果要发送的内容突然变大,需要调整发送方和接收方的长度。

2.4、LengthFieldBasedFrameDecoder+LengthFieldPrepender自定义消息长度

  • LengthFieldPrepender对自定义消息长度进行编码。

  • LengthFieldBasedFrameDecoder对自定义消息长度进行解码。

LengthFieldBasedFrameDecoder的构造方法如下:

public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {

参数说明:

  • byteOrder:数据存储采用大端模式或小端模式
  • maxFrameLength:发送的数据帧最大长度
  • lengthFieldOffset: 发送的字节数组中从下标lengthFieldOffset开始存放的是报文数据的长度。
  • lengthFieldLength: 在发送的字节数组中,报文数据的长度占几位,也就是字节数组bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength]存放的是报文数据的长度
  • lengthAdjustment: 长度域的偏移量矫正。如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长-长度域的值-长度域偏移 – 长度域长。
  • initialBytesToStrip:接收到的发送数据包,去除前initialBytesToStrip位
  • failFast:为true表示读取到长度域超过maxFrameLength,就抛出一个TooLongFrameException。为false表示只有真正读取完长度域的值表示的字节之后,才会抛出TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。

场景一

  • lengthFieldOffset=0
  • lengthFieldLength=2
  • lengthAdjustment=0
  • initialBytesToStrip=0

场景二

  • lengthFieldOffset=0
  • lengthFieldLength=2
  • lengthAdjustment=0
  • initialBytesToStrip=2

场景三

  • lengthFieldOffset=0
  • lengthFieldLength=2
  • lengthAdjustment=整个包长(14)-长度域的值(14)-长度域偏移(0)-长度域长(2)=-2
  • initialBytesToStrip=0

场景四

  • lengthFieldOffset=2
  • lengthFieldLength=3
  • lengthAdjustment=0
  • initialBytesToStrip=0

场景五

  • lengthFieldOffset=0
  • lengthFieldLength=3
  • lengthAdjustment=2
  • initialBytesToStrip=0

场景六

  • lengthFieldOffset=1
  • lengthFieldLength=2
  • lengthAdjustment=1
  • initialBytesToStrip=3

场景七

  • lengthFieldOffset=1
  • lengthFieldLength=2
  • lengthAdjustment=整个包长(16)-长度域的值(16)-长度域偏移(1)-长度域长(2)=-3
  • initialBytesToStrip=3

参考:
https://www.cnblogs.com/Leo_wl/p/10297113.html

https://www.cnblogs.com/sidesky/p/6913109.html

https://blog.csdn.net/u022812849/article/details/107254239

//www.greatytc.com/p/c90ec659397c

https://network.51cto.com/art/201910/604438.htm

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容