目录
[三次握手 --- 建立连接](#三次握手 --- 建立连接)
[为什么是三次握手呢?(面试题,重要)](#为什么是三次握手呢?(面试题,重要))
[四次挥手 --- 断开连接](#四次挥手 --- 断开连接)
[验证 accept](#验证 accept)
[listen() 的第二个参数 backlog](#listen() 的第二个参数 backlog)
[CLOSE_WAIT状态 和 TIME_WAIT状态](#CLOSE_WAIT状态 和 TIME_WAIT状态)
[TIME_WAIT :](#TIME_WAIT :)
[FIN_WAIT_1状态 和 FIN_WAIT_2状态](#FIN_WAIT_1状态 和 FIN_WAIT_2状态)
[1. 最左侧丢失](#1. 最左侧丢失)
[2. 中间丢失](#2. 中间丢失)
[3. 最右侧丢失](#3. 最右侧丢失)
上一篇文章中我们讲了 TCP 协议的报文格式,围绕报文格式对 TCP 的可靠性、流量控制、以及三次握手与四次挥手有了初步的认识,本篇文章在上篇文章的基础上,对 TCP 的确认应答机制,超时重传机制,连接管理机制,流量控制,滑动窗口展开正式讲解。
一、确认应答(ACK机制)机制
我们先来讲解 TCP 可靠性传输的核心基石 ------ ACK 确认应答机制。

TCP 之所以能实现可靠传输,核心原因之一就是依靠确认应答机制,接收方每收到一个数据报文,都会回复对应的 ACK 确认应答报文,以此保证数据的安全到达。又因为 TCP 最新发送的报文永远没办法获得最新的应答 。所以 TCP 只对历史报文进行可靠性应答 。只要对方针对某一段历史报文回复了应答确认,发送方就可以百分百确定这段历史数据已经被对方完整接收。
配合这套确认应答规则,TCP 报头里定义了两个的 32 位序号:第一个是32 位序号 ,也就是我们之前一直在讲的全局字节序号,它标记当前这段数据,在整条 TCP 字节流里的起始字节编号,对应缓冲区数组里的下标位置,这个序号也是对重复报文去重的关键;第二个是32 位确认序号 ,它的含义永远是对方下一次应该发给我的第一个字节编号。就像上图中主机 A 发送 1~1000 字节的数据,主机 B 就会回复确认序号 1001,代表 1000 及之前所有历史字节都已经完整接收,接下来等待主机 A 从 1001 序号开始继续发送后续数据。正是依靠一一对应的发送序号与累计确认序号,配合 ACK 应答机制,TCP 才能精准追溯每一个字节的收发状态,完美保障全链路数据传输可靠不丢失、不乱序。
ACK 报文 = TCP 应答报文,ACK 就是确认标志位,TCP 报文里 ACK 标志位置 1,这个报文就表示是用来做应答确认的 ACK 应答报文。
ACK 应答报文核心依靠 32 位确认序号,告诉发送方:你之前所有序号之前的字节,我都已经完整收好了,下次你就从这个确认序号开始接着发就行。TCP 全程就是靠一次次 ACK 应答报文,给历史所有数据做可靠兜底,保障整条字节流传输不丢包、不乱序。
二、超时重传机制
接下来我们讲解 TCP 第二个核心可靠传输机制 ------超时重传机制。
网络里 TCP 丢包一共就分为两种场景,这也是 TCP 协议人为统一规定的判定逻辑 :
- 含有效载荷数据的报文丢了
- 应答报文丢了
含有效载荷数据的报文丢了(没发过去)
第一种是携带有效载荷的数据报文丢失,主机 A 发送的数据报文在发送途中丢失,主机 B 根本收不到这份数据,自然就不会回复对应的 ACK 应答报文,主机 A 迟迟等不到回应,此时主机 A 再等就是无意义的。
应答报文丢了(没发回来)
第二种则是ACK 应答报文丢失,主机 B 其实已经正常收到了发来的数据报文,也正常回复了应答报文,但这个应答报文在回发途中丢失,主机 A 依旧收不到回应,同样此时主机 A 再等就是无意义的。
所以站在发送方主机 A 的视角,它无法区分到底是数据没送到,还是应答没回来。因此 TCP 就约定了一个固定的超时时间间隔,只要主机 A 发出报文后,在规定时间间隔内没有收到对方的 ACK 确认应答,就判定为丢包,就会触发超时重传机制重新发送对应的数据报文,无论是哪种情况。这个重传机制就是 TCP 超时重传机制。
如果单次重传过后依旧收不到应答,主机就会继续重传,当多次重传全部失败、始终无法收到对方确认时,就会判定当前网络链路彻底异常,主动断开这条 TCP 连接。
那这个特定的时间间隔是谁设定的?
这个超时判定用的特定时间间隔,并不是固定写死的数值,而是 TCP 协议动态调控的。如果这个超时间隔设置得过短,主机 B 还没来得及回复 ACK 应答报文或还没发至给主机 A,超时计时就已经结束,主机 A 就会误判为丢包、就会重传数据,此时就会导致效率下降。而如果超时间隔设置得太长,比如数据报文已经在丢失了,发送方却还在漫长等待应答,白白浪费时间,同样会严重降低 TCP 通信效率。

所以 TCP 想要适配千变万化的网络环境,就必须动态计算地调整这个超时时间间隔,从而找到最优的时间间隔。在 Linux、Windows 等主流操作系统里,这个超时时间都以 500ms作为基础单位来管控:第一次超时未收到应答,等待 500ms 就触发重传;依旧没有回应,就等待 2 倍 500ms 再次重传;后续等待时间按照 4 倍、8 倍...... 以指数形式不断递增。当累计重传次数达到上限后,TCP 就会判定网络链路或对端主机出现严重异常,强制断开当前 TCP 连接。
由此也能明确,这个超时时间间隔从来不是固定值,它是持续浮动变化的,和当下网络健康状况强相关。
补充 : 如果是应答报文半路丢失引发的超时重传,主机 B 其实已经就收到了正常数据,重复重传会让它收到重复报文数据,这时 TCP 就会依靠字节序号做有序去重处理,默认丢弃老旧重复的数据报文,不会造成上层数据错乱,完美保障字节流传输一致性。
同一个32 位序号,同时承担两大核心作用:
- 标记全局字节位置:它是 TCP 缓冲区字节流里,每一个字节独一无二的数组下标,标记了数据在整个字节流中的偏移位置,配合 ACK 确认、紧急指针,完成有序收发、插队优先处理。
- 报文自动去重:因为每个字节序号是唯一不重复的,接收端只要对照已经确认接收过的序号区间,收到重复序号的数据,就可以直接判定是超时重传带来的重复报文,直接丢弃去重,不会重复写入接收缓冲区、不会错乱上层字节流。
不管是正常顺序传输、超时重复重传、应答报文丢失引发的重复发包,32 位序号都既能找准字节在全局数据流里的准确位置,又能过滤重复数据,两个功能共用一套序号体系,底层逻辑统一。
三、连接管理机制
三次握手 --- 建立连接
三次握手是 TCP 最核心的连接管理机制,专门负责可靠建立 TCP 双向连接。
在学习三次握手之前,我们要有以下的前提 :
- 三次握手全过程都发生在双方的TCP 传输层,和上层无关;
- 双方交互的报文至少包含 TCP 报头,不只是单纯的标志位或比特位,只是把对应的标志位置 1 即可,特殊场景握手报文也可以携带少量业务数据;
- 每一次报文收发完成后,双方都会发生状态的变化。
流程:
客户端主动向服务器发送 SYN 同步报文,报文发送完成的瞬间,客户端的状态就从初始状态 CLOSED 切换为 SYN_SENT 同步发送状态。服务器收到 SYN 报文后,自身状态就从 LISTEN 监听状态变为 SYN_RCVD 同步接收状态,同时立刻回复 SYN+ACK 同步应答报文。客户端收到服务器的 SYN+ACK 报文后,发送最后一份 ACK 确认应答报文,这份 ACK 报文发送出去之后,客户端就直接进入 ESTABLISHED 连接已建立就绪状态;而服务器必须完整收到这份收尾 ACK 报文之后,状态才会切换为 ESTABLISHED,至此双方都处于 ESTABLISHED 连接建立状态,双方 TCP 连接正式建立完毕,应用层就可以开始正常收发数据。
细节:
细节1 : 三次握手中每一次握手是否完成,都以本方报文成功发送出去作为判定标准,只要当前这一轮的报文发出去了,就代表本次握手步骤完成,不会等待也不关心对方有没有收到这份报文。
细节2 : TCP 三次握手全流程,都是两端操作系统内核的 TCP 传输层自动独立完成的,和上层应用层无关。我们代码里调用 socket、bind、listen、accept 这些套接字接口,只是给内核做好连接准备、等待内核通知连接结果,底层报文交互、状态切换、握手逻辑,应用层感知不到,和内核三次握手底层流程没有直接关联。
细节3 : 就是 RST 复位标志位,刚好对应之前我们讲过的异常场景:客户端发完最后一次 ACK,就进入 ESTABLISHED 就绪状态并发业务数据,但服务器一直没收到这份收尾 ACK,依旧停在 SYN_RCVD 状态。这时服务器突然收到客户端发来的数据报文,发现连接状态异常不匹配,就会立刻回复 RST 复位报文,强制中断本次异常 TCP 连接,强制要求双方重新建立正常连接。
三次握手与套接字的关系:
客户端的 connect 是用来发起三次握手,相当于只是发起建立连接的请求,三次握手的底层细节不关心,connect 的返回值就代表三次握手成功,连接已经建立好了,客户端就处于 ESTABLISHED状态,然后内核把建立好的连接和 socket 创建的文件描述符关联起来,所以我们上层就能通过文件描述符fd来收发数据了。connect 会阻塞等待,所以整个三次握手的时间就是 connect 接口阻塞等待的原因。
再看服务端的 accept 接口,如果底层三次握手后连接已经建立好了,一个已经建立好的连接,我们调用 accept 后直接接受就行了,再将这个连接和文件描述符关联起来即可,所以 accept 是用于获取已经建立好的连接。底层表现为 TCP 三次握手在内核中完成、连接正式建立完毕后 accept 会被唤醒,它负责从内核已完成连接的队列里,取出这条就绪连接,生成一个全新的通信文件描述符返回给应用层,把这条完整连接和新 fd 做绑定关联。
所以 connect 和 accept 都不参与三次握手。相对来说 connect 只是发起三次握手,而 accept 压根不会参与,accept只是单纯领取一条早就握手完成、就绪可用的连接。
服务端:
首先服务端的 socket() 接口,socket 负责创建监听套接字 listenfd,这是服务端第一个文件描述符,此时它还只是一个空白套接字,没有绑定任何端口、不监听任何连接。接着 bind() 接口,专门给这个 listenfd 绑定固定的服务器 IP 和端口号,让这个文件描述符和端口挂钩,明确自己要在哪个地址等待客户端连接,全程依旧不参与任何三次握手报文交互。之后 listen() 接口,把绑定好端口的 listenfd 正式升级为监听套接字,让内核开启监听模式,持续在这个端口等待客户端发来 SYN 握手请求。这个监听套接字只负责接收客户端连接请求,不会参与三次握手。当客户端发来 SYN 开始三次握手后,内核和客户端完整跑完三次握手、TCP 连接建立完毕,这条就绪连接会进入内核已完成连接队列。这时阻塞的 accept() 接口被唤醒,它不会改动监听套接字 listenfd,而是全新创建一个通信套接字 connfd,把这条已经握手完成的 TCP 连接,和这个全新 connfd 绑定关联,再返回给上层应用。
客户端:
客户端首先调用 socket() 接口,创建出唯一一个通信文件描述符 fd,就专门用来和服务器建立连接、后续收发业务数据,没有监听、绑定端口的步骤。客户端不需要固定端口,操作系统会自动随机分配一个临时端口给这个 fd,不需要 bind 绑定地址,也不需要 listen 监听别人的连接。接着客户端调用 connect() 接口,传入服务器的 IP 和端口,主动发起 TCP 三次握手请求。整个 SYN、SYN+ACK、ACK 握手报文交互、两端 TCP 状态切换,全部由内核 TCP 层自动完成,和双方上层无关,connect 会一直阻塞等待,直到三次握手全部结束、连接正式建立。握手完成后,内核直接把这条双向 TCP 连接,和 socket 创建的这同一个 fd 绑定关联,客户端就进入 ESTABLISHED 连接就绪状态。全程之后所有和服务器的数据读写,都只用这一个文件描述符 fd 完成,不会像服务端一样拆分出监听套接字和通信套接字,一条连接从头到尾只对应一个套接字。
为什么是三次握手呢?(面试题,重要)
为什么是三次握手,首先我们要建立以下的认知,首先我们要明白三次握手的目的是双方建立连接,在通信前先建立好通信的信道。
至于为什么是三次,我们首先要确认一个细节 : 那就是阻碍双方正常通信的原因是什么?
1.双方的意愿问题 : 要么主机AB或服务端和客户端的一方不想通信,即没有通信的意愿。更重要的原因是TCP是全双工的,要正常通信就双方就都得支持。
2.网络问题 : 如果双方都很愿意,但是网络进行了阻碍,比如由网络导致的各种问题都会阻碍本次的通信。在有了上面的认知之后我们再谈为什么是三次握手,所以我们也就可以理解为三次握手能不能帮我们把上面的两个问题解决了,首先三次握手能把网络问题验证了,因为它以最小次数验证了全双工,也就是说作为客户端来讲,三次握手一定能保证客户端能发,也一定能收,因为客户端发了 SYN,也收到了服务端发的 SYN+ACK。作为服务端,也保证了自己本身能收和能发,因为服务端发了 SYN+ACK,也收到了客户端的 ACK。这也就成功的验证了TCP的全双工,本质上也就验证了网络没问题,换句话说三次握手是以最小成本验证了网络的通信问题。
那怎么证明双方的意愿问题呢?我们可以从四次握手的层面来回答(为什么是四次握手?因为有捎带机制,SYN+ACK捎带发送了),如果是四次握手,客户端发送了 SYN,接收到了服务端的 ACK。服务端再发送自己的 SYN,然后又接收到了客户端的 ACK。此时双方都向对方发送了建立连接的请求,也就验证了双方的意愿问题。但是真正在我们的网络中,是不需要四次握手的,因为服务端对客户端的正常请求总是愿意的,所以就可以通过捎带应答,将本来要分别发送的SYN和ACK合在一起发送过去,就变成了三次握手,虽然次数减少了,但是效果不变,所以三次握手以最小的成本验证了双方通信意愿的问题。
此时三次握手都能完美高效的解决上面的问题,所以为什么是三次握手就有答案了。
那为什么握手的次数非要是三次?
答案是三次是最小成本的能验证上述问题,当然四次五次六次肯定也可以,但是三次的成本最小,其他的就不需要了。那为什么两次和一次不行呢?
如果是两次握手,只能保证客户端自己能发送和接收数据,而服务端只能证明自己能收数据,无法证明自己能发数据,因为自己发了ACK后,无法保证能被客户端收到,所以就无法证明自己能发数据,也就是无法确认客户端有没有收到自己的应答。所以两次握手也就验证不了TCP的全双工。至于双方的意愿问题,如果客户端先发,就只能证明客户端有这个意愿,无法证明服务端的意愿,所以两次握手也无法验证双方的意愿问题。
两次握手都不行,一次握手就更不行了,所以这就是为什么是握手的次数是三次的原因。
补充细节:
三次握手成功建立连接之后,双方就可以开始通过 read 和 write 收发数据了,但这里有一个非常关键的细节:read 和 write 本身并不是直接和网络打交道的系统调用。
当我们调用 write 发送数据时,它并不是直接把数据丢到网络上,而是把数据拷贝到操作系统内核为这个 TCP 连接分配的发送缓冲区里。真正把这些数据打包成 TCP 报文、加上报头、交给网卡发送出去的,是操作系统内核的 TCP 协议栈。同样,当调用 read 接收数据时,它也不是直接从网络上读取数据,而是从内核维护的接收缓冲区里读取数据。真正负责从网卡接收报文、剥离报头、重组字节流并放入接收缓冲区的,同样是操作系统内核的 TCP 协议栈。
简单来说,应用层的read和write只是和内核的收发缓冲区打交道,网络层面的读写完全由操作系统 TCP 层自主完成,应用程序全程感知不到底层网络的细节。
四次挥手 --- 断开连接
四次挥手正是 TCP 连接管理机制的后半部分,专门负责双向可靠断开 TCP 连接,和三次握手建立连接一一对应。
流程:

我们以客户端请求关闭连接为例 : 客户端调用 close(fd) 接口,率先发送 FIN 断开连接报文,报文发出后自身状态从 ESTABLISHED 切换为 FIN_WAIT_1。服务端收到 FIN 报文后,立刻回复 ACK 确认应答,应答发出后服务端自身状态进入 CLOSE_WAIT 关闭等待状态;同时客户端收到这个 ACK,客户端状态就从 FIN_WAIT_1 切换为 FIN_WAIT_2,此时客户端→服务端的单向写通道已经关闭,无法再发送数据。此时服务端可能还有业务数据没处理完,等到服务端处理完成后,也调用 close() 关闭套接字,向主动端发送自己的 FIN 断开报文,报文发出后服务端状态切换为LAST_ACK 最后等待应答状态。客户端端收到对方 FIN 报文后,回复最后一轮 ACK 确认报文,发出报文后进入TIME_WAIT超时等待状态;服务端收到这个收尾的 ACK 报文后,服务端立刻进入CLOSED 彻底关闭连接状态。等待 TIME_WAIT 时长超时结束后,主动关闭端也最终切换为CLOSED状态,整条 TCP 双向连接完整断开。
同时应用层配合逻辑:服务端被动关闭时,会通过 read 阻塞等待客户端数据,当 read 返回 0 时,就代表对方已经没有数据、发送了 FIN 断开报文,此时再调用close完成自身断开流程。
细节:
细节1 : TCP 四次挥手里,通信双方任意一方都可以充当主动断开连接的发起方,客户端、服务端都能主动发起断开流程,我们上面是以客户端主动断开为例。
细节2 : 一方断开连接的本质是什么? 本质就是当前这一端不再写数据了。
以客户端主动断开为例,客户端断开连接只代表自己没有后续业务数据要发送,此时服务端可能还在处理剩余数据、还有数据要回传,等服务端全部数据处理完毕后,再关闭自身通道,最终全双工双向通信链路就全部关闭。
细节3 : 客户端先发 FIN 报文、收到对应 ACK 应答后,就代表客户端→服务端这条单向信道已经关闭了。但我们上面说了,一方断开连接就表示这一方不写了。那如果下来服务端发来 FIN,客户端还要再回 ACK,再回 ACK 的过程不也是写吗?这不就矛盾了吗?答案并不是,不写是指不发送有效数据载荷了,不代表不能发送 ACK、FIN 带有标志位的报头。回复 ACK 应答属于协议状态交互,不属于数据写入,二者不冲突。
细节4 : 不能认为一方发送 FIN、收到 ACK 应答后,内核就会销毁 TCP 连接结构体和释放对应的文件描述符 fd。此时内核并不会释放连接资源,仅仅是把本地 TCP 连接状态切换为对应的 FIN_WAIT 系列状态,底层连接依然在。
细节5 : 应用层调用 close(fd) 接口,会同时产生两大核心影响:第一,内核会直接关闭对应的文件描述符 fd,清空该文件所有读写权限、关闭文件读写两端;第二,内核会主动触发 TCP 四次挥手流程,向对端发送 FIN 断开报文。简单来说,应用层调用 close,就是四次挥手正式开始的标志。
close(fd) 会一次性把文件描述符对应的文件的读写两端全部关闭,那 TCP 全双工连接能不能只单独关闭其中一端,只断开读通道、或是只断开写通道呢?答案是完全可以,Linux 就代表专门提供了 shutdown() 这个系统调用来实现单向半关闭。
shutdown
shutdown() 函数专门用来单独关闭 TCP 全双工连接里的某一条单向数据流。
它有两个参数,分别是套接字文件描述符,和指定关闭方向的how参数
- SHUT_RD:只关闭读端,之后自己无法再接收对方发来的数据,但依旧可以正常向对方发送业务数据。
- SHUT_WR:只关闭写端,之后自己不能再发送新的业务数据,内核会正常发送 FIN 断开报文;因为 FIN 只是协议控制标志位,不属于业务数据,所以发完 FIN 之后,依旧可以正常接收对方传回的数据。
举一个四次挥手的例子:客户端写完所有业务数据,不需要再发任何内容了,但还要继续接收服务器返回的剩余应答数据。这时客户端就调用 shutdown(fd, SHUT_WR) ,只关闭自己的写通道、发送 FIN 报文完成单向断开,读通道完全保留,还能正常读取服务器后续发来的数据。
而就算用 shutdown 同时关闭读写两端,它和 close() 也有着本质区别:shutdown 双向关闭只是切断两条读写通道,对应的文件描述符 fd 依旧存在、不会被内核销毁;而 close 会直接销毁释放 fd,彻底关闭文件所有读写权限,同时触发整套 TCP 断开流程。简单总结就是,close是彻底释放文件资源,而shutdown只切断网络连接链路,套接字本身还能继续复用使用。
验证 accept
我们在上面的三次握手讲过,accept 接口本身不参与 TCP 三次握手建立连接的底层流程,接下来我们就用服务端代码进行验证 :
我们保留 TCP 服务端完整的 socket创建套接字、bind绑定端口、listen开启端口监听,直接把业务代码里调用 accept 接收连接的逻辑屏蔽掉、不执行 accept,之后正常启动运行这个服务端程序。接着我们用客户端去主动访问这台服务端,发起 TCP 三次握手流程,最后通过系统网络指令,查看两端 TCP 连接的真实状态。
运行结果:
从最终查看网络状态的结果就能明确看到:哪怕服务端代码全程没有调用accept函数,客户端和服务端依旧完整走完了三次握手,内核层面 TCP 连接已经成功建立,连接状态正常显示为ESTABLISHED已连接状态。
这个实验印证了我们之前的核心结论:TCP 三次握手、TCP 连接在内核层面的建立,全都是操作系统内核 TCP 传输层自主完成,和上层应用层有没有调用 accept 没有关系。accept 不负责建立连接,它只是在内核已经把连接握手完成之后,去内核队列里取出这条就绪连接、生成新的通信文件描述符返回给上层应用,让用户代码可以操作这条连接收发数据而已。哪怕应用层一直不调用 accept,底层 TCP 三次握手、连接建立也会正常完成。
listen() 的第二个参数 backlog
之前在套接字编程时我们在编写服务端代码时服务端的 listen() 套接字的第二个参数我们并没有过多解释,因为涉及到了现在的知识,现在学习了这些知识后我们再来回看 listen() 的第二个参数backlog:


接着上面的实验结论:就算服务端不调用 accept,客户端依旧可以和服务端完成三次握手,成功建立 ESTABLISHED 状态的 TCP 连接。这些在内核里握手完成、但是还没被 accept 取到应用层的连接,就全部存放在全连接队列当中。
backlog参数本质就是用来限制这个全连接队列的最大长度(Linux 规定:队列最大容量 = backlog + 1),系统默认常用值一般为 32,我们代码里用的 10 作为赋值。全连接队列里存放的,都是三次握手已经全部完成、已经建立好连接,但是上层应用还没通过 accept 取出、还没分配 connfd 交给用户使用的连接。
全连接队列不能太短:不然大量客户端同时发起连接,三次握手瞬间完成后,队列很快就会塞满,新的连接就无法正常建立。
全连接队列 也不能太长:不然内核会堆积大量这种连接,导致占用大量系统内存资源,拖垮服务器性能,所以全连接队列默认长度都较短。
所以全连接队列的核心作用就是管控服务器并发连接上限,保障服务器稳定满载运行,不会被海量突发连接冲垮。
这里再补充一个半连接队列,半连接队列中存放的是只走完一次握手、收到 SYN 报文,还没完成完整三次握手的连接;而 backlog 管控的,永远是已经握手完毕的全连接队列。
CLOSE_WAIT状态 和 TIME_WAIT状态

CLOSE_WAIT
CLOSE_WAIT 称为被动关闭等待状态,别人主动跟我断开连接,我已经应答确认了,但我自己还没执行断开操作的中间状态
在四次挥手流程里,被动关闭的一方,收到对方发来的 FIN 断开报文后,立刻回复 ACK 确认应答,回复完这份 ACK 之后自身状态就会进入 CLOSE_WAIT 被动关闭等待状态。此时对方到自己的单向写通道已经关闭,但自己还有剩余数据可以发给对方,暂时不会主动发起 FIN 断开。
如果服务器出现大量堆积 CLOSE_WAIT 状态连接,就说明服务端代码存在 BUG。
TIME_WAIT :

TIME_WAIT 称为超时 等待状态,在 TCP 四次挥手流程里,主动发起断开连接的一方,发送完最后一轮 ACK 应答报文之后,就会立刻进入TIME_WAIT 超时等待状态;而被动断开连接的一方,收到这个 ACK 之后就会直接关闭连接、释放所有资源。
这个状态的核心作用,是 TCP 协议强制主动关闭方多等待一段固定时长,用来兜底保障:防止四次挥手最后一个 ACK 报文在网络中丢失、没有成功送达对端,同时避免网络里残留的老旧迟到报文,干扰后续建立的 TCP 连接。
当一个连接处于 TIME_WAIT 状态时,算不算关闭?
不算。因为这条连接绑定的 IP + 端口,依旧被操作系统锁定占用,无法被其他连接复用,这也是 TCP 默认不允许端口地址复用的原因。也正是因为端口被系统独占锁定,如果这时我们重启服务程序想要再次绑定同一个端口,就会直接报错端口绑定失败。
解决这个问题,我们就通过 setsockopt 接口,设置 SO_REUSEADDR 地址复用选项。相当于告诉内核:跳过 TIME_WAIT 的安全保护机制,允许直接复用处于等待状态的端口地址。设置之后服务重启,就能立刻绑定同一个端口正常运行,不用等待 TIME_WAIT 超时结束。
MSL
主动关闭连接的一方,必须进行超时等待,才能从 TIME_WAIT 状态切换为 CLOSED 彻底关闭。那这里的超时等待怎么理解? 这里就引出了一个概念 : MSL
MSL 全称 Maximum Segment Lifetime,它是报文最大生存时间,是 TCP 协议里的核心时间定义,代表一个 TCP 报文在整个网络中能存活、传输的最长时间。Linux 系统里,这个默认时长一般固定为 30 秒。
主动关闭连接的一方必须等待 2 个 MSL时长,才会从 TIME_WAIT 状态切换为 CLOSED 关闭状态。首先是为了保障四次挥手断开流程完整可靠地闭环完成。1 个 MSL 的等待时间,专门用来观测对端是否会重传 FIN 断开报文,如果这段时间内对方没有重复发送 FIN,就说明主动关闭方发出的最后一轮 ACK 应答已经成功送达,四次挥手正常完结;如果对方重复重传 FIN,就代表收尾 ACK 在网络中丢失,主动端可以及时重传 ACK,补齐断开流程。而额外 1 个 MSL 时长,则覆盖报文在网络往返的最大耗时,完整兜底整条链路的极限传输延迟。同时 2MSL 等待时间,还能让这条旧 TCP 连接所有散落在网络中滞留、迟到的老旧报文全部超时失效、自然消散,彻底清空网络里的残留数据包,避免这些历史脏报文干扰后续复用同一端口新建的 TCP 连接,保障新旧连接数据不会错乱混淆。
FIN_WAIT_1状态 和 FIN_WAIT_2状态

FIN_WAIT_1
FIN_WAIT_1 叫作终止等待 1 状态 ,是主动关闭方调用 close(),刚发出 FIN 断开报文之后,这条连接立刻从 ESTABLISHED 变成 FIN_WAIT_1,意思就是我已经跟你说我要断开、不再发业务数据了,正在等你回复 ACK 确认。
FIN_WAIT_2
FIN_WAIT_2 叫作终止等待 2 状态 ,主动关闭方收到了对方发来的 ACK 应答报文,连接状态立刻从 FIN_WAIT_1 切换成 FIN_WAIT_2。此时就表明单向通道已经关闭(我→你这条路断了,我再也不发业务数据),此时我还能正常接收你剩下的数据、以及你后面发来的 FIN 断开报文,只是自己不再往外发数据了。
我们上面说的**这些状态,本质是这条连接 (单条 TCP 双向连接) 的状态,不是某个主机/进程的状态。**一台服务器可以同时维护成百上千条独立 TCP 连接,每条连接各自拥有各自独立的状态、收发缓冲区与生命周期。SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT、CLOSE_WAIT、TIME_WAIT 等所有状态,都只绑定对应这一条连接。我们上面说的 "客户端处于 SYN_SENT、服务器处于 TIME_WAIT",只是简化通俗说法,严谨来讲并不准确。只是两端视角不一样、步骤不一样,所以状态不一样。
一个 TCP 连接 == 一个连着客户端、服务端的双向管道
四、流量控制
TCP 流量控制,本质是接收端通过告知自身接收窗口大小,限制发送端的数据发送速率,避免发送方发送数据过快,导致接收端接收缓冲区溢出、数据丢失,从而实现两端传输速率匹配,保障 TCP 传输稳定可靠。

发送主机 A 首先向接收主机 B 发送序号 1~1000 的数据报文,主机 B 成功接收后,回复确认应答,告知下一个期待接收的字节序号为 1001,同时在 TCP 报文头部告知自身 16 位窗口大小为 3000 字节,也就是此时自身接收缓冲区可容纳的数据总量是 3000 字节。随后发送方按照接收窗口限制,连续分批发送 1001~2000、2001~3000、3001~4000 批次数据,接收端每接收一批数据,都会同步返回对应确认应答,并同步更新自身期待接收的下一字节序号。随着数据不断写入,接收主机 B 的接收缓冲区被逐渐占满,16 位窗口大小最终变为 0。此时通过最后一个应答报文主机 A 也就知道了主机 B 的缓冲区容量为 0 了。
当接收窗口大小为 0 时,就代表接收端暂时无法接收数据了,发送方主机 A 就会停止发送新的数据报文并进行一个固定时间的等待,这个时间是TCP 约定的固定超时时间 ,如果等待超时后,接收端依旧没有更新窗口状态,这时发送主机 A 就会主动发送窗口探测报文,去询问接收端当前剩余的可用接收窗口大小。此时接收端会回复并更新自己的窗口大小,同步最新的缓冲区窗口大小给发送方。
这里注意的是接收窗口为 0 仅代表无法接收业务数据载荷,但是依旧可以正常接收 TCP 报头。同时如果窗口更新通知报文在网络传输中丢失,会直接导致两端通信卡死,窗口探测机制就可以周期性轮询窗口状态,保障连接通信不会永久中断。
还有个问题就是,16 位窗口大小,16 位数字表示 65535。也就意味着 TCP 窗口最大是 65535 字节吗?
当然不是,16 位的窗口大小的最大值只能表示 65535 字节,但这远远无法满足现在高速网络的传输需求,所以 TCP 专门设计了窗口扩大因子选项 来突破这个限制。TCP 在三次握手建立连接的时候,会协商一个窗口扩大因子 M,实际可用的滑动窗口大小,就等于原本 16 位窗口字段的值向左偏移 M 位,也就是乘以 2 的 M 次方。通过这个机制,TCP 的实际滑动窗口上限被极大放大,不再被 65535 字节限制,完全可以适配高速、大带宽的网络传输场景。
三次握手之后,第一次发送数据时,应该发送多大的数据?
虽然是第一次发数据,但是并不是第一次发报文,三次握手时发的报头就是报文,只不过可能没带数据,但是互相发的报头里就已经交换过窗口大小了。所以三次握手之后,第一次发送数据时根据对应的窗口大小自己决定发送多大的数据。
所以三次握手除了建立连接,还有交换接收能力,支持流量控制的作用。
五,滑动窗口
问题铺垫:
**1.如果一个报文发出去了,在收到对端的应答之前,这个报文应该被发送方OS丢弃吗?**不应该被丢弃,也就是在一端在发出去和收到应答之前,会把发出去的报文临时保存起来,那保存在哪里呢?
2.我们可以根据对方的接收能力动态调整发送数据的速率,这叫流量控制,我们上面刚讲过,那流量控制具体是怎么实现的呢?
3.之前我们说过 TCP 有两种模式,第一种是发一个收一个应答,第二种是一次发多个再一次收多个应答,那第二种模式又是怎么实现的呢?
滑动窗口
要想弄清上面的几个问题,我们就必须得先明白TCP的"滑动窗口",也就是我们下来要讲的内容
什么是滑动窗口?
滑动窗口,就是发送方在自身发送缓冲区内,划出一块特殊的窗口区域。处于这个窗口内的数据,发送方无需等待对方逐一返回的确认应答报文,从而可以批量、连续向外发送数据,以此提升 TCP 整体传输效率。
滑动窗口在哪?
在发送缓冲区内,是发送缓冲区当中的一部分特定区域。
我们之前说过,内核物理层面里,TCP 发送缓冲区与接收缓冲区本质都是独立的队列结构。而因为 TCP 是面向字节流的传输协议,我们在逻辑上,可以把收发缓冲区统一抽象成线性连续的字符数组 char in/outbuffer[N] 。用数组模型理解缓冲区后,序号管理、字节流编号、数据拷贝等相关逻辑都会变得非常清晰。
以发送缓冲区举例,滑动窗口逻辑上就是一段连续数组区间,用起始下标 start 和结束下标 end 来界定范围。窗口内的数据报文可以批量先发出去,暂时不用阻塞等待对方应答;这并不代表对方不需要回复 ACK 确认,只是允许发送方不用等单条报文的应答,这样就能连续发送多段数据,优化了网络传输性能。
三个区域:

我们可以把 TCP 发送缓冲区,结合滑动窗口逻辑,划分成三个属性不同的区域:
最左侧是已确认数据区域,这片区域的数据已经收到对方的 ACK 应答,传输完成,在缓冲区里已经属于无效数据,可以被内核清理释放。
中间就是正发送区域,也就是滑动窗口本身,这片区域的数据无需等待单条应答回执,就可以批量直接发送,我们用 start_win 和 end_win 两个下标,来标记这个窗口在线性缓冲区里的起止范围。
最右侧是待发送数据区域,这片数据暂时不能对外发送,需要等待滑动窗口向右滑动、纳入窗口范围之后才能向外传输。
严格来说缓冲区末尾还存在空闲空白区域,为了简化模型、方便理解滑动逻辑,我们暂时不做额外讨论。因此整体来看,TCP 发送缓冲区本质,就是由这三段不同业务属性的数据区域,共同构成的字节流数据载体。
最开始时滑动窗口的大小是多少?
TCP 连接刚建立、第一次发送数据时滑动窗口的大小等于对端通告的 16 位接收窗口大小,与本地待发送真实数据量两者之间的较小值,也就是公式

其中 ACK-win 是三次握手阶段对端告知的自身接收窗口大小,代表了对方的接收处理能力,也就是接收端接收缓冲区能一次性承接的数据上限。这里我们默认发送方发送的数据是满的,数据量大于对方接收窗口,此时两者取最小值后,初始滑动窗口大小就等于对端应答回来的接收窗口大小。

滑动窗口的工作过程:

如上图 : 一开始主机 A 的发送滑动窗口范围是 1001 ~ 5000,总共 4 个分段,每一段对应 1000 字节数据。主机 A 先批量发送窗口内1001~2000这段数据,主机 B 成功接收后,回复确认应答报文,告知下一个期待接收的序号是 2001,同时在报文中同步自己当前剩余的接收窗口大小。当这份 ACK 确认应答回到主机 A 之后,滑动窗口的左侧边界就从 1001,向右滑动更新到 2001。此时整个窗口依旧保持对应大小,整体向后平移,继续等待后续数据发送与应答确认。
滑动窗口本身就是站在发送方视角设计的,因此我们分析时,始终以主机 A 的发送缓冲区作为参照,不用管接收方 B 的缓冲区细节。所有工作都是围绕发送方自身缓冲区完成的。而接收主机 B 只需要完成两项核心工作:一是将收到的数据写入自身接收缓冲区 ,二是在回复 ACK 确认报文时,同步告知发送方自己剩余的可用接收缓冲区大小。
确认应答里的序号,本质就是用来确认滑动窗口的左侧边界,代表该序号之前的所有数据都已经被接收端成功接收确认。而滑动窗口的右侧边界 = 最新的确认应答序号 + 对端报文里通告的接收窗口大小。
**滑动窗口的整体大小,是由对方的接收处理能力的大小决定的,是动态变化的。**如果对端通告的可用窗口大小变大,本地发送滑动窗口就随之扩大,一次可以批量发送更多数据;如果对端接收窗口变小,滑动窗口就会减小。
正是靠着这种滑动窗口跟随对端接收能力动态平移、伸缩调整的机制,TCP 才能精准管控两端的数据收发速率**,这也就是流量控制完整的底层实现逻辑。**
滑动窗口会变大吗? 会变小吗? 会不变吗? 分别是什么时候?
刚才提到了 : 如果接收方的 16 位窗口大小很大,那发送方的滑动窗口的大小就会很大;如果接收方的窗口大小不变,那发送方的滑动窗口大小就不变。 什么时候变大变小和不变,取决于发送方收到的应答报文中表明自己接收方接收能力的窗口大小相较上一次窗口大小是变大了还是变小了,
滑动窗口左侧总是会进行右移,因为序号总要被确认,至于右侧,也就是从左至右滑动窗口囊括的范围的变大变小,取决于对方接收缓冲区中的剩余空间的大小,可以理解为对方上层应用层取了多少数据,如果对方接收缓冲区中剩余空间变大了,就说明对方的接受能力变大,16 位窗口大小也就变大了,因此对应的滑动窗口也就在这时变大,反之就会变小或不变。
滑动窗口可能回向"左"滑动吗?
滑动窗口不可能左移,要么向右移动,要么不移动 (因为对方接收能力变小导致滑动窗口的右侧可能不变),但是绝对不会左移。
工作过程:
滑动窗口的工作过程可以分两个工作过程理解 :
1.正常工作过程:
上面我们说的都是正常工作流程。就是发送方按照接收端告知的接收能力的大小,正常批量发送数据。每收到一次 ACK 确认应答,窗口左侧边界就会向右移;窗口整体大小要么跟随对方接收空间变大而变大,要么保持不变,整套传输平稳有序推进,不会出现异常等待与重传行为。
2.异常工作过程:中途出现丢包的情况
也就是说上面我们证明了因为滑动窗口的存在,发送方一次能发送多个数据报文,那如果这多个报文在发送过程中出现了丢包问题呢?该如何进行重传?这里也分两种情况讨论
1.第一种情况就是发送方发出去的数据包都送到了接收端,只是接收端回复回来的 ACK 确认应答报文,在网络传输途中丢失了。

比如上图,主机 A 把 1~6000 字节的数据全都发送完成,本该依次收到 1001、2001、3001、4001、5001、6001 确认应答序号,但中途很多 ACK 应答报文丢失,只收到了 2001、5001、6001 这几个确认序号。此时需要不要重传呢?
不需要重传 。因为 **TCP 确认序号的规则就是:只要收到了 6001 这个确认序号,就代表 6001 之前所有字节的数据,接收端都已经完整收到了。**只是中间部分的应答 ACK 报文在路上丢了而已,后续通过高阶的确认序号,也能确认前面的所有数据都已安全送达。简单总结就是:滑动窗口批量发送模式,天生就允许少量 ACK 应答报文丢失,不会因此触发不必要的数据重传。
2. 第二种异常情况就是发送方发出去的数据报文直接就在网络传输途中丢失,数据根本没有抵达接收端。

如上图,主机 A 一次性批量发送了 1~1000、1001~2000、2001~3000 一直到 6001~7000 的全部数据,其中只有 1001~2000 这段数据报文在中途丢失,其余所有后续报文都正常送达了接收端。接收端正常收到 1~1000 的数据后,回复了序号为 1001 的确认应答。按照正常传输逻辑,接下来接收端应该回复 2001、3001 、4001...依次递增的 ACK 确认应答序号,但因为1001~2000 报文缺失了,哪怕后面 2001~7000 的数据都完整收到,后续所有的应答报文中的确认序号也只能统一回复的 1001。
当发送方连续多次收到序号同为 1001 的重复确认应答时,就能立刻判断出 1001~2000 这段数据报文发生了丢包。如果这段数据正常送达,接收端一定会回复递增的确认应答序号,而不是反复确认同一个 1001 序号。哪怕后续还存在有报文的丢失,接收端依旧只会持续回复最小的 1001,因此发送方可以精准定位丢包位置,优先重传补发 1001~2000 这段缺失报文。只有这段数据补发成功、被正常确认后,接收序号才会向后更新,后续数据才能继续正常确认传输。
快重传
这里补充滑动窗口下的重传规则:发送方只要连续收到3 次一模一样序号的确认应答报文,就会立刻对丢失的数据报文进行重传补发。
这种触发重传的方式不用等待超时计时结束,和超时重传不是一个概念,是一套独立的重传方案,也就是 TCP 里的快重传(高速重发控制)。就像之前的例子,发送方连续收到 3 次序号为 1001 的重复 ACK,就马上重传 1001~2000 这段丢失数据;等这段数据正常送达后,接收端就会直接返回最新的 7001 序号应答,因为 2001~7000 的数据早就到达、存放在接收缓冲区里了。
那这个快重传和超时重传有什么关系呢?
这里我们接着上面的例子,如果 6001~7000 是窗口里最后一个要发送的数据报文,前面所有报文都正常收到了 ACK 应答,唯独这最后一段中途丢包。因为它后面没有数据报文了,发送方也就收不到 3 次重复的确认应答,快重传机制就无法触发。这个时候超时重传就会作为兜底方案生效,发送方一看在规定的超时时间间隔内都没收到最后一个数据报文的应答报文,就会触发超时重传,补发最后一个6001-7000的数据报文。
换句话说,超时重传是快重传的兜底。
基于滑动窗口的丢包情况
结合上面滑动窗口批量连续发送报文的场景,我们又可以把窗口内丢包细化为三种典型情况 :
1. 最左侧丢失
第一种滑动窗口丢包情况:窗口最左侧报文丢失。

如上图,发送方窗口内 2001~3000、3001~4000、4001~5000 的数据都正常送达接收端,唯独窗口最开头的 1001~2000 报文中途丢失。 此时哪怕后续报文都接收成功,接收端所有回复的确认应答序号也只会是 1001,发送方通过接收到重复的相同 ACK 确认序号 1001,就能立刻判定 1001~2000 这段报文发生丢失,随即触发快重传或是超时重传,马上补发这段缺失的数据。当补发的报文被接收端成功确认后,发送方收到的应答序号就会直接跳转为 5001。同时这份应答报文里会同步告知接收端最新可用窗口大小,例如 2000 字节,发送方就会同步更新自身滑动窗口:窗口左边界移动到 5001,整体窗口容量变为 2000 字节,新的滑动窗口范围就是 5001~7001。
当滑动窗口出现最左侧报文丢失时,TCP 会通过两套机制依次完成报文重传补发。发送方连续收到三次序号完全相同的重复确认应答报文,就会立刻触发快重传机制,无需等待超时计时结束,马上对丢失的最左侧数据报文进行重传补发,这一机制响应速度更快,也不会大幅降低网络传输速率。而如果重复 ACK 应答数量不足、无法触发快重传,此时等待超时时间到期后,就会启动超时重传作为兜底方案,强制补发丢失报文。
本质上快重传优先响应有序丢包异常,超时重传为所有异常场景兜底保障,二者配合就可以完整处理滑动窗口左侧丢包后的全部重传流程。
接着我们解答最开始的问题:报文在发送出去后、还没收到对应确认应答的这段时间里,数据会临时保存在哪里? **答案就是这些未被确认的报文,全程暂存在发送方的滑动窗口内部。只有当报文收到对应 ACK 应答后,就会被划分到窗口左侧已确认数据区域时,**这段缓存数据才会被清理释放。也正是依靠滑动窗口的临时缓存能力,TCP 才能支持丢包后的重传补发,保障有序可靠传输。
所以报文从滑动窗口区域被划分到左侧的已确认数据区,本质就是删除这个报文
2. 中间丢失
接下来是第二种滑动窗口报文丢失情况:窗口中间数据丢失:

如上图,窗口内 3001~4000 这段报文在传输途中丢失,而它前面 1001~2000、2001~3000,以及后面 4001~5000 的报文都正常送达了接收端。接收端针对前两段正常数据,依次回复序号为 2001、3001 的确认应答;哪怕末尾报文正常接收,也依旧只会回复 3001 的确认序号,缺失的中间数据报文不会产生的应答。
发送方收到前两段数据的 ACK 应答后,就会判定这两段传输正常,滑动窗口左侧边界连续向右滑动两次,1001~2001 和 2001~3001 的数据报文被移至滑动窗口左侧的已确认数据区。当窗口左边界移动到 3001 后,就无法继续向右滑动了。因为 TCP 滑动窗口遵循有序确认规则,不会跳过任何未被确认的报文,只要 3001 对应的报文没有收到有效应答,窗口就会卡在这个位置无法前进。
此时滑动窗口最新的左侧边界就变成了 3001,原本的中间丢包问题,就直接转化成了我们上一种讲过的窗口最左侧报文丢失问题。后续就可以沿用左侧丢包的处理逻辑,触发快重传或是超时重传,完成这段缺失报文的补发重传。
3. 最右侧丢失
接下来是第三种滑动窗口丢包情况:窗口最右侧报文丢失:

如上图,窗口末尾 4001~5001 这段数据丢失,随着前面所有报文陆续被确认、窗口不断向右滑动,原本在窗口最右侧的丢失报文,最终会慢慢变成滑动窗口新的左侧边界。因此窗口最右侧丢包,本质上依旧会转化成我们之前讲过的窗口最左侧报文丢失问题。
由此我们可以总结:滑动窗口内所有位置的丢包场景,最终都能归约为窗口最左侧丢包问题,中间丢失、右侧丢失都只是左侧丢包的变种。
滑动窗口会滑越界吗?
滑动窗口是存在越界可能的。因此我们可以在逻辑上,把滑动窗口对应的字节流数组理解为环形数组,序号起始位置可以通过模运算循环表示。而内核底层实际依旧是队列结构,只是通过对应的运算逻辑,封装实现成了环形队列的效果。
六、总结
本文深入解析了TCP协议的五大核心机制:确认应答(ACK)、超时重传、连接管理(三次握手/四次挥手)、流量控制和滑动窗口。ACK机制通过序号确认保障数据可靠传输;超时重传动态调整等待时间处理丢包;连接管理详细阐述了三次握手建立连接和四次挥手断开连接的完整流程;流量控制通过窗口大小调节发送速率;滑动窗口则实现了高效批量数据传输和丢包处理。文章还探讨了TCP状态转换、半/全连接队列、TIME_WAIT状态等关键概念,并通过实验验证了accept与三次握手的关系,全面展现了TCP可靠传输的实现原理。
谢谢大家的观看!




我们保留 TCP 服务端完整的 socket创建套接字、bind绑定端口、listen开启端口监听,直接把业务代码里调用 accept 接收连接的逻辑屏蔽掉、不执行 accept,之后正常启动运行这个服务端程序。接着我们用客户端去主动访问这台服务端,发起 TCP 三次握手流程,最后通过系统网络指令,查看两端 TCP 连接的真实状态。
