QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

为什么 TCP 协议有粘包问题

  • 2020-03-20
  • 本文字数:3086 字

    阅读完需:约 10 分钟

为什么 TCP 协议有粘包问题

为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。


TCP/IP 协议簇建立了互联网中通信协议的概念模型,该协议簇中的两个主要协议就是 TCP 和 IP 协议。TCP/ IP 协议簇中的 TCP 协议能够保证数据段(Segment)的可靠性和顺序,有了可靠的传输层协议之后,应用层协议就可以直接使用 TCP 协议传输数据,不在需要关心数据段的丢失和重复问题1



图 1 - TCP 协议与应用层协议


IP 协议解决了数据包(Packet)的路由和传输,上层的 TCP 协议不再关注路由和寻址2,那么 TCP 协议解决的是传输的可靠性和顺序问题,上层不需要关心数据能否传输到目标进程,只要写入 TCP 协议的缓冲区的数据,协议栈几乎都能保证数据的送达。


当应用层协议使用 TCP 协议传输数据时,TCP 协议可能会将应用层发送的数据分成多个包依次发送,而数据的接收方收到的数据段可能有多个『应用层数据包』组成,所以当应用层从 TCP 缓冲区中读取数据时发现粘连的数据包时,需要对收到的数据进行拆分。


粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解,忽略了 TCP 协议的定义并且缺乏设计应用层协议的经验。本文将从 TCP 协议以及应用层协议出发,分析我们经常提到的 TCP 协议中的粘包是如何发生的:


  • TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;

  • 应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;


很多人可能会认为粘包是一个比较低级的甚至不值得讨论的问题,但是在作者看来这个问题还是很有趣的,不是所有人都系统性地学过基于 TCP 的应用层协议设计,也不是所有人对 TCP 协议也没有那么深入的理解,相信很多人学习编程的过程都是自底向上的,所以作者认为这是一个值得回答的问题,我们应该传递正确的知识,而不是负面的和居高临下的情绪。

面向字节流

TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议3,应用层交给 TCP 协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。


Nagle 算法是一种通过减少数据包的方式提高 TCP 传输性能的算法4。因为网络


带宽有限,它不会将小的数据块直接发送到目的主机,而是会在本地缓冲区中等待更多待发送的数据,这种批量发送数据的策略虽然会影响实时性和网络延迟,但是能够降低网络拥堵的可能性并减少额外开销。


在早期的互联网中,Telnet 是被广泛使用的应用程序,然而使用 Telnet 会产生大量只有 1 字节负载的有效数据,每个数据包都会有 40 字节的额外开销,带宽的利用率只有 ~2.44%,Nagle 算法就是在当时的这种场景下设计的。


当应用层协议通过 TCP 协议传输数据时,实际上待发送的数据先被写入了 TCP 协议的缓冲区,如果用户开启了 Nagle 算法,那么 TCP 协议可能不会立刻发送写入的数据,它会等待缓冲区中数据超过最大数据段(MSS)或者上一个数据段被 ACK 时才会发送缓冲区中的数据。



图 2 - Nagle 算法


几十年前还会发生网络拥塞的问题,但是今天的网络带宽资源不再像过去那么紧张,在默认情况下,Linux 内核都会使用如下的方式默认关闭 Nagle 算法:


TCP_NODELAY = 1
复制代码


Linux 内核中使用如下所示的 tcp_nagle_test 函数测试我们是否应该发送当前的 TCP 数据段


static inline bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,          unsigned int cur_mss, int nonagle){  if (nonagle & TCP_NAGLE_PUSH)    return true;
if (tcp_urg_mode(tp) || (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)) return true;
if (!tcp_nagle_check(skb->len < cur_mss, tp, nonagle)) return true;
return false;}
复制代码


Nagle 算法确实能够在数据包较小时提高网络带宽的利用率并减少 TCP 和 IP 协议头带来的额外开销,但是使用该算法也可能会导致应用层协议多次写入的数据被合并或者拆分发送,当接收方从 TCP 协议栈中读取数据时会发现不相关的数据出现在了同一个数据段中,应用层协议可能没有办法对它们进行拆分和重组。


除了 Nagle 算法之外,TCP 协议栈中还有另一个用于延迟发送数据的选项 TCP_CORK,如果我们开启该选项,那么当发送的数据小于 MSS 时,TCP 协议就会延迟 200ms 发送该数据或者等待缓冲区中的数据超过 MSS5


无论是 TCP_NODELAY 还是 TCP_CORK,它们都会通过延迟发送数据来提高带宽的利用率,它们会对应用层协议写入的数据进行拆分和重组,而这些机制和配置能够出现的最重要原因是 — TCP 协议是基于字节流的协议,其本身没有数据包的概念,不会按照数据包发送数据。

消息边界

如果我们系统性地学习过 TCP 协议以及基于 TCP 的应用层协议设计,那么设计一个能够被 TCP 协议栈任意拆分和组装数据包的应用层协议就不会有什么问题。既然 TCP 协议是基于字节流的,这其实就意味着应用层协议要自己划分消息的边界。


如果我们能在应用层协议中定义消息的边界,那么无论 TCP 协议如何对应用层协议的数据包进程拆分和重组,接收方都能根据协议的规则恢复对应的消息。在应用层协议中,最常见的两种解决方案就是基于长度或者基于终结符(Delimiter)。



图 3 - 实现消息边界的方法


基于长度的实现有两种方式,一种是使用固定长度,所有的应用层消息都使用统一的大小,另一种方式是使用不固定长度,但是需要在应用层协议的协议头中增加表示负载长度的字段,这样接收方才可以从字节流中分离出不同的消息,HTTP 协议的消息边界就是基于长度实现的:


HTTP/1.1 200 OKContent-Type: text/html; charset=UTF-8Content-Length: 138...Connection: close
<html> <head> <title>An Example Page</title> </head> <body> <p>Hello World, this is a very simple HTML document.</p> </body></html>
复制代码


在上述 HTTP 消息中,我们使用 Content-Length 头表示 HTTP 消息的负载大小,当应用层协议解析到足够的字节数后,就能从中分离出完整的 HTTP 消息,无论发送方如何处理对应的数据包,我们都可以遵循这一规则完成 HTTP 消息的重组6


不过 HTTP 协议除了使用基于长度的方式实现边界,也会使用基于终结符的策略,当 HTTP 使用块传输(Chunked Transfer)机制时,HTTPz 头中就不再包含 Content-Length 了,它会使用负载大小为 0 的 HTTP 消息作为终结符表示消息的边界。


当然除了这两种方式之外,我们可以基于特定的规则实现消息的边界,例如:使用 TCP 协议发送 JSON 数据,接收方可以根据接收到的数据是否能够被解析成合法的 JSON 判断消息是否终结。

总结

TCP 协议粘包问题是因为应用层协议开发者的错误设计导致的,他们忽略了 TCP 协议数据传输的核心机制 — 基于字节流,其本身不包含消息、数据包等概念,所有数据的传输都是流式的,需要应用层协议自己设计消息的边界,即消息帧(Message Framing),我们重新回顾一下粘包问题出现的核心原因:


  1. TCP 协议是基于字节流的传输层协议,其中不存在消息和数据包的概念;

  2. 应用层协议没有使用基于长度或者基于终结符的消息边界,导致多个消息的粘连;


网络协议的学习过程非常有趣,不断思考背后的问题能够让我们对定义有更深的认识。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:


  • 基于 UDP 协议的应用层协议应该如何设计?会出现粘包的问题么?

  • 有哪些应用层协议使用基于长度的分帧?又有哪些使用基于终结符的分帧?


本文转载 Draveness 技术网站。


原文链接:https://draveness.me/whys-the-design-tcp-message-frame


2020-03-20 21:29929

评论

发布
暂无评论
发现更多内容

余额宝|三年开发|一二三四+HR面面经,已拿offer|

Java架构师迁哥

升级JDK到1.8笔记

风翱

jdk8 4月日更

不确定的海浪中,更需要数字化转型的定海神针

脑极体

从程序员角度看湖南电信网络全崩,如何防范服务器被攻击以及解决方案

北游学Java

Java 网络安全 网络 服务器

用吃解决生活问题

石云升

读书笔记 好好吃饭 28天写作 4月日更

读scss/sass实例项目带你入门

devpoint

SASS scss css预处理器

树莓派安装pytorch

IT蜗壳-Tango

4月日更

三分钟热度的你,不会得到你想要的结果

小天同学

坚持 日常感悟 4月日更 专心 个人思考

感谢Github帮我斩获了8家大厂Offer

Java架构师迁哥

Java编辑器

ベ布小禅

4月日更

完美的,从不空口说白话,140个案例带你深入理解微服务

Java架构师迁哥

JVM-技术专题-MAT解析OOM问题

洛神灬殇

JVM

一个可递归遍历的Vue树型组件

空城机

JavaScript vue.js 大前端 4月日更

计算机原理学习笔记 Day4

穿过生命散发芬芳

计算机原理 4月日更

「MySQL」深入理解事务的来龙去脉

学Java关注我

Java 编程 架构 程序人生 软件架构

Spark测试用例生成apache iceberg结果

聚变

大数据 iceberg

《分布式系统设计》(1) 从程序思维到系统思维

陈皓07

升级版数字人民币试点在深启动 这次有啥不一样?

CECBC

数字人民币

cat监控http请求-CatFilter

Java个体户

监控 cat

Scrum Patterns:小团队(译)

Bruce Talk

敏捷开发 译文 Agile Scrum Patterns

微信被单删或拉黑?这两个免打扰检测方法你要知道。

彭宏豪95

微信 工具 社交 数据备份 4月日更

真假敏捷教练

escray

面试 面经 4月日更

Coinbase上市在即,这里有你想知道的一切

CECBC

比特币

重读《重构2》- 内联函数

顿晓

重构 4月日更

你看起来很美味?独家揭露视频推荐系统AI秘方

脑极体

《分布式系统设计》(2) 关键概念和基本问题

陈皓07

InheritableThreadLocal源码解析,子线程如何获取父线程的本地变量?

徐同学呀

ThreadLocal Java源码

Linux 下的Zabbix Agent 安装

耳东@Erdong

Linux zabbix 4月日更

《中寰卫星导航项目管理部负责人卜钢:智能网联行业的问题与前景》(采访提纲):

谙忆

颜色值JavaScript换算(HSV、RGB、十六进制颜色码)

空城机

JavaScript 大前端 颜色值换算

Python模拟MOBA手游(三)

Bob

Python Python 游戏编程 4月日更

为什么 TCP 协议有粘包问题_文化 & 方法_Draveness_InfoQ精选文章