Netty入门教程6 ——TCP 粘包与半包的概念

进行 Socket 编程时经常会碰到 TCP 的粘包与半包问题,很多时候我们选用 netty 等框架而不直接采用原生的 Socket 编程也是因为 netty 帮我们将该类传输过程中可能出现的问题屏蔽掉了,使我们可以抽出更多精力来关注功能的实现,而不是挣扎在处理这些底层问题上。但尽管如此,我们也必须要对这些问题有所了解。

认识问题

想要了解粘包与半包问题,首先要了解 TCP 报文的发送过程,以传统的 BIO 为例子:

1. 我们调用操作系统提供的系统函数,建立一个 Socket 监听,监听线程会阻塞在 socket 的 accept 方法上,直到有连接请求到来。

image

2. 有客户端发起连接请求,服务端与客户端进行 三次握手 。三次握手 是操作系统层面的协议栈完成的,我们在应用层编程感知不到,直到三次握手完成,客户端与服务端建立了一个 TCP 连接,我们步骤 1 中阻塞在 accept 方法的线程被唤醒。

3. 连接建立后,操作系统会在 内核空间 为本次连接分配两个缓冲区:发送缓冲区 和 接收缓冲区(体现了 TCP 协议是全双工协议), 我们可以通过 socket 实例拿到这两个缓冲区。之后数据从发送缓冲区封装为 TCP 报文传输到网卡、以及接收到的报文被层层拆包后内容传输到接收缓冲区是操作系统的任务。我们要做的,是建立一个线程扫描接收缓冲区,一旦有数据写入则将数据读入进程空间;同时如果有数据需要发送则将数据写入发送缓冲区。

image

服务端与客户端就这样,接收缓冲区一旦接收到数据便读取进进程空间,有数据需要发送就写入到发送缓冲区,循环往复直到本次连接完成四次挥手。

我们可以发现,有数据就读,我们并无法得知这些数据的边界。比如,客户端发送了两个报文 AB 和 CD ,因为报文的大小很小,如果两次发送的间隔时间很短的话,很可能 AB 还在发送缓冲区,没有来得及被封装为报文, CD 便也被写入进发送缓冲区了。这样在发送时原本应该是两个报文的数据便会被封装到一个报文中发送给服务端。服务端并无法区别这是两个报文还是一个报文,只知道把数据整个的读入进程空间中,这就是 TCP 的粘包。

再考虑一种情况,我们一次请求中携带的数据非常多,操作系统的协议栈将我们这一次请求分割为了多个报文发送到服务端。多个报文到达后,服务端并无法区别哪些包合并起来是一次完整的请求,这便是 TCP 的半包。

看起来问题的根源在于,将数据从发送缓冲区打包发出和将数据从网卡拆包写入接收缓冲区这两个动作是操作系统完成的,操作系统可能调用了标准I/O库,也可能通过更高层的封装完成这些事情,但不管怎样我们无法控制打包和拆包的时机。

再深入想一下,操作系统中协议栈的实现并没有将打包和拆包时机的控制权交给我们,协议栈是对底层协议的实现,TCP 协议便是这样定义的通讯过程。

也就是说,TCP 协议只负责建立可靠的传输通道,保证数据的准确有序的到达,但 TCP 协议不会帮我们定义数据的边界。

那么问题的根源找到了:

TCP 是流式协议,消息无边界。

(PS : UDP 虽然也可以一次传输多个包或者多次传输一个包,但每个消息都是有边界的,因为 UDP 是无连接的,因此不会有粘包和半包问题。)

解决问题

找到了问题的原因,我们再来考虑解决方案。

既然问题是传输层不帮我们确定消息的边界,那么我们在应用层自己为消息设置边界就好了。

目前主流的解决方案有四种:

1. 将数据封装为帧。也就是数据固定长度,不管你发送了什么,服务端读到固定长度的数据就判定这是一次完整的请求。

2. 通过标识位为数据添加边界。比如换行符,服务端每读到一个换行符便认定,之前读到的数据是一次完整的请求。

3. 通过固定字段标识本次请求的长度。比如我们规定每次发送数据,头两个字节标识本次请求的数据长度。服务端收到请求后先读取两个字节,转换为 int ,后读取该长度的数据。长度用完则标识一次完整的请求读完了。

4. 使用短连接,一次请求只结束便关闭该链接。这样类似 UDP ,为消息添加了天然的边界,但缺点也很明显,频繁的三次握手和四次挥手及其浪费系统资源。

方式 1 不灵活,不能充分利用系统资源,但好在实现简单;方式 2 需要对数据进行转义防止请求内容中包含我们约定的标识,但也好在实现简单;方式 3 比较通用,HTTP 协议 header 中的 Content-length 字段便是用来标识本次请求的长度,但实现较前两种而言更加复杂。

使用哪种方式要结合具体的场景决定,通常情况下推荐使用方式 3 。当然既然是为消息添加边界,方式自然多种多样,比如如果传输的是 json ,可以以 { } 对为边界来判断数据是否完整,类似该类特殊场景下的处理方式不再一一列举。

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