1. 连接的本质:TCP建立的不是"通路",而是"共识"
1.1 TCP连接到底是个什么东西
说起TCP连接,很多人脑子里蹦出来的第一幅画面,是一条从客户端到服务器的"管道",数据在里面哗哗地流。这个画面其实是个误导。TCP连接并不像管道那样是一个物理存在的实体,它本质上是一组状态——通信双方为了保持可靠传输,各自在内存里维护的一套数据结构。
具体来说,一条TCP连接由四元组唯一标识:源IP、源端口、目的IP、目的端口。只要这四项确定,便唯一确定了这条连接。通信双方在自己的协议栈里为这条连接维护着发送缓冲区、接收缓冲区、当前序列号、确认号、窗口大小、拥塞窗口等等一系列信息。这些数据合在一起,才构成了"连接"这个实体。
所以当你用netstat看到一条ESTABLISHED状态的时候,那并不是说网络里真有根管子连着两台机器,而是说两台主机各自记住了对方的存在,并且各自准备好了一个账本,接下来要交换的数据都需要在这个账本上记账。这才是"连接"的本质。
搞懂这一点,你才能理解后面所有的细节:三次握手是为了让双方统一账本,四次挥手是为了让双方确认账目结清,TIME_WAIT是为了让最后的账目在网络上彻底消失。整个TCP协议的核心逻辑,就藏在这几个问题里面。
1.2 建立连接之前,双方的初始状态
在三次握手开始之前,通信双方的状态是这样的:
- 客户端:处于CLOSED状态,主动调用connect()函数发起连接。
- 服务器:处于LISTEN状态,通过socket()、bind()、listen()完成监听,等待客户端的到来。
这里面有一个很关键但常被忽略的细节:客户端调用connect()的时候,它还有一个隐含的动作,就是为自己挑选一个本地端口。如果你在代码里没有显式bind端口,操作系统会从临时端口范围内自动选一个。这个选择过程对理解后续问题非常重要,因为Tcp的每一个连接都必须由四元组唯一确定,而客户端临时端口就是其中一个可变维度。
服务器端LISTEN状态背后的数据结构同样值得一提。内核为每个监听socket维护了两个队列:半连接队列(SYN队列)和全连接队列(Accept队列)。半连接队列存放已完成第一次握手、等待第二次握手的客户端信息,全连接队列存放已完成三次握手、等待应用层accept()的连接。这两个队列溢出,是生产环境中最常见的建连问题来源之一,我后面会单独展开讲。
1.3 从报文视角看一次连接建立
在真正展开三次握手之前,建议你先有一种"看懂抓包"的能力。无论你用tcpdump还是Wireshark,TCP报文头的关键字段就那么几个:
| 字段 | 作用 | 握手中的典型特征 |
|---|---|---|
| Source Port / Dest Port | 标识通信两端 | 服务器端固定端口,客户端为临时端口 |
| Sequence Number | 本端发送数据的字节流编号 | 初始值为随机数ISN |
| Acknowledgment Number | 确认对端数据,表示"我期望收到对方的下一个字节编号" | 等于对方的序列号+1 |
| Flags | 控制位:SYN、ACK、FIN、RST、PSH、URG | 握手期间SYN和ACK交替置位 |
| Window Size | 声明自身的接收窗口剩余空间 | 反映接收端的处理能力 |
三次握手的过程用一句话说就是:客户端发SYN问"在吗",服务器回SYN+ACK说"在,你那边能听到我吗",客户端再回ACK说"能听到,咱们开始聊吧"。之所以需要三个来回而不是两个,是因为TCP要保证双方的收发方向都能正常通行,并且要协商好各自的初始序列号。这个"为什么是三次"的问题,值得多花点时间理解。
1.4 为什么不是两次握手
很多人问过一个问题:既然第二次握手已经既确认了客户端的SYN、又带上了自己的SYN,为什么客户端还要再回一个ACK?如果省掉第三次握手,服务器发出SYN+ACK后就直接认为连接建立,会出什么问题?
问题的关键在于历史重复SYN。考虑一个典型的网络延迟场景:客户端先发了一个SYN报文,因为网络拥堵迟迟未到;客户端等不及,重传了一个新的SYN;旧的SYN后来还是到达了服务器。此时服务器如果第一次收到旧SYN就回SYN+ACK并建立连接,它并不知道这个SYN已经过期。客户端收到这个SYN+ACK后,通过对比序列号可以发现自己期望的编号对不上,就会发RST断开这个"幽灵连接"。但如果服务器在收到旧SYN时已经分配了连接资源,那么这次错误的资源分配虽然最终会被清理,期间却可能造成端口和内存的浪费,甚至在SYN泛洪场景下放大攻击效果。
而两次握手的致命问题在于:服务器无法区分"客户端确实收到了我的SYN+ACK"和"客户端根本没收到"。它单方面建立连接后,会一直等待客户端发送数据,但客户端可能压根不知道这条连接的存在。结果是服务器资源被白白占用。三次握手通过客户端的最终ACK确认,确保双方都对"连接已建立"这件事达成共识,不让任何一方单方面地以为连接可用。
这个"双方达成共识"的设计理念,贯穿TCP连接全生命周期。理解了这个底层逻辑,再来逐一拆解每一步的报文细节,就会顺理成章。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三次握手:每一步都有不可替代的使命
2.1 第一次握手:客户端发出SYN报文
客户端调用connect()后,内核会构造一个SYN报文发出。这个报文有以下几个特征:
- Flags中SYN置1
- 序列号为一个随机初始值,记作client_isn(Initial Sequence Number)
- 不携带应用数据(但序列号仍会占一个编号,这一点常被忽视)
- 源端口是临时端口,目的端口是服务器监听的端口
为什么初始序列号要随机,而不是固定从0开始?这其实是出于安全考虑。如果序列号可预测,攻击者可以伪造一个合法的RST报文,把一条正常连接切断。RFC 793里最初的实现确实是从0开始的,后来发现的这个安全漏洞直接推动了随机初始序列号机制的普及。现在的主流操作系统都采用随机或半随机的ISN生成算法。
另一个容易忽略的细节是:SYN报文虽然不携带应用数据,但它依然会消耗一个序列号。这意味着当服务器收到这个SY
