TCP 详解:从三次握手到可靠传输、滑动窗口和四次挥手

前言

IP 负责把包送到目标主机,TCP 负责把数据可靠送到目标主机上的具体进程。

一句话:

TCP 在不可靠的 IP 之上,提供可靠、有序、面向连接的字节流传输。

这篇文章围绕 TCP 的完整生命周期展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
端口定位进程
↓
三次握手建立连接
↓
序号、确认和重传保证可靠
↓
滑动窗口提高效率
↓
流量控制保护接收方
↓
拥塞控制保护网络
↓
四次挥手关闭连接

1. TCP 不只是建立连接

很多人第一次学 TCP,会把注意力放在三次握手上,于是误以为:

1
TCP 只是用来建立连接。

其实不是。

三次握手只是 TCP 生命周期的开始。TCP 更主要的工作是:

1
在连接建立后,持续承载应用层字节流。

比如:

  • HTTP 请求
  • HTTP 响应
  • TLS 握手数据
  • 加密后的 HTTPS 数据

这些都可以通过 TCP 传输。

所以 TCP 做两类事:

1
2
1. 控制连接:三次握手、四次挥手、ACK、窗口等。
2. 传输数据:把应用层字节流切成一段一段发送。

这些一段一段的数据通常叫:

1
TCP 段 / TCP 报文段

2. 端口:找到目标进程

IP 地址只能找到目标主机。

但一台主机上可能同时跑很多服务:

1
2
3
4
5
80:HTTP
443:HTTPS
22:SSH
3306:MySQL
6379:Redis

TCP 通过端口找到具体进程。

比如访问:

1
https://example.com

DNS 解析后得到服务器 IP,浏览器会连接:

1
目标 IP:443

客户端自己也会分配一个临时端口:

1
192.168.1.10:52341 -> 93.184.216.34:443

一条 TCP 连接通常可以用五元组标识:

1
2
3
4
5
源 IP
源端口
目标 IP
目标端口
协议 TCP

所以:

1
2
IP 找主机
端口找进程

3. 三次握手:建立连接

TCP 三次握手不是为了发送 HTTP,而是为了建立 TCP 连接。

典型过程是:

1
2
3
客户端 -> 服务端:SYN,seq = x
服务端 -> 客户端:SYN + ACK,seq = y,ack = x + 1
客户端 -> 服务端:ACK,ack = y + 1

三次握手确认了三件事:

1
2
3
双方都能发送
双方都能接收
双方同步了初始序号

第一次 SYN:

1
客户端说:我想建立连接,我的初始序号是 x。

服务端收到后能确认:

1
2
客户端发送能力正常
服务端接收能力正常

第二次 SYN + ACK:

1
服务端说:我收到了你的 SYN,我的初始序号是 y。

客户端收到后能确认:

1
2
服务端发送能力正常
客户端接收能力正常

第三次 ACK:

1
客户端说:我收到了你的 SYN,我确认你的序号 y。

服务端收到后,才确认客户端收到了自己的回应。

所以三次握手的核心目的:

1
2
3
确认双方收发能力
同步双方初始序号
避免旧连接请求造成错误连接

4. 三次握手有没有 HTTP 数据

通常没有。

三次握手传的是 TCP 控制报文:

1
2
3
SYN
SYN + ACK
ACK

普通 HTTP 流程是:

1
2
3
4
5
TCP 三次握手
↓
HTTP 请求
↓
HTTP 响应

HTTPS 流程是:

1
2
3
4
5
6
7
TCP 三次握手
↓
TLS 握手
↓
加密后的 HTTP 请求
↓
加密后的 HTTP 响应

但 TCP 握手包本身也要真实经过网络:

1
2
3
4
5
6
7
TCP SYN
↓
IP
↓
MAC 帧
↓
物理层信号

也就是说:

TCP 握手不是应用层数据,但它仍然要被 IP、数据链路层和物理层承载。


5. TCP 为什么会有报文长度

TCP 不是只建连接,它还要传输应用层数据。

应用层可能给 TCP 一大段数据:

1
HTTP 响应体:1MB

TCP 不会把 1MB 一口气塞进一个包里发出去,因为底层链路有 MTU 限制。

以太网常见 MTU 是:

1
1500 字节

IP 头和 TCP 头通常各占:

1
2
IP 头:20 字节
TCP 头:20 字节

所以一个 TCP 段里能装的应用数据通常大概是:

1
1500 - 20 - 20 = 1460 字节

这个值常叫:

1
MSS,Maximum Segment Size

于是 TCP 会把应用层字节流切成多个 TCP 段:

1
2
3
第 1 段:Seq = 1000,Len = 1460
第 2 段:Seq = 2460,Len = 1460
第 3 段:Seq = 3920,Len = 1460

这里的 Len 就是这段 TCP 承载了多少字节的应用数据。

三次握手里的 SYN、ACK 通常没有应用数据,所以数据长度通常是 0。

但注意:

SYN 和 FIN 虽然不带应用数据,但会消耗一个序号。


6. TCP 是字节流协议

TCP 不关心 HTTP 是一条消息还是两条消息。

它只看到一串字节:

1
GET / HTTP/1.1\r\nHost: example.com\r\n\r\n

然后根据 MSS、窗口、拥塞控制等情况,切成合适大小的 TCP 段发送。

所以 TCP 的序号不是“第几个包”,而是:

1
字节序号

比如一段数据:

1
2
Seq = 1000
Len = 500

表示:

1
这段数据覆盖字节 1000 ~ 1499

7. ACK 与累计确认

ACK 是接收方告诉发送方:

1
我下一个希望收到哪个字节。

比如接收方回:

1
ACK = 1500

意思不是“我收到了第 1500 个字节”,而是:

1
2
1500 之前的字节我都收到了。
下一个请从 1500 开始发。

这叫累计确认。

比如发送方连续发:

1
2
3
Seq = 1000, Len = 500   覆盖 1000 ~ 1499
Seq = 1500, Len = 500 覆盖 1500 ~ 1999
Seq = 2000, Len = 500 覆盖 2000 ~ 2499

如果接收方都收到了,可以直接回:

1
ACK = 2500

表示:

1
2500 之前的字节都收到了。

8. 丢包、重传、乱序和去重

IP 层是不可靠的,可能丢包、乱序、重复。

TCP 通过序号和 ACK 处理这些问题。

如果中间一段丢了:

1
2
3
1000 ~ 1499 到了
1500 ~ 1999 丢了
2000 ~ 2499 到了

接收方不能确认到 2500,只能继续回:

1
ACK = 1500

意思是:

1
我还在等 1500 开始的数据。

发送方发现一直收到重复 ACK,或者等超时了,就会重传缺失的数据。

常见重传机制:

1
2
超时重传:一段时间没收到 ACK,就重传。
快速重传:连续收到多个重复 ACK,不等超时,提前重传。

乱序数据可以先缓存,等缺失部分到齐后再按顺序交给应用层。

重复数据可以根据序号识别并丢弃。

所以 TCP 可靠性的本质是:

1
2
3
4
5
Seq 标记我发到哪
ACK 告诉你我收到哪
丢了就重传
乱了就重排
重了就丢弃

9. 传输数据不是“两次握手”

三次握手之后,TCP 连接已经建立。

之后传输数据时,确实经常能看到这样的节奏:

1
2
发送方 -> 接收方:DATA
接收方 -> 发送方:ACK

但这不叫“两次握手”。

握手是连接建立阶段的动作:

1
2
3
SYN
SYN + ACK
ACK

数据阶段是在已建立连接上发送数据,并用 ACK 确认对方收到哪里。

比如客户端发:

1
2
Seq = 1000
Len = 500

服务端回:

1
ACK = 1500

意思是:

1
1000 ~ 1499 我收到了,下一个请从 1500 开始。

这只是确认机制,不是重新建立连接。

并且 TCP 不是每发一段都必须等 ACK 才能发下一段。只要窗口允许,发送方可以连续发送多段数据,接收方再用 ACK 累计确认。

所以:

三次握手建立连接;数据传输阶段是 DATA 和 ACK 持续流动。


10. TCP 是全双工的

TCP 建立连接后,客户端和服务端可以同时发送数据。

因为 TCP 是全双工:

1
2
3
客户端 -> 服务端 可以发
服务端 -> 客户端 也可以发
两个方向可以同时进行

这两个方向各自有独立的序号空间。

比如三次握手时:

1
2
客户端初始序号 = 1000
服务端初始序号 = 8000

客户端发数据:

1
2
3
客户端 -> 服务端
Seq = 1001
Len = 500

服务端同时也可以发数据:

1
2
3
服务端 -> 客户端
Seq = 8001
Len = 1200

这不会冲突,因为:

1
2
Seq 管自己这个方向发出的字节位置
ACK 管确认对方那个方向收到的位置

服务端发数据时,也可以顺便确认客户端的数据:

1
2
3
4
服务端 -> 客户端
Seq = 8001
Len = 1200
ACK = 1501

意思是:

1
2
我发给你 1200 字节数据。
同时确认你发来的 1001 ~ 1500 我收到了。

这叫:

1
捎带确认

普通 HTTP/1.1 看起来像:

1
2
客户端请求
服务端响应

但这是 HTTP 的请求-响应模型,不代表 TCP 是半双工。

WebSocket 更能体现 TCP 的全双工能力:

1
2
客户端可以随时发消息
服务端也可以随时发消息

底层 TCP 支持双方同时传输。


11. 滑动窗口:提高效率

如果 TCP 每发一段数据都等一个 ACK,效率会很低:

1
2
3
4
发 1 段
等 ACK
再发 1 段
再等 ACK

滑动窗口允许发送方在没收到 ACK 前,连续发送一定范围内的数据。

比如窗口大小是 3000 字节,可以连续发:

1
2
3
1000 ~ 1999
2000 ~ 2999
3000 ~ 3999

收到:

1
ACK = 3000

说明 3000 之前都收到了,窗口就可以继续往后滑:

1
2
4000 ~ 4999
5000 ~ 5999

所以滑动窗口解决的是:

1
既保证可靠,又提高吞吐量。

12. 流量控制:别撑爆接收方

流量控制关注的是:

1
接收方还能不能接得住?

接收方会告诉发送方:

1
我现在还能接多少数据。

这个值叫:

1
接收窗口 rwnd

比如:

1
rwnd = 8000

意思是:

1
你最多还能发 8000 字节,别再多了。

流量控制保护的是接收方。


13. 拥塞控制:别撑爆网络

拥塞控制关注的是:

1
网络还能不能扛得住?

接收方可能很强,但中间网络可能堵了:

1
2
3
4
路由器队列满
链路带宽不足
丢包增加
延迟升高

TCP 会根据 ACK、丢包、延迟等信号动态调整发送速度。

这个控制量叫:

1
拥塞窗口 cwnd

最终发送方真正能发多少,取决于:

1
min(rwnd, cwnd)

常见过程:

1
2
3
慢启动:从小窗口开始,指数增长,先试探网络。
拥塞避免:达到阈值后,线性增长。
拥塞发生:丢包时降低发送速度。

流量控制和拥塞控制的区别:

对比 流量控制 拥塞控制
关注对象 接收方 网络
问题 接收方缓冲区会不会满 中间链路/路由器会不会堵
控制依据 接收窗口 rwnd 拥塞窗口 cwnd
目的 别撑爆接收方 别撑爆网络

一句话:

1
2
流量控制看对方吃不吃得下
拥塞控制看路上堵不堵

14. 四次挥手:关闭连接

TCP 是全双工的:

1
2
客户端可以发给服务端
服务端也可以发给客户端

关闭时要分别确认两个方向都不再发送数据。

假设客户端主动关闭:

1
2
3
4
客户端 -> 服务端:FIN
服务端 -> 客户端:ACK
服务端 -> 客户端:FIN
客户端 -> 服务端:ACK

含义是:

1
2
3
4
客户端:我不发了
服务端:知道了
服务端:我也不发了
客户端:知道了

有时第二次 ACK 和第三次 FIN 可以合并,看起来像三次,但逻辑上仍然是四个动作:

1
2
确认客户端关闭
服务端自己关闭

15. TIME_WAIT 是什么

主动关闭的一方在发完最后一个 ACK 后,不会立刻消失,而是进入:

1
TIME_WAIT

主要原因有两个:

1
2
确保最后一个 ACK 能到达。
让旧连接的残留包自然过期。

如果最后一个 ACK 丢了,服务端会重发 FIN。

主动关闭方在 TIME_WAIT 状态下还能再次回复 ACK。

所以:

1
谁主动关闭,谁通常进入 TIME_WAIT。

16. HTTP 响应结束不等于 TCP 关闭

HTTP 请求结束,不一定马上 TCP 四次挥手。

因为 HTTP/1.1 支持长连接:

1
一个 TCP 连接可以复用多个 HTTP 请求。

可能是:

1
2
3
4
5
6
7
8
请求 A
响应 A
请求 B
响应 B
请求 C
响应 C
一段时间不用了
再关闭 TCP 连接

所以:

1
HTTP 响应结束 ≠ TCP 连接马上关闭

17. 一句话总结

TCP 可以用这条线记:

1
2
3
4
5
6
7
8
9
10
11
端口:找到目标进程
三次握手:建立连接,确认双方能收能发
序号 / ACK:确认数据位置
重传:丢了能补
乱序重排 / 去重:保证应用层看到有序字节流
DATA + ACK:持续传输和确认,不是重新握手
全双工:两边可以同时发送,ACK 可以捎带数据
滑动窗口:提高效率
流量控制:别撑爆接收方
拥塞控制:别撑爆网络
四次挥手:两个方向分别关闭

最短总结:

TCP 不是靠网络本身可靠,而是靠连接、序号、确认、重传、窗口和拥塞控制,在不可靠的 IP 之上提供可靠有序的字节流。