【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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

评论

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

仅用3年!青软集团跃升华为云教育类目伙伴TOP2

科技怪咖

ARMS实践|日志在可观测场景下的应用

阿里巴巴中间件

阿里云 云原生 可观测

皮皮APP夏日防溺水公益讲座 联动武汉长江救援队筑建生命安全线

联营汇聚

自动化测试如何解决日志问题

老张

自动化测试 日志处理

【数据结构实践】从0到1带你利用Python实现自定义集合

迷彩

数据结构 集合运算 8月月更 自定义集合

从事DevOps工作应该掌握哪些语言及工具

穿过生命散发芬芳

DevOps 8月月更

腾讯云大数据平台 TBDS全面升级,加速构建安全可控的大数据生态

科技热闻

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

岛上码农

flutter ios 前端 安卓开发 8月月更

直播预告 | PolarDB-X 动手实践系列—— PolarDB-X on OSS 冷热数据分离存储

阿里云数据库开源

数据库 阿里云 开源 分布式 PolarDB-X

美团二面被pass,肝完这份1213 页 的算法刷题神册成功拿到字节offer

了不起的程序猿

Java 字节跳动 算法 java程序员 java编程

活动预告(29日)|诚邀您参与AWS & 观测云「可观测性体验日」

观测云

风险组件已经升级到最新版本,仍然提示风险,如何快速解决——kaptcha 安全漏洞

墨菲安全

Kaptcha 漏洞修复 开源安全 漏洞检测 开源安全与治理

基于STM32设计的拼图小游戏

DS小龙哥

8月月更

老板问我要ROI,我让他先挑宽门or窄门

科技怪咖

新元联手倍市得,以数字化手段实现人才公租房项目满意度持续监测

科技怪咖

看准六点,帮你选对客户体验管理(CEM)系统

科技怪咖

一加和OPPO是什么关系?我来揭秘

Geek_8a195c

购物中心的运营保障,数衍科技数据桥接服务系统升级

科技怪咖

C/CPP中int和string的互相转换详解与多解例题分析

CtrlX

c c++ 后端 数据类型 8月月更

Python自学教程5-字符串有哪些常用操作

和牛

Python 测试 8月月更

数衍科技与超市发达成合作,共同探索数字小票的新应用

科技怪咖

[CSS入门到进阶] 外国前端开发者说的 Intrinsic Ratios in css 是什么意思?

HullQin

CSS JavaScript html 前端 8月月更

defi质押dapp智能合约系统开发代码逻辑

开发微hkkf5566

开源一夏 |分布式事务--TCC解决方案

六月的雨在InfoQ

开源 分布式事务 TCC 最终一致性 8月月更

云途加油站 | 一文读懂 Dynatrace 与Amazon Lambda 的“双剑合璧心法”

亚马逊云科技 (Amazon Web Services)

数据库 Serverless Lambda

Flu tter开发小技巧

坚果

开源 8月月更

分布式雪花算法

源字节1号

前端开发 后端开发

合成资产赛道风云突变,Linear Finance有望成为最具潜力的黑马

鳄鱼视界

数据点按时间间隔以及数据值分割数据块

waitmoon

算法 SLO

每日一R「14」错误处理

Samson

学习笔记 8月月更 ​Rust

OceanBase 4.0:当我们谈单机分布式一体化架构时,我们在说什么?

OceanBase 数据库

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