你在使用哪种编程语言?快来投票,亲手选出你心目中的编程语言之王 了解详情
写点什么

为什么 TCP 协议有粘包问题

2020 年 3 月 20 日

为什么 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 年 3 月 20 日 21:29253

评论

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

微服务架构下的核心话题 (一):微服务架构下各类项目的顺势崛起

xcbeyond

架构 微服务

架构师训练营 - 第九周 - 作业

韩挺

Atlassian 重磅推出12个新功能为您打造全新 DevOps 体验!

Atlassian

项目管理 DevOps Atlassian Jira

秒杀系统的架构设计

莫莫大人

【第九周作业】

Aldaron

面经手册 · 第2篇《数据结构,HashCode为什么使用31作为乘数?》

小傅哥

Java 数据结构 小傅哥 面试官

架构师训练营第九章作业

叮叮董董

Elasticsearch从入门到放弃:瞎说Mapping

Jackey

elasticsearch

HomeWork

天之彼方

训练一个数据不够多的数据集是什么体验?

华为云开发者社区

数据 数据集 华为云 标签 modelarts

秒杀系统的挑战和应对方案

2流程序员

秒杀系统设计初稿

jason

JVM学习总结

jason

尚未到来的远程工作

ThoughtWorks洞见

敏捷 敏捷开发 软件开发 远程办公 thoughtworks

我还在生产玩 JDK7,JDK 15 却要来了!|新特性尝鲜

楼下小黑哥

Java jdk

LeetCode题解:189. 旋转数组,3次翻转,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

当面试官问我ArrayList和LinkedList哪个更占空间时,我这么答让他眼前一亮

鄙人薛某

Java 面试 集合 面试题 java基础

《RabbitMQ》如何保证消息的可靠性

Java旅途

SpreadJS 纯前端表格控件应用案例:SPDQD 质量数据云

Geek_Willie

SpreadJS 案例

架构师训练营第九章总结

叮叮董董

并发-草稿

superman

NOSQL or NEWSQL

大唐小生

sql nosql

第九周总结

andy

树莓派上安装docker记录

田振宇

Ubuntu启动盘无法格式化

kraken0

第9周总结+作业

林毋梦

JVM系列之:从汇编角度分析Volatile

程序那些事

Java JVM JIT 汇编

一周信创舆情观察(7.27~8.2)

统小信uos

关于微信电子发票生态,这三种服务商最有机会

诸葛小猿

电子发票 发票

计算机网络基础(十四)---传输层-UDP协议详解

书旅

计算机网络 网络 协议栈 通信协议

Docker-compose实战

北漂码农有话说

围绕“三个问题”开展的网易云音乐数据基础建设

围绕“三个问题”开展的网易云音乐数据基础建设

为什么 TCP 协议有粘包问题-InfoQ