Linux 网络基础之传输层TCP(六)TCP报头格式,TCP可靠性,序号/确认序号,窗口大,标志位,初识三次握手四次挥手

目录

一、认识TCP协议

[二、TCP 协议的报文格式](#二、TCP 协议的报文格式)

4位首部长度

[1. 报头与有效载荷的分离问题](#1. 报头与有效载荷的分离问题)

[2. 分用问题? ---目的端口号](#2. 分用问题? ---目的端口号)

[三、TCP 的可靠性](#三、TCP 的可靠性)

确认应答机制

丢包

初步认识重传和去重

四、TCP通信的两种模式

序号

确认序号

捎带应答机制

五、流量控制

16位窗口大小

[六、6 个标志位](#六、6 个标志位)

ACK

初识三次握手

为什么要进行三次握手?

SYN​

FIN ​

初识四次挥手

为什么要是四次挥手?

RST

PSH:

[URG :](#URG :)

紧急指针

为什么会有紧急指针的存在?

七、总结


前面我们学习了传输层 UDP 以及传输层以上层的相关内容,本篇文章我们学习传输层的 TCP 传输控制协议。

一、认识TCP协议

TCP 全称传输控制协议,是互联网中应用最广泛、最核心的传输层协议。通信双方在正式传输数据前,会先建立专属可靠连接,全程按照统一规则有序收发数据,保障报文不丢失、不错乱、不重复、按顺序完整交付。整个TCP通信逻辑,本质都是围绕双方约定好的报文规则、序号确认机制、连接管理、流量管控、拥塞控制等一系列传输约定有序执行。

二、TCP 协议的报文格式

想要弄清 TCP 的工作原理,首先就要理解 TCP 报文结构,也就是 TCP 协议的报文格式。

上图就是 TCP 完整的报文结构 :一份 TCP 报文由TCP 报头 + 上层有效数据载荷两部分共同组成。

TCP 固定报头长度为 20 字节 ,后续还可以按需追加可选扩展字段,因此整体首部长度是可变的 。TCP 报头在底层实现上本质就是一个固定格式的结构体,和 UDP 报文首部特性一致,二者都是严格大端网络字节序存储,通信双方遵循统一的字段排布规则,因此在传输解析过程中,都不存在字节序错乱带来的反序列化和系列化问题

这个 struct tcphdr 是 TCP 报头结构体,对应 TCP 报文里的报头部分,不包含后面的应用层数据载荷。从定义上看,它完整还原了 TCP 报头的所有固定字段:source/dest 对应源 / 目的端口号,seq/ack_seq 对应序号和确认序号,后续的位域定义则对应报头里的标志位、首部长度等字段,window/check/urg_ptr 也分别对应窗口大小、校验和、紧急指针等等。

4位首部长度

这里的 4 位首部长度,指的就是 TCP 报头中用4 个比特位来存储这个字段。4 个比特位的取值范围是 0~15,但它表示的长度单位不是 1 个字节,而是 4 个字节。

举个例子,我们可以这样理解它的计算逻辑:

TCP 报头的长度不是固定的,基础部分是 20 字节,加上可选的扩展选项后,最长可以到 60 字节。 4 位首部长度字段就是用来标记这个总长度的。计算时,我们需要用4 位首部长度的值乘以单位长度:字段值 × 4字节。因为 4 位能表示的最大值是 15,所以 TCP 报头的最大长度是 15 × 4 = 60 字节;而最小值则是 5 × 4 = 20字节,对应没有任何选项的基础报头。比如当报头总长度是 40 字节时,我们反推计算:40 ÷ 4 = 10,那么这个 4 位首部长度字段里就会被写入数值 10,接收方拿到这个 10 后,乘以 4 字节,就能知道报头结束的位置,从而分离出后面的有效载荷。没有任何扩展选项时,TCP 报头为固定 20 字节,该字段填充数值 5。接收方只需要读取该字段数值并乘以 4 字节,就能精准定位 TCP 报头的结束位置,顺利完成报头与后续有效载荷的数据分离。

1. 报头与有效载荷的分离问题

TCP 依靠4 位首部长度字段,精准完成报头与有效载荷的拆分分离。这个字段会以 4 字节为单位,标记出整个 TCP 报头一共占用多少字节。接收方拿到报文后,直接读取首部长度的值,就能精准算出报头结束的位置,前面所有字节都是 TCP 头部信息,剩下后续所有字节,就是上层应用层的有效数据载荷。

同时 TCP 报头不携带整包报文总长度字段,这和 UDP 有本质区别。UDP 是面向数据报协议,自带总长度字段,用总长度减去固定 8 字节报头,就能直接算出载荷大小;而 TCP 是面向字节流协议,本身没有报文边界概念,不按一个个完整数据包区分数据,因此不需要总长度字段,只靠首部长度分割头部与数据即可。

2. 分用问题? ---目的端口号

TCP 的分用就是依靠目的端口号来实现的。操作系统收到 TCP 报文后,会解析报文首部里的目的端口,根据端口号找到当前主机上对应的 TCP 应用进程,把字节流数据精准交付给绑定了这个端口的程序。

三、TCP 的可靠性

在讲 TCP 报头的剩余字段前,我们先来认识并熟悉 TCP 的可靠性。

TCP 是传输层面向连接的可靠传输协议,可靠性也是它最核心的标志性特点之一。

TCP 的传输可靠性是什么意思?我们用两台通信主机 A、B 举例来理解 :

主机 A 向主机 B 发送报文后,想要确认对方成功接收,就需要 B 在收到报文后,向 A 回复对应的应答报文,只有 A 成功收到这份应答,才能百分百确认自己最开始发出的报文已经被 B 正常接收。顺着这个逻辑往下走,A 为了让 B 知道自己已经收到了 B 的应答报文,就又需要再对 B 的应答报文进行二次应答,以此类推,这个应答流程就会无穷无尽的循环下去,结果就是互联网里永远会有最新发出的报文,永远无法收到对应的收尾应答。所以严格来说,互联网环境里不存在绝对 100% 可靠的协议,最新发送的报文永远没办法获得最新的应答

而 TCP 定义的可靠性,不是体现在对发出的所有报文都进行应答,而是对历史报文进行可靠性应答只要对方针对某一段历史报文回复了应答确认,发送方就可以百分百确定这段历史数据已经被对方完整接收。

而 TCP 的可靠性核心,就是对所有已发出的历史报文,都能收到对应的应答报文,以此保障历史传输的数据百分百被对方接收确认。

确认应答机制

确认应答核心逻辑:

上面的死循环问题本身是无法彻底解决的,但仔细思考 TCP 只需要保证第一个携带有效数据载荷的报文 被对方准确接收确认即可,也就是只需要验证对含有有效数据载荷的报文的应答报文的可靠性即可,不需要死循环地去验证应答报文本身是否可靠。因为报文传输可靠性≠ 应答报文自身可靠性。并且整个确认应答的流程都是由 TCP 协议内核自动完成,不需要上层应用程序干预。

TCP 全双工可靠性特性:

上面我们说的只是 A 主机向 B 主机发送数据的应答逻辑,而 B 主机向 A 主机发送数据时也是同理。因为 TCP 是典型的全双工通信协议,通信双方地位完全对等,两端可以同时独立发送数据、同时进行应答确认,互不干扰。

网络对可靠性的限制:

这套确认应答机制,只侧重于 "接收方收到数据后,发送方能明确知晓"。

但真正的数据能否成功送达,根源还是由当下的网络质量决定,如果网络拥堵,哪怕发送方发送了再多的报文,也无法完成可靠传输。

丢包

TCP 确认应答机制,体现在两个方面 : 一个是接收方成功收到数据报文后,会对发送方进行应答回复,让发送方知晓数据已经正常送达;另一个方面是如果接收方没有收到报文,发送方也必须能及时感知到异常情况。

而发送方收不到接收方的应答,本质分为两种完全不同的情况:

  1. 一种是发送方自己发出的数据报文在网络中直接丢失,
  2. 另一种是接收方正常收到了数据、并回复了应答报文,但应答报文在半路丢失了。

这两种故障,最终都会让发送主机 A 无法收到对应的确认应答。

发送方怎么判断自己能不能等到对方的应答?

TCP 依靠超时截止时间 (deadline) 来统一界定这个边界。我们可以设定一个固定的等待时长,比如 5 毫秒,只要发送方在规定的 5 毫秒内没有收到接收方回复的应答报文,就会认为本次传输出现了丢包。

因为站在发送方的视角,永远没办法精准区分,到底是原始数据报文丢了,还是对方回复的应答报文丢了。所以 TCP 里的丢包,其实是协议规则人为规定出来的结果,并不是百分百确定真实发生了报文丢失。超时未收到应答,只是统一判定为丢包的标准,因为永远无法确切知道到底是哪一段报文出现了丢失,只能按照超时规则,默认判定传输失败并进行后续重传处理。

初步认识重传和去重

在了解了上面的内容后,我们才能真正理解 TCP 的可靠性。当发送方主机 A 迟迟没有收到对方的应答报文时,主机 A 本身无法百分百确定主机 B 有没有成功收到自己发送的数据报文。这时就分为两种情况:第一种是 A 发给 B 的原始数据报文在网络传输途中直接丢失了;第二种是 B 已经正常收到了数据报文,但 B 回复给 A 的确认应答报文在半路丢失。这两种完全不同的故障场景,最终都会让 A 收不到对应的应答,而 TCP 协议会将这两种情况都判定为丢包,随即触发报文重传操作。

如果是原始数据报文丢失导致的异丢包常,那么 A 重传报文后,B 就能正常接收这份补发的数据。但如果是 B 回复的应答报文丢失导致的丢包异常,B 其实早就收到过原始数据报文,再次收到重传的报文后,就会出现重复接收数据报文的问题,既然重复接收,那就得去重。而 TCP 报头里的序号 字段,就是用来解决这种重复报文去重问题的核心,依靠序号就能精准识别重复数据,从而进行去重。

因此重传的本质就是没有收到报文后的补救措施。理解 TCP 可靠性的核心就是理解确认应答机制的核心:数据成功送达、发送方能知道;数据发送失败、发送方也能及时知道,这两种结果都清晰可控,才是 TCP 可靠传输的真正含义。


四、TCP通信的两种模式

在理清 TCP 可靠性原理之后,我们再来了解 TCP 的两种通信传输模式。因为 TCP 本身是全双工通信协议,通信双方地位对等,两端可以同时收发数据、互不干扰,这和 HTTP 这类应用层通信有着本质区别。HTTP 里客户端主动发起请求、服务端被动响应处理,双方通信角色不对称、流程固定单向;而 TCP 双向传输逻辑平等,没有主动端与被动端的严格区分。

**第一种是基础串行应答通信模式:**一方发送数据报文后,必须等待对方回复对应确认应答,成功收到回执之后,才能继续发送下一份报文。反向通信也遵循一样的规则,你来我往依次交互。但这种串行等待应答的方式传输效率极低,同一时间只能单向传递一份数据,大量时间都浪费在等待应答的空耗上,并不会在实际 TCP 通信中使用。

**第二种是 TCP 真实使用的批量连续发送模式,**也是高效并行传输方案。发送方主机 A 可以一次性连续向主机 B 发送多份数据报文,不需要每发一个就等待一次应答。接收方 B 收到批量报文后,统一进行确认和批量回复应答即可。原本单报文串行等待应答的空闲时间被充分利用,传输从串行排队变成了并行批量处理,极大压缩了整体等待耗时,大幅提升了数据的传输效率。


序号

有了上面的知识背景后,我们正式讲解 TCP 报头里两个核心关键字段:32 位序号32 位确认序号

我们以第二种批量连续发送报文模式为例 : 主机 A 一次性向主机 B 发送多份数据报文时,A 发出报文的先后顺序,和 B 最终收到报文的顺序,并不是完全一致的。可能会受网络路由转发、链路传输差异等诸多因素影响,报文在传输过程中易出现乱序抵达的情况。如果接收方直接按照杂乱的到达顺序向上层交付数据,应用程序读取到的字节流就会错乱异常,如果读取数据错乱就会引发相关问题,而报文乱序本身,也是 TCP 传输可靠性需要解决的问题之一。

因此接收方主机 B 在收到一批乱序报文后,必须对接收数据做有序整理,而解决乱序问题的核心依据,正是 TCP 报头里的32 位序号字段。就像主机 A 依次发送序号为 1000、2000、3000、4000 的四份报文,网络传输后可能以 2000、4000、3000、1000 的混乱顺序到达接收端。接收方只需要根据每份报文自带的序号做升序排序,就能还原出发送端原本的发送顺序,保证数据按序完整交付,以此规避乱序异常,守住 TCP 可靠传输的特性。

32 序位号标记的不是第几份报文,而是这段数据在全局连续字节流里的起始字节编号。

确认序号

应答报文本身属于 TCP 报文,同样也需要用序号进行区分。就像主机 A 连续发送 4 个数据报文,最终只收到了 3 个的应答报文,此时就一定能知道有一个数据报文出现了丢包,而应答报文的序号就是 TCP 报头里的 32 位确认序号

TCP 确认序号有着固定统一的规则:接收方将自己完整收到的数据的最大字节序号加 1,作为本次回复的确认序号。这个数字的含义非常明确:确认序号之前的所有连续字节数据,都已经被我接收方完整、有序接收完毕。还是上面的例子 : B 收到 A 发送的序号为 1000 的数据报文后,回复的 32 位确认序号就填写 1001,代表 1001 之前的全部字节都已接收;同理 2000 对应应答 2001、3000 对应 3001、4000 对应 4001。

TCP 的确认序号,代表该序号之前所有连续完整的数据都已经被接收方成功收到 。就像确认序号 2001,不光说明序号 2000 的报文已接收,同时也代表 1000 及更早的全部数据都收到了。回到之前的例子,如果发送方只收到了 1001、2001、4001 这三个应答,就代表 3000 对应的报文一定没有丢失,此时不需要重传 3000 序号的数据。因为既然能收到 4001 的累计确认应答,就证明 3000 这份报文早就被接收方完整接收,只是针对它的 3001 应答报文,在返回发送方的途中丢失了而已。

哪怕是极端情况,发送方只收到了 4001 这一个应答报文,也能百分百确定 1000、2000、3000、4000 所有报文都已经安全送达对方。不再需要发送方重复重传原始数据,极大减少了不必要的报文重传次数,大幅提升了整体传输效率。


1. 主机 A 向主机 B 发送数据时,传输的是什么报文?主机 B 回复应答时,发送的又是什么报文?

站在传输层视角来看,通信双向发送的全部都是 TCP 报文 。TCP 报文 = TCP 报头 + 上层有效数据载荷两部分共同组成。发送方 A 主动传输业务数据时,报文既包含完整报头,也携带对应的有效载荷;而接收方 B 回复确认应答报文时,大概率不需要附带数据,也就是没有有效载荷,但必须携带完整的 TCP 报头 ,序号、确认序号、标志位等关键信息都封装在报头当中。无论是否携带业务数据,应答报文和数据报文在格式规范上本质都属于 TCP 报文,我们必须要有这个认知。

捎带应答机制

接着我们思考第二个核心疑问:主机 A 发送数据报文时,报头只需要填写 32 位序号,主机 B 回复应答报文时,只需要填写 32 位确认序号就足够,那为什么 TCP 报头里,每个报文都必须同时保留序号、确认序号两个字段只用单独一个序号字段不行吗?

这就要结合 TCP 全双工通信捎带应答机制来理解。当 A 向 B 发送数据报文后,此时 B 刚好也有数据需要发送给 A (因为是全双工通信,A 和 B需要同时向对方收发消息),此时 B 就不需要单独发送一份数据报文给 A,而是直接把自己要发给 A 的数据,打包进回复 A 的应答报文中一起发送。这种双向数据同步收发、应答顺带携带业务数据的模式,就是 TCP 捎带应答机制。

在这种场景下,这个报文既需要用 32 位序号 标识 B 自己发给 A 的数据顺序,又需要用 32 位确认序号应答 A 之前发来的数据,两个字段缺一不可。序号负责标记自身传输的数据字节顺序,确认序号负责回复对方的应答报文顺序,二者配合就实现了 TCP 全双工双向的并发传输,大幅减少报文数量、提升通信效率。现实网络里这种捎带应答的场景十分普遍,这也是 TCP 每个报文报头,都必须同时携带序号与确认序号两个字段的核心原因。


五、流量控制

我们之前在学 TCP 套接字时就已经知道了,TCP 通信的两端都会各自维护发送缓冲区与接收缓冲区 ,这里我们光看接收方的接收缓冲区

一旦发送方持续高频发送数据或发送速率过快,导致要发出的数据报文总量超出了接收方接收缓冲区的剩余容纳空间,接收方就会来不及处理这些数据报文。举个很直白的例子:发送方一次性准备发送 10 个数据报文,但接收方缓冲区只剩下能存放 1 个报文的空闲位置。如果采用粗暴丢弃报文的方式,直接丢掉剩下 9 个报文,接收方也只会应答已收到的 1 个报文,剩余数据就只能依靠发送方反复超时重传。这种处理方式虽然可行,但会造成大量无效重传、网络资源浪费,整体传输效率极低。

因此 TCP 必须严格约束发送方的发送速度,让发送速率能匹配上接收方当下的缓冲区的接收能力与数据处理能力,这种匹配收发速率、平衡双方传输能力的机制,就是TCP 流量控制

想要让发送方主机 A 精准掌握接收方主机 B 的数据接收能力,就需要理清两个核心问题。

第一个问题,接收方该如何衡量自身的接收能力?

衡量接收方最核心、最本质的接收能力,就是接收方本身接收缓冲区当中剩余可用空间的大小。

16位窗口大小

第二个问题,发送方要怎么得知对方的接收能力?

这就要依靠 TCP 报头里的 16 位窗口大小 字段,这个窗口大小对应的就是接收方自身接收缓冲区的剩余容量大小。我们之前讲过,TCP 双向通信往来的所有报文,本质都是标准 TCP 报文,因此接收方主机 B 在给主机 A 回复应答报文时,会把自己当下接收缓冲区的剩余空间大小,填写在报文头部的 16 位窗口大小字段里,一同发送给主机 A。 主机 A 收到这份应答后,就能知晓 B 的接收缓冲区上限与剩余可用空间,以此动态调整自己单次发送的数据总量、整体发送速率 。这里一定要牢记规则:报文里窗口大小,永远填写自己本机的接收缓冲区大小,发送方就填发送方自身发送相关窗口,接收方就填接收方自身接收缓冲区大小。因为这份报文是发给对方的,填写对方的缓冲区数值没有任何实际意义。

由此我们也能明白,TCP 原则上会对每一份数据报文都做出应答,核心目的就是持续同步双方最新的窗口大小,以此预判彼此收发能力、实时动态调整传输策略。如果主机 A 数据发送速度过慢,流量控制机制也会对应上调发送速率。因此 TCP 流量控制同时兼顾了传输可靠性与通信效率,数据发送太快会造成丢包,绝对不允许;发送太慢会浪费网络传输资源,同样会被动态调控优化。

六、6 个标志位

接下来我们学习 TCP 报头里非常关键的 6 个标志位字段。之前我们一直在讲解两台主机 A、B 之间的点对点通信,而实际网络里,服务端和客户端也属于主机,服务端会同时对接大量客户端,会收到各式各样的 TCP 报文。这些报文类型并不统一,既有普通的数据传输报文,也有连接建立报文、连接断开报文,还有单纯的确认应答报文。

**不同类型的 TCP 报文,服务端对应的处理逻辑就不同。因此 TCP 报文头部专门设计了 6 个标志位,用来区分每一种报文的用途与类型。**报文本身天生就存在功能类型差异,标志位就是 TCP 用来识别报文身份、执行对应处理流程的核心字段。

TCP 报头里最常用的就是上面 6 个核心标志位。严格来说底层内核结构体里一共有 8 个标志位,多出的 2 个标志位属于后期拥塞控制字段,日常常规通信、三次握手四次挥手、数据应答场景都不会用到,默认不用关注。同时 TCP 内核报文结构体里,本身就专门配套了对应的标志位字段,硬件和内核就是靠这个字段,识别每一份 TCP 报文的类型与功能。

我们先讲第一个标志位 :

ACK

我们先来讲解第一个核心标志位 ACK。就像主机 A 向主机 B 发送携带数据的报文后,B 需要向 A 回复对应的确认应答报文。而主机 A 判断收到的这份报文是不是应答报文,看的就是应答报文报头里的 ACK 标志位。只要 ACK 为 1,就代表当前报文具备应答功能 ,里面的确认序号正式生效;如果为 0,就代表这份报文不携带应答信息,不属于应答报文。结合我们之前学习的捎带应答机制,TCP 全双工通信里,绝大多数往来报文都会同时携带应答与数据,因此日常传输中大部分 TCP 报文的 ACK 标志位都固定为 1。同时 ACK 标志位也是三次握手建立连接、四次挥手断开连接流程里的核心标识,整个连接建立与断开全过程,都离不开 ACK 位的应答确认配合。

初识三次握手

接下来我们正式初识 TCP 的三次握手。三次握手本质就是 TCP 通信双方建立连接的过程。TCP 是面向连接的传输协议,主机 A 想要向主机 B 传输数据,绝对不能上来就直接发送消息,必须先通过三次报文交互完成连接建立,确认双方收发能力正常。

三次握手的流程依次是:首先客户端主机 A,向服务端主机 B 发送一个 SYN 标志位为 1 的同步报文,申请和对方建立 TCP 连接 。主机 B 收到这份连接请求后,确认自身可以建立连接,就会回复给 A 一份 SYN+ACK 两个标志位均为 1 的应答同步报文,既确认收到了 A 的连接请求,同时也同步自己的连接序号。主机 A 收到这份报文后,最后再向 B 发送一份 ACK 标志位为 1的确认应答报文,完成双向序号收尾确认。一来一回总共三次报文交互,因此被叫做三次握手,三次报文全部交互完成后,双方 TCP 连接就正式建立成功,进入可以正常传输数据的状态。

这三次交互的报文,我们都可以优先理解为只携带 TCP 报头、没有额外业务数据。其中第三次 A 发给 B 的 ACK 报文,可能会带有数据,但初学阶段我们统一理解为三次报文都以报头交互为主。三份报文里的 SYN、ACK 标志位,都会严格按照自身报文功能,对应置 1,以此区分连接请求、同步应答、最终确认三种不同报文身份。

TCP 连接结构体:

在三次握手顺利完成、TCP 连接正式建立之后,我们就要弄明白一个核心问题:TCP 连接到底是什么?

其实 TCP 连接的本质,就是操作系统内核里的一个专属结构体。inet_connection_sock 结构体,正是 Linux 内核里标准的 TCP 连接结构体,里面完整存储了这条连接的序号信息、超时定时器、窗口大小、拥塞控制参数、等待队列、状态信息等所有和这条 TCP 连接相关的全部数据。

服务端会同时和多个客户端建立连接,所以两端的OS内核都会同时存在大量的 TCP 连接,此时就要用先描述再组织的方式对连接结构体进行组织管理。

也正因如此,TCP 建立、维护连接都需要消耗额外的内核内存与 CPU 时间资源,它的时间与空间开销,要远比无连接、不需要维护内核结构体的 UDP 协议大得多。

为什么要进行三次握手?

双方在正式通信传输数据前,首要前提就是保障双向网络是通畅的,而三次握手就是验证网络连通性最高效、次数最少的方案。TCP 本身是全双工通信协议,必须同时确认主机 A、主机 B 双向都具备正常发送、正常接收数据的完整能力。整个验证流程逻辑清晰:**A 先向 B 发送连接请求,确认 A 可以发送、B 可以接收;随后 B 向 A 同步应答,确认 B 可以发送、A 可以接收;最后 A 再次向 B 发送确认报文,让 B 明确知晓自己上一轮的应答已经被成功接收,彻底验证 B 的发送能力正常。**经过这三轮交互,双方双向收发能力全部核验无误,整条网络连通正常。三次就是验证全双工链路的最少必要次数,再多额外握手交互,都只会无端消耗网络资源、增加时延,属于完全没有意义的性能浪费。

SYN

讲完 ACK 标志位,我们接着讲解第二个核心标志位 SYN。SYN 标志位,就是专门用来标识TCP 连接建立请求的标志,只要报文报头里 SYN 位为 1,就代表这是一个同步 报文段,表示申请建立 TCP 可靠连接。TCP 三次握手整个流程,核心就是依靠 SYN 和 ACK 两个标志位配合完成。同时 SYN 报文不携带数据,只占用 1 个序号,这也是它和普通数据报文关键区别。

TCP 报文段就是我们说的 TCP 报文,是传输层经过完整封装后、在网络中独立发送的 TCP 数据包,整体结构由 TCP 报头与数据载荷两部分共同组成。而 SYN 标志位为 1 的报文,就被称作同步报文段 ,这类报文不会携带任何业务数据,唯一的作用就是让通信双方交换、对齐整条 TCP 字节流的起始编号,完成序列同步 。TCP 本身是面向连续字节流的协议,数据流里的每一个字节,都拥有独一无二的专属序号,在 TCP 连接正式建立之前,通信双方会各自生成一个专属的初始字节序号 ISN,这个用来约定字节流起点、在 SYN 报文中互相交换对齐的序号,就是同步序列号。客户端会通过 SYN 报文,把自己的初始同步序列号 发送给服务端,服务端也会通过 SYN 报文,把自身的初始同步序列号同步给客户端,双方互相确认、认可彼此的数据流起始编号,完成全双向的序号同步。

FIN

接下来我们讲解 TCP 第三个核心标志位 FIN。FIN 标志位的作用非常明确,就是用来告知通信对方:当前本端已经没有数据需要继续发送,主动申请关闭当前方向的 TCP 连接。 凡是报头中 FIN 标志位置 1 的报文,我们都统一称作结束报文段

和 SYN 同步报文段类似,FIN 结束报文段同样不携带业务数据,只会占用一个字节的序号,专门用来发起连接断开请求。TCP 是全双工的,客户端与服务端两个方向的连接需要单独关闭、单独确认,因此 FIN 标志位配合 ACK 标志位,就构成了 TCP 断开连接四次挥手流程的核心逻辑。

初识四次挥手

当客户端主动想要关闭 TCP 连接时,就会发起断开连接请求,先向服务端发送一份 FIN 标志位置 1 的结束报文段。服务端收到这份断开请求后,会立刻回复 ACK 应答报文,用来确认收到客户端的关闭请求,告知对方自己已知晓关闭意愿。此时服务端可能还有剩余数据没有传输完毕,等自身所有数据发送处理完成后,再向客户端发送 FIN 报文,告知客户端自己也已经没有数据需要发送,同样申请关闭自身方向的连接。客户端收到这份 FIN 报文后,最后再回复一份 ACK 确认报文。整个流程就像 A 向 B 提出断开连接,B 应答同意;B 再向 A 提出断开连接,A 最终应答确认,TCP 全双工双向连接,必须经过双方互相确认同意,才能完整断开。

四次挥手交互的报文,依旧是以 TCP 报头为主,也支持报头顺带携带少量收尾数据。

为什么要是四次挥手?

而 TCP 必须需要四次挥手,正是因为全双工通道两条链路相互独立,要用最少、最高效的次数,完成双方双向关闭意愿的互相确认。

而连接断开完成之后,双方内核里对应的 TCP 连接结构体就会被系统自动回收释放。代码层面上,就表现为通信双方进程调用 close 接口关闭套接字,走完完整四次挥手状态流程。流程全部结束后,这条连接对应的内核连接结构体、套接字缓冲区、状态信息、定时器资源等所有相关数据,都会被操作系统彻底清空释放。

四次挥手的核心本质,就是关闭 TCP 双向独立的全双工通信通道:A 告知 B 自己数据发送完毕,不再向对方传输数据;B 也告知 A 自身数据发送完毕,不再向对方传输数据,两条单向通道分别关闭、分别确认,最终整条 TCP 连接彻底断开。

四次挥手也可以压缩为三次挥手,也就是中间有捎带应答了。反过来,三次握手本质上也可以等价理解为四次握手流程。TCP 三次握手的核心目的,就是完成两件关键事情:

  1. 第一是双方主机建立通信共识,
  2. 第二是验证全双工,即验证网络是否通畅。

日常常规网络场景里,TCP 建立连接固定使用三次握手,断开连接固定使用四次挥手,那为什么二者不统一流程?

核心原因就在于连接建立和断开的场景不同。建立连接时,客户端向服务端发起连接请求,服务端本身就是被动等待连接的角色,一定会无条件同意建立连接,因此服务端可以同时把ACK 应答报文和 SYN 同步连接报文,通过捎带应答合并在同一个报文里发送,直接减少一次交互,把四次流程压缩成三次握手。

但断开连接的场景完全不一样,主动方发起关闭请求后,被动方并不会立刻同意断开连接。因为断开连接和上层业务绑定,客户端发起 FIN 关闭请求时,服务端上层应用可能还有未处理完毕、未发送完成的业务数据,没办法马上响应断开。它必须先单独回复 ACK 确认收到关闭请求,等所有剩余数据全部处理传输完毕后,才能再单独发送 FIN 关闭报文。所以断开连接时 ACK 应答和 FIN 关闭请求无法合并捎带,不能压缩流程,因此常态下 TCP 断开连接就必须走完整四次挥手流程。

RST

接下来我们讲解第四个 TCP 标志位 RST。这里我们先抛出一个关键问题:站在客户端视角,三次握手流程里客户端发出最后一次 ACK 报文后,到底是报文一发送出去就算握手完成? 还是必须确认服务端成功收到这个 ACK,才算三次握手正式结束?

正确答案是前者客户端只要把最后这个 ACK 报文发送出去,就会判定三次握手流程完毕,直接进入可以传输数据的连接就绪状态,这是发送端单方面的判定逻辑。

随之就会产生一个异常场景:**如果这份收尾 ACK 报文在网络传输途中丢失,或是因为网络时延等问题,服务端始终没有收到这份报文。那么在服务端视角里,这次三次握手全程并未成功,连接依旧处于未就绪状态。**此时客户端已经默认连接建立完成,开始向服务端发送业务数据报文,服务端在未完成连接建立的状态下,突然收到了不属于正常连接的数据,就会判定当前 TCP 连接出现异常错乱。

这时服务端就会向客户端发送一份 RST 标志位置 1 的报文 ,我们把这类携带 RST 标志的报文叫做复位报文段,用来告知对方当前连接异常失效,需要断开当前错乱连接、重新发起 TCP 连接建立流程。简单来说 RST 标志,就是用来复位异常 TCP 连接,强制要求双方重新建立正常连接。

**从这个场景我们也能理解,三次握手本质上也存在一次概率博弈,就是赌最后一份 ACK 报文能否被对方正常接收。一旦这次博弈失败、报文丢失导致两端连接状态不一致,就会触发 RST 报文复位,双方重新走完整连接建立流程。**当然真实网络环境里 RST 报文触发的场景、处理逻辑,远比我们讲解的基础案例要复杂得多。

PSH:

接下来我们讲解第五个 TCP 标志位 PSH。我们用一个很直白、略微极端但极易理解的例子理解这个标志位 :

正常通信流程里,客户端向服务端传输数据,本质就是把数据写入服务端(接收方)的接收缓冲区,而服务端能接收多少数据,完全由自身接收缓冲区的剩余空间决定。服务端在回应客户端的 ACK 应答报文中,会通过 16 位窗口大小,告知自己当前缓冲区剩余容量,比如标注窗口大小为 1024 字节,客户端就只会发送对应大小的数据。一旦数据填满缓冲区,剩余空间变为 0,客户端就必须暂停发送业务数据,等待服务端释放缓冲区位置。

而服务端缓冲区能不能腾出空间,取决于上层应用层的业务处理速度。如果服务端上层程序长时间不读取缓冲区里缓存的数据,接收缓冲区就会持续阻塞、无法接收新数据。这时客户端就会周期性发送报头探测报文,循环询问服务端缓冲区是否已经就绪。此时虽然服务端的接收缓冲区大小为 0,不能接收数据,但并不代表服务端不能接受报头,因此客户端可以发一个报头给服务端,里面并没有业务数据,不会占用缓冲区空间,服务端收到后也会回复纯报头应答,同步最新的窗口状态。

如果客户端反复多次询问,服务端缓冲区始终没有空闲空间,客户端就会将报文头部的 PSH 标志位置 1 发送出去。服务端识别到 PSH=1 后,就会收到强制提醒:需要立刻让上层应用程序,快速读取走 TCP 接收缓冲区里积压的数据。 所以 PSH 标志位的核心作用,就是主动告知对端应用层,马上从 TCP 内核缓冲区中取出缓存数据,不要再积压等待。

这个场景虽然比较极端,但非常方便理解,实际上日常正常传输场景里,PSH 标志位也经常会被置 1。 本质上来说,要不要及时读取缓冲区数据,主动权完全掌握在对端应用层手上,正常逻辑就是缓冲区一有数据,上层程序就尽快取用。而真实网络环境里,TCP 会先在缓冲区积攒一部分数据,再批量通知上层统一读取,以此减少频繁交互开销,提升整体传输效率,PSH 就是用来打破缓存等待、强制立即交付数据的信号。

URG :

紧急指针

最后我们来学习第六个 TCP 标志位 URG。URG 标志位的核心作用就是判断报文里配套的 16 位紧急指针字段是否生效。TCP 报头中专门预留了 16 位紧急指针字段,当 URG 标志位取值为 0 时,代表这个紧急指针暂时无效、没有实际意义,接收端可以忽略;当 URG 标志位为 1 时,就代表对应的 16 位紧急指针正式生效,接收端必须重点解析这个字段的内容。因此 URG 本质上就是紧急指针功能的开关。

关键在于理解 16 位紧急指针到底是什么?

指针本身就具备独有的指向性与唯一索引特性,不管是内存地址、数组下标、文件索引,还是数据偏移量,都可以归为指针的范畴。**而这里的 16 位紧急指针,本质就是一个数据偏移量,**用来精准标记当前报文段里,哪一段字节数据属于需要优先处理的紧急数据,让对端绕过常规缓冲区排队逻辑,优先读取响应高优先级紧急内容。

首先 TCP 里 16 位紧急指针,核心含义就是在整条 TCP 字节数据流里,标记出一段需要被优先插队处理的紧急数据的末尾位置 ,它本身就是一个序号偏移量

那这个偏移量,到底是当前这份报文自己内部的偏移,还是整个接收缓冲区字节流的偏移量?

答案非常明确:它是接收缓冲区内的偏移量。因为 TCP 是面向连续字节流的协议,每一个字节在整条连接里都有独一无二的序号

缓冲区内的偏移量 ? 怎么理解?

想要理解缓冲区内的偏移量,我们可以把 TCP 发送缓冲区直接类比成一个 char 字符数组。 TCP 本身是面向字节流的协议,只要应用层通过 read、write 接口把数据拷贝进缓冲区,每一个独立字节都会天然拥有专属的全局序号。比如单次发送 1000 字节数据,这 1000 个字节就依次对应连续的 TCP 序号,对端收到报文后,就会回复 32 位确认序号 1001,代表自己已经收到 1000 及之前所有字节,告知我方下一次传输要从数组下标也就是序号 1001 的字节开始继续发送。双方就是依靠这套序号机制完成可靠确认应答,我们完全可以把 TCP 字节序号,等价理解为缓冲区字节数组的下标,而数组下标本身,本质就是相对于整条字节流起点的偏移量。当然实际网络场景里,序号还会结合安全随机初始 ISN、循环序号机制,整体逻辑会比单纯数组下标更复杂严谨。

那我们可不可以把紧急指针的位置,当作应用层缓冲区中的某一个偏移量?

可以把紧急指针对应的位置直接理解为 TCP 接收缓冲区字节流当中的全局偏移量。我们依旧沿用缓冲区字符数组的类比,整个 TCP 接收缓冲区就相当于一段连续的 char 字节数组,连接里每一个字节对应的 TCP 序号,就等价于这个数组的专属下标,而数组下标本身就是相对于字节流起点的偏移位置。紧急指针本质就是一段数值偏移量**,它和报文自带的起始序号相加后,就能算出紧急数据末尾在缓冲区字节流里的序号,这个序号就是紧急数据在缓冲区里对应的位置。**需要注意的是这个偏移是针对整条 TCP 全局字节流的整体偏移,并不是单份报文内部的字节偏移,接收端定位到该偏移后,就会优先处理这段紧急数据。

所以在TCP字节流当中,在一个特定的偏移量所对应的位置处,所对应的那一个字节,就叫紧急指针,一般情况下紧急指针所指的数据是1个字节。就表示从特定的下标处那一块属于紧急数据。

为什么会有紧急指针的存在?

我们可以把 TCP 接收缓冲区视作一段连续字节数组,紧急指针本质就是字节偏移量,它和报文起始序号相加,就能算出紧急字节在缓冲区字节流里的序号,精准定位到对应位置,让这段数据被上层优先处理。而 TCP 紧急指针与紧急数据机制诞生的核心原因,正是 TCP 严格的有序字节流特性:所有报文数据都会按到达顺序依次写入接收缓冲区,整体遵循先进先出的队列规则,正常情况下必须从头依次读取,不能跳过前面数据读取后面内容。可一旦缓冲区队列中间出现需要立刻响应的高优先级数据,就没办法正常排队等待,必须实现插队优先处理,TCP 紧急指针就是专门用来实现这种字节流插队能力的机制。

当报文 URG 标志位置 1 时,就代表当前携带紧急带外数据,接收端先识别这份插队报文,再通过 16 位紧急指针锁定缓冲区里对应字节的精准偏移位置,就可以绕过正常队列顺序,单独优先读取这一个紧急字节,注意紧急数据没有后续偏移跟随,只能单独读取这一个插队字节,无法向后连续读取普通顺序数据。 这个完整流程可以在 Xshell 搭配 Linux 网络接口验证,我们读取 TCP 数据使用的 recv函数,最后一个参数 flags就专门用来匹配紧急数据,传入 MSG_OOB标志位,就代表本次读取带外紧急数据(Out Of Band),和正常字节流里的顺序普通数据完全区分开,不会被缓冲区排队逻辑干扰。

recv的标志位OOB:

TCP 紧急带外数据的存在,本质就是给严格串行排队的 TCP 字节流增加优先级插队管理能力,不用等待前面积压的普通数据处理完毕,就能立刻响应关键紧急指令,保障高频、高优先级的控制指令可以极速送达上层应用,这也是传输层和应用层协同配合的核心紧急通信机制。


下面关于这个接收缓冲区有几个问题。首先就是这个接收缓冲区全是字节流吗?里面是纯字节流吗?只有报文里的有效数载荷数据?没有报头?

TCP 接收缓冲区内部存储的是纯粹无边界的连续字节流 ,不存在 TCP 报头等其他数据类型。内核在收到网络传输过来的 TCP 报文段时,会第一时间剥离解析所有 TCP 报头信息,报头里的序号、各类标志位、窗口大小等协议控制内容,全部由内核自行处理校验,并不会写入接收缓冲区,只有报文当中承载的上层业务有效数据载荷,才会被拷贝存入接收缓冲区当中。因此接收缓冲区里只有纯净的二进制业务字节,没有任何协议头部内容,乱序到达的多个报文数据,也会在内核按照 TCP 序号重新排序拼接,合并成一段完整连续、有序的字节流水,再等待上层应用程序读取使用。

那第二个问题就来了,我们之前不是说 TCP 的接收缓冲区和发送缓冲区本质上不应该是由 sk_buff 结构体组成的队列是吧?

其实二者并不矛盾,一个是内核底层的真实实现,一个是上层逻辑抽象理解

Linux 内核里真实的 TCP 收发缓冲区,底层正是由一个个 sk_buff 结构体串联组成的双向链表队列来管理,每一个 sk_buff 就对应网络收到的一份独立 TCP 报文,结构体里会完整存放这份报文的报头信息、数据载荷、序号信息等全部内容,内核依靠这个链表队列,完成报文的乱序排序、超时管理、重传、去重所有底层操作。

而我们平时学习时,把缓冲区抽象成连续 char 字节数组,是面向 TCP 字节流特性的逻辑简化理解:**内核会把链表中所有 sk_buff 里的数据载荷,按照 TCP 全局序号依次拼接、排序重组,剥离掉所有报文报头边界后,在逻辑上拼成一整条连续无分割的纯净字节流。**底层链表队列负责处理一个个独立数据包、协议控制逻辑、内核调度管理,上层字节数组逻辑负责面向应用层,屏蔽掉报文拆分细节,让应用层只感知连续有序的字节数据,二者相辅相成,底层队列保障 TCP 可靠传输的协议规则,上层字节流抽象匹配 TCP 面向字节的通信特性,完全不会产生冲突。

至此,关于 TCP 协议报文 (报头) 格式中的所有字段就讲解完毕了。

七、总结

本文系统讲解了TCP协议的核心机制与工作原理。首先介绍了TCP作为面向连接的可靠传输协议,通过三次握手建立连接确保双向通信能力,通过序号确认、超时重传等机制保障数据传输可靠性。文章详细解析了TCP报文头部结构,包括端口号、序号、确认号、窗口大小等关键字段的作用。重点阐述了流量控制、拥塞控制原理,以及SYN、ACK、FIN等6个标志位在三次握手和四次挥手过程中的核心功能。同时深入探讨了TCP全双工通信特性、字节流传输模式、紧急指针机制等底层实现原理,揭示了TCP如何在保证可靠性的同时兼顾传输效率。最后通过内核连接结构体分析,说明了TCP连接的本质及其资源管理方式。

谢谢大家的观看!

相关推荐
文青小兵4 小时前
云计算Linux——数据库MySQL主从复制和读写分离(十七)
linux·运维·服务器·数据库·mysql·云计算
文青小兵4 小时前
云计算Linux——负载均衡 (十四)
linux·运维·服务器·nginx·云计算·负载均衡
Sagittarius_A*4 小时前
H3CSE 高性能园区网:STP 生成树协议技术原理与配置
网络·计算机网络·h3cse
深圳恒讯4 小时前
荷兰服务器到中国大陆的平均延迟是多少?
运维·服务器
zincsweet4 小时前
Linux中环境变量的逐步理解
linux
酿情师4 小时前
记一次 CentOS 7 服务器网络配置与 SSH 远程连接排错
服务器·网络·centos
zhglhy4 小时前
Ubuntu mongodb-org-tools工具安装
linux·mongodb·ubuntu
yezipi耶不耶4 小时前
讲讲 RTMate (WebSocket as A Service)中的消息的发布订阅机制
websocket·网络协议·rust
hbugs0014 小时前
PNetLab-vs-EVE-NG安全性分析
网络·eve-ng·eve-ng模拟器