ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

计算机网络篇15:传输层——TCP 报头:4 位首部长度切出可变报头,确认应答与序号撑起可靠性

计算机网络篇15:传输层——TCP 报头:4 位首部长度切出可变报头,确认应答与序号撑起可靠性


📚 本文收录于「流浪」的系列专栏

🐧Linux系统⚙️C++
📊数据结构与算法🐍Python
🔗LangChain & LangGraph🗄️MySQL 数据库
🌿Git 工具🌐计算机网络
🤖LLM💯大厂面试、八股
📚学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前言:《计算机网络篇14》把 UDP 的 8 字节定长报头拆完,TCP 的报头是另一副样子:20 字节起步,后面还能挂可变选项。本篇先拆 4 位首部长度怎么把报头和有效载荷分开、TCP 为什么没有长度字段,再顺着可靠性往下走:确认应答为什么不必再被应答、并发发送怎么逼出序号、全双工又为什么要求两个独立的序号字段。


一、TCP 报头与有效载荷的分离

1.1 20 字节固定报头加可变选项,16 位端口号负责交付

1. TCP 报头由固定部分加选项构成
  1. TCP 报头是「20 字节固定长度 + 选项」的组合:固定部分每条报文都有、格式永远不变,选项部分可长可短、可有可无
  2. 这和 《计算机网络篇14》里 UDP 那种 8 字节一次定死的报头不是一种设计——UDP 把长度写死在协议里,TCP 把长度做成了可调的
  3. 代价随之而来:UDP 靠定长就能切出报头和载荷,TCP 必须自己回答一个问题,这条报文的报头到底到哪里结束
2. 交付由 16 位目的端口号完成
  1. 报文跨过网络到达主机后,传输层从报头里取出 16 位目的端口号,把有效载荷交到对应进程的手上,这套两级查表在 《计算机网络篇3》里走过完整的流程
  2. 「交给谁」由端口号解决,「从哪开始算载荷」就是另一个问题了——报头和有效载荷怎么分离,得有一个字段来管

端口号解决交付,首部长度解决分离;TCP 报头里每个字段背后都压着这样一个具体的问题。

1.2 4 位首部长度撑起最多 60 字节的可变报头

1. 4 位字段的天然上限是 15 个单位
  1. 负责分离的字段叫 4 位首部长度,在 RFC 里叫数据偏移(Data Offset),只有 4 个比特,取值范围 [0000, 1111],也就是 [0, 15]
  2. 如果按 1 字节为单位理解,它能声明的报头长度最多 15 字节——可 TCP 光固定部分就有 20 字节,[0, 15] 连下限都盖不住
  3. 4 个比特撑不起 20 字节起步的报头,这不是字段设计失误,而是基本单位还没定对
2. 协议把基本单位定成 4 字节
  1. TCP 规定:发送方和接收方在计算首部长度时,以 4 字节为基本单位,字段里的数值是多少,报头就占多少个 4 字节
  2. 换算回字节,4 位的上限 15 对应 15×4 = 60 字节,报头长度范围一下子扩成了 [0, 60]
  3. 固定的 20 字节是每条报文必有的部分,折成单位是 20÷4 = 5,所以字段实际取值 [5, 15],报头实际长度范围 [20, 60]
3. 选项长度从首部长度里倒推出来
  1. 报头总长减去固定 20 字节,剩下的就是选项的空间,选项长度范围 0 到 40 字节
  2. 选项是 TCP 给自己留的扩展和协商参数的余地,具体哪个选项对应哪个机制,讲到对应机制时自然会碰到

报头能伸能缩的范围全在一个 4 位字段里声明完:单位放大到 4 字节,15 个单位封顶 60,5 个单位打底 20。

1.3 分离的判定:按首部长度读满为止

1. 首部长度之前是报头,之后全是载荷
  1. 接收方拿到报文,按首部长度字段读满对应字节数,读掉的这部分按报头格式逐一解释,剩下的从头到尾都是有效载荷
  2. 选项部分因为同样被首部长度覆盖,解析时不会和载荷混淆——不管选项带了几个字节,都在「读满为止」的那一段里面
  3. UDP 靠定长免掉了这个字段,TCP 靠字段补上了可变长,两条路通向同一个目的:让接收方不用猜

定长把分离规则写进协议,可变长把分离规则写进字段;TCP 选了后一条路,换来的是报头可以按需伸缩。


二、TCP 报头没有长度字段,字节流没有边界

2.1 UDP 的长度字段在 TCP 报头里找不到对应物

1. UDP 用 16 位长度记录整条报文
  1. 《计算机网络篇14》里拆过:UDP 报头的 16 位长度字段记录的是整条数据报的长度,首部加数据一把全算
  2. 有这个字段在,OS 处理 UDP 时能明确区分每条报文的边界——整条多长,读满就完
2. TCP 报头里没有 TCP 长度
  1. 同样的字段在 TCP 报头里找不到,TCP 没有「TCP 长度」这个东西
  2. 也就是说,仅凭 TCP 报文自身,OS 说不出这条报文的数据部分到底有多长

2.2 面向字节流:OS 不能真实区分每次传输的长度

1. TCP 眼里只有一串连续字节
  1. TCP 是面向字节流的协议,交付的单位不是「一条条报文」,而是一串没有边界的字节——《计算机网络篇8》讲报文边界时正面撞过这件事
  2. 应用层一次 write 交下来的数据,TCP 可能拆成几段发出去,也可能攒着和别的数据合着发,每一段和用户的一次写入之间不再一一对应
  3. 所以 OS 不能真实区分「每一次 TCP 传输」的长度——不是它懒得记,是这串字节本来就没有「每一次」这个划分
2. 两个协议对长度的态度对照
维度UDPTCP
报头长度8 字节定长20 字节起步,[20, 60] 可变
长度字段16 位长度记整条报文没有对应字段
边界归属协议层保留报文边界边界交回应用层自己切
OS 区分边界能不能,只有字节流

UDP 的长度字段保住的是报文边界,TCP 没这个字段,是因为它压根不打算保留边界——字节流和长度字段,一个存在就宣判了另一个不存在。


三、确认应答机制:应答本身不需要再被应答

3.1 C 发消息、S 作应答,以及「应答谁来确认」

1. C 发消息、S 作应答,这是最粗略的通信样貌
  1. 站在 C 端和 S 端来讲:C 发消息给 S,S 收到后给 C 一个应答——这一来一回,就是确认应答(Acknowledgment)机制最粗略的样子
  2. S 给出应答这个动作,代表 S 确实收到了 C 的消息,这一层没有歧义
2. 但 S 不确定自己的应答 C 有没有收到
  1. 应答也是一条报文,同样可能丢、可能没送到——S 发出应答之后,并不知道 C 收没收到它
  2. 那要不要让 C 再补一条,告诉 S「你的应答我收到了」?
3. 若继续确认下去,循环停不下来
  1. 让 C 补一条「应答已收到」,这条新报文立刻变成新的待确认消息,轮到 S 来确认它收到没有
    • S 补一条「确认已收到」,这条又轮到 C 来确认……每补一层,刚发出去的那层就又变成待确认的
  2. 确认链永远差一层才能收尾,谁也说不清哪一条才算最后一句,通信没有终点
  3. 这不是实现上的取舍问题,是规则本身的结构问题——只要「每条消息都必须被确认」这个规则在,循环就必然出现

3.2 裁决:不对应用答作应答

1. TCP 把整个循环裁掉
  1. 确认应答机制只做一层:收到消息就应答,但对「应答」本身不再作应答
  2. 两个方向同理——S 给 C 发消息时 C 作应答,S 不对 C 的这条应答再作应答;谁收到数据谁应答,应答本身免检
2. 免检不产生漏洞的原因
  1. C 收到 S 的应答,这件事本身就说明:C 之前发给 S 的消息,S 已经收到并且处理过了——确认消息被收到,是捎在「C 继续往下通信」这个动作里的
  2. 换句话说,应答到达的那一刻,它要确认的事已经成立;给应答再配一个应答,只是把已经成立的事实重复一遍
  3. 停止确认循环,可靠性一分没少,报文的数量却不用无限叠加

应答不需要被应答,是因为应答到达时它所确认的事实已经成立;确认链在一层处截断,是确认应答机制能落地的前提。

3.3 可靠性的边界:历史可靠,最新悬空

1. 有应答保证的是历史消息
  1. 已经拿到应答的那些消息,可靠性是有保证的——应答就是接收方亲口承认「收到了」
  2. 通信持续进行,绝大部分消息都会沉淀成历史消息,被一个个应答逐条锁定
2. 最新报文永远没有应答
  1. 通信中最新发出的那条报文,永远处在「还没有应答」的状态——它的应答要么在路上,要么还没发出
  2. 所以严格说,确认应答机制保证的从来不是「所有消息都可靠」,而是「除了最新的那条,其余全部可靠」——最新消息的可靠性无法保证,这是这套机制的天然边界

确认应答的可靠性画到最新报文为止:历史消息被逐条锁定,最新一条永远悬在「等确认」的位置上。


四、从串行等待到并发发送

4.1 收到应答再发下一条,效率太慢

1. 串行的速度被往返时间锁死
  1. 「C 发一条,等 S 应答,收到应答再发下一条」——这条规则看起来稳妥,每条消息都在上一条被确认之后才出发
  2. 代价是速度:每发一条都要完整等一个网络往返,网络越远等待越长,大量时间花在「等」而不是「传」上
  3. 效率太慢,这条路线注定要被换掉

4.2 通用的流程:并发地发送与应答

1. 两种方案放在一起对照
  1. 串行停等:发一条停一条,每条都等确认落地再发下一条,稳当直观,但速度被一个又一个往返时间锁死
  2. 并发流水:C 不必等 S 应答之后再发送,而是并发地发送与应答——应答在回来的路上,新消息已经在去路上,两条线互不等待,这是更通用的流程
2. 快是有代价的,新问题跟着并发进来
  1. 并发换来了吞吐量,也把「丢包」从一个小麻烦升级成了必须正面回答的问题
  2. 发送端从此要面对一个串行时代不存在的疑问

4.3 新问题:丢的是哪一个

1. 应答比报文少一个,缺口对应不上
  1. C 在并发发送过程中如果丢了 1 个报文,他收到的应答数量肯定比发出的报文少一个
  2. 但「少一个」只能说明有丢,说明不了丢的是哪个——发出去的报文对 C 来说长得都一样,没有任何标记能把它们区分开
  3. 不知道丢的是哪个,就没法定向补救;要定位,得先让每个报文有自己的编号

并发把「有没有丢」变成了容易回答的问题,把「丢的是哪个」变成了新问题——这个新问题,只能靠编号来解决。


五、32 位序号与 32 位确认序号

5.1 请求带序号,应答带确认序号

1. 两个 32 位字段配对出现
  1. TCP 报头里用 32 位序号和 32 位确认序号来确保可靠性:发送方发消息时,在自己报头里填上序号;接收方应答时,在自己报头里填上确认序号
  2. 通俗口径下,确认序号就是收到的序号加一——S 收到 C 序号为 x 的报文,应答时把确认序号填成 x+1
  3. 序号和确认序号的关系落在报头上:收发双方之间就是靠报头的这两个字段一来一回地交互,机制没有任何额外通道
2. 真实 TCP 的序号按字节编号

  1. 上面按「报文个数」编号,是为了立直觉的简化模型
  2. 真实 TCP 的序号计的是字节位置:字节流逐字节编号,报文里填的是首字节的编号,确认序号是期望收到的下一个字节的序号——这和 《计算机网络篇8》里「TCP 把数据当成一串无边界字节」是同一件事的另一面
  3. 编号口径变了,机制没有变:序号标位置、确认序号标「期望的下一个位置」,下面仍按简化模型讨论

5.2 确认序号的语义:之前的全部收到,即累积确认

1. 确认序号代表它之前的所有信息全部收到
  1. 确认序号的语义是「代表指定序号之前的所有信息已经全部收到」——它确认的不是某一条报文,而是一个截止位置:到这个序号为止,前面一串全部落袋
  2. 这种「一个确认覆盖一串」的机制,标准名字叫累积确认(Cumulative Acknowledgment)
  3. 接收方不需要对每条报文单独回一个确认,只要把「我已经连续收到了哪里」报出去,发送方就能推算出前面全部的接收情况
2. 累积确认的容错:允许少量丢包
  1. 「之前全部收到」这种写法带来实实在在的容错好处,可以总结成一句话:允许少量丢包
    • 某一条应答报文丢了,不影响结论——后面更大的确认序号一到,它覆盖的范围自然把丢掉那条应答要说的话补了回来,应答报文本身允许丢
    • 确认的粒度是「位置」而不是「条数」,一个确认顶一串,不必逐条应答,确认的开销也省了下来
  2. 应答可以丢、确认可以省,靠的都是「确认序号自带截止位置」这一个语义
3. 乱序问题也由序号接管
  1. 既然是并发发送,报文到达就有先后之分——先发的不一定先到,后发的可能抢先落地
  2. 接收方拿到报文后可以根据序号重新排序,把先到的后发的放回它该在的位置,乱序问题就此解决——这是序号在「定位丢包」之外的另一个好处
  3. 同一个 32 位序号同时干了三件事:定位丢包、排序乱序、承载确认,可靠性的地基就是它垫的

序号给每个位置一个标记,确认序号报出「连续收到了哪里」;累积确认的容错和乱序的排序,全部建立在这两个位置标记上。


六、全双工通信要求两个独立的序号

6.1 一个序号为什么不够用

1. 单向通信的错觉
  1. 确认序号既然就是序号加一,看起来完全可以互相推算,一个序号字段似乎就够用了
  2. 这个判断成立的前提,是前面讨论默认的视角:单向通信——C 只发、S 只答,一条道走到底
  3. 一旦换成双向同时通信,这个前提就塌了

6.2 全双工:双方既发信息又作应答

1. TCP 是全双工通信
  1. TCP 里是全双工(Full Duplex)通信,双方可以互相通信:不仅有应答,还有信息——C 和 S 随时可能各自作为发送方往外发数据
  2. 每个方向上都有自己的「发送」要编号、自己的「接收」要确认,两个方向各自独立计数,谁也不能替谁记账
2. 应答搭上反向数据的便车:捎带应答
  1. 全双工带来的一个直接机制是捎带应答(Piggybacking):反向要发数据时,应答不必单独占一条报文,直接填在自己数据报文的确认序号里捎过去
  2. 应答被捎在信息里一起走,「不仅有应答,还有信息」说的就是这种一报两用的形态
3. 双向流程把两个字段各归其位
  1. C 给 S 发信息,C 在报头里填序号
  2. S 向 C 作确认,S 在报头里填确认序号
  3. S 给 C 发信息,S 在报头里填序号——此刻 S 是发送方,用的是自己方向上的序号
  4. C 向 S 作确认,C 在报头里填确认序号

每个方向都完整地需要「序号 + 确认序号」这对字段,方向之间互不共用——两个 32 位字段不是同一个数的两种写法,而是两条方向各自的账本。

序号和确认序号不是「一个数和它的加一」,是两个方向各自的账本;全双工一开,两个独立字段就从可有可无变成了必须。


七、全篇总结

1. 4 位首部长度切出可变报头
  1. TCP 报头是 20 字节固定加选项,以 4 字节为单位,字段取值 [5, 15],报头范围 [20, 60]
  2. 按首部长度读满为止,其后全是有效载荷
2. TCP 没有长度字段,字节流没有边界
  1. UDP 靠 16 位长度字段保住报文边界,TCP 报头里没有对应字段
  2. 面向字节流,OS 不能真实区分每一次传输的长度
3. 确认应答只做一层,不对应答作应答
  1. C 发消息、S 作应答,但应答本身免检
  2. 应答到达时它确认的事实已经成立,确认链在一层处截断
4. 可靠性的边界在最新报文
  1. 有应答锁定的是历史消息
  2. 最新报文永远没有应答,可靠性悬空
5. 并发发送引出「丢哪个」的问题
  1. 串行等应答效率太慢,通用的流程是并发地发送与应答
  2. 应答比报文少一个只说明有丢,定位丢的是哪个要靠序号
6. 序号与确认序号垫起可靠性
  1. 确认序号代表之前全部收到,即累积确认,允许少量丢包
  2. 乱序按序号排序;全双工下两个方向独立计数,必须有独立的两个 32 位字段

八、文末面试题

8.1 推导题(按本篇知识点,附答案)

  1. 【推导】TCP 报头最少 20 字节,一个 4 位的字段怎么把它标出来?实际取值范围怎么算?
    答:关键在基本单位——协议规定首部长度以 4 字节为单位计算,4 位二进制最大是 15,对应 15×4 = 60 字节的上限;固定 20 字节折合 5 个单位是下限,所以字段实际取值 [5, 15],报头实际长度 [20, 60],选项部分最大 40 字节,4 个比特靠放大单位盖住了 20 字节起步的可变报头。

  2. 【推导】S 给 C 的应答没有任何人确认,这条「免检」为什么不算漏洞?
    答:因为应答到达的那一刻,它要确认的事实已经成立——C 收到应答,说明 C 之前发的消息 S 已经收到;如果再给应答配应答,确认链会一层层无限叠加,且每一层都不增加任何新的可靠性,所以 TCP 裁掉循环、只确认一层,可靠性一分没少。

  3. 【推导】既然确认序号等于序号加一,为什么序号和确认序号要做成两个独立的 32 位字段?
    答:因为 TCP 是全双工——两个方向都各自要发信息和作确认,每个方向都有自己的序号要填、自己的确认要回;加上应答可以捎在反向数据的报文里一起发,一条报文要同时装下「我的序号」和「对你的确认」,一个字段装不下两本独立的账。

8.2 真题(来源已核实,转述注明)

  1. TCP 的序列号有什么作用?
    答:标识发送出去的每一个数据的位置,接收方据此把乱序到达的报文按序拼回、把重复到达的报文丢弃,同时确认报文里的确认序号也建立在它之上,让发送方知道哪些数据已经被正常接收——序号是 TCP 解决乱序、重复和确认这三件事的共同基础。
    【真题·转述自 小林coding《图解网络:TCP 基本认识》】

  2. TCP 的确认应答号是什么意思?解决什么问题?
    答:指下一次期望收到的数据的序列号,发送端收到这个确认以后,可以认为在这个序号之前的数据都已经被正常接收;它解决的是丢包问题——发送方对比「发了什么」和「被确认到了哪里」,就能定位出缺失的部分。
    【真题·转述自 小林coding《图解网络:TCP 基本认识》】

  3. 为什么 UDP 头部有「包长度」字段,而 TCP 头部没有?
    答:TCP 的数据长度可以用 IP 总长度减去 IP 首部长度再减去 TCP 首部长度推出来,报头里不需要重复记录;UDP 保留这个字段,一方面补齐首部到 4 字节的整数倍,另一方面配合 8 字节定长首部直接给出整条数据报的边界。
    【真题·转述自 小林coding《图解网络:TCP 基本认识》】

  4. 什么是累积确认?
    答:TCP 的确认只确认到第一个丢失位置为止——接收方回的确认号表示「期望收到的下一个序号」,等于一次性承认了它之前的所有数据都已收到;比如收到包 1、2、3 之后回一个确认号 4,发送方就知道前三个全部到位,必要时只处理包 4,一个确认覆盖一串报文,而不必逐包确认。
    【真题·转述自 牛客网《常见 C++ 面试题及基本知识点总结(二)》】

  5. TCP 是如何保证可靠传输的?
    答:核心是序列号加确认应答——每个数据都有编号,接收方按序号排序乱序、丢弃重复,并通过确认号告诉发送方已经连续收到哪里;没被确认的数据由重传机制补发,再配合流量控制和拥塞控制调节发送节奏,多套机制叠加,在不可靠的 IP 之上拼出可靠传输。
    【真题·转述自 牛客网《面试官:TCP 是如何保证可靠传输的》】

  6. 为什么客户端和服务端的初始序列号 ISN 是随机生成且互不相同的?
    答:因为网络中的报文会延迟、会丢失、也可能被复制重发,如果不同连接的初始序列号固定且相同,旧连接迟到、重复的报文就可能落进新连接的序号范围里被当成合法数据;随机且不同的 ISN 把不同连接的序号空间错开,减小旧报文干扰新连接的概率。
    【真题·转述自 小林coding《图解网络:TCP 连接建立》】


TCP 的可靠不是承诺出来的,是算出来的:序号给每个位置一个标记,确认序号报出一条截止线,应答免检把确认链截在一层。可靠性从「尽力而为」变成「有账可查」,靠的就是报头里这几笔账。

返回列表