写点什么

为什么 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:29883

评论

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

测试策略实践之移动应用的测试策略与测试架构

刘冉

软件测试 软件自动化测试 移动测试

ALB Ingress 发布!轻松应对云原生应用流量管理

阿里巴巴云原生

阿里云 容器 云原生 ALB

今年双 11,阿里业务 100% 上云,云原生有哪些技术亮点?

阿里巴巴云原生

阿里云 技术 云原生 双11

一个基于web服务器的PoW案例(二)

Regan Yue

区块链 11月日更 趣讲区块链

.NET6新东西--Random.Shared

喵叔

11月日更

架构训练营 - 模块 3 作业

焦龙

架构实战营

外包学生管理系统设计文档

波波

「架构实战营」

性能测试之问题与思考

刘冉

软件测试 性能测试 软件自动化测试

软件测试新趋势-2018

刘冉

软件测试 软件自动化测试

CRM User Status profile中Business Transaction字段的用途

汪子熙

CRM SAP 订单 11月日更

在线文本转拼音工具

入门小站

工具

Lua-复合变量

Changing Lin

11月日更

quarkus接触研究个人总结

weir威尔

架构 微服务架构 istio Service Mesh 服务网格 Quarkus

进击的Java(五)

ES_her0

11月日更

【Promise 源码学习】第六篇 - 实现 Promise 的链式调用

Brave

源码 Promise 11月日更

Prometheus HTTP API 查询(七)管理员接口

耳东@Erdong

Prometheus PromQL HTTP API 11月日更

自动化测试之框架分类与思考

刘冉

软件测试 自动化测试

linux检测系统是否被入侵(下)

入门小站

Linux

mongoDB 简单的索引类型

liuzhen007

11月日更

区块链在营销方面的应用探索

石云升

区块链 区块链应用 11月日更

学生管理系统详细架构文档练习

御道而行

架构实战营

【高并发】深度解析线程池中那些重要的顶层接口和抽象类

冰河

Java 并发编程 多线程 高并发 异步编程

架构实战营-模块四

瓜子葫芦侠

「架构实战营」

聊聊开源精神

baiyutang

11月日更

Android C++系列:JNI调用时的异常处理

轻口味

c++ android jni 11月日更

了解一下OpenVINO的API

IT蜗壳-Tango

11月日更

今天参加了“2021全球架构师峰会”...

后台技术汇

11月日更 全球架构师峰会

Vue进阶(幺柒捌):延伸阅读Sass、Scss、Less与Stylus编程特性

No Silver Bullet

CSS Vue 11月日更

#低码系列#低代码来了,程序员会失业吗?

布鲁

低代码开发 低代码平台

【LeetCode】检测大写字母Java题解

Albert

算法 LeetCode 11月日更

如何用20分钟就能获得同款企业级全链路灰度能力?

阿里巴巴云原生

阿里云 微服务 云原生 MSE

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