3、TCP三次握手:从协议头到抓包实战
TCP三次握手,说白了就是两台机器建立连接前的「打招呼」仪式。我经常跟团队里的新人说,搞懂了这个握手过程,你就能解决一半的网络连接问题。
咱们先看协议头部,再看握手细节,最后用Wireshark抓包验证。这样你以后遇到连接超时、握手失败的问题,心里就有底了。
3.1 TCP协议头部结构
TCP头部最少20字节,最多60字节。我习惯把它分成两部分记:固定部分和选项部分。
| 字段 | 长度(位) | 说明 |
|---|---|---|
| 源端口 | 16 | 发送方端口号 |
| 目的端口 | 16 | 接收方端口号 |
| 序列号(SEQ) | 32 | 本报文段数据的第一个字节序号 |
| 确认号(ACK) | 32 | 期望收到的下一个字节序号 |
| 数据偏移 | 4 | 头部长度,单位是4字节 |
| 标志位 | 9 | SYN、ACK、FIN等控制位 |
| 窗口大小 | 16 | 接收窗口,用于流量控制 |
| 校验和 | 16 | 覆盖头部和数据 |
| 紧急指针 | 16 | 仅URG标志位为1时有效 |
这里我重点说三个字段,因为三次握手全靠它们:
- 序列号(SEQ):每个TCP报文都有一个序列号。第一次握手时,客户端会随机生成一个初始序列号(ISN)。
- 确认号(ACK):表示「我已经收到了你发的数据,下一个该发这个序号了」。比如收到SEQ=1000的报文,ACK就填1001。
- 标志位:SYN表示发起连接,ACK表示确认,FIN表示关闭连接。三次握手主要用SYN和ACK。
3.2 三次握手详细过程
握手过程其实就三步。我画个图,你一看就明白。
好,咱们一步步拆解:
第一步:客户端发SYN
客户端主动打开连接,发送一个SYN报文。这个报文里:
- SYN标志位 = 1
- 序列号SEQ = x(随机生成的初始序列号)
- 不携带数据
发送后,客户端进入SYN_SENT状态。
第二步:服务端回复SYN-ACK
服务端收到SYN后,如果同意连接,就回复SYN-ACK报文:
- SYN标志位 = 1
- ACK标志位 = 1
- 序列号SEQ = y(服务端自己的初始序列号)
- 确认号ACK = x + 1(确认收到了客户端的SYN)
服务端进入SYN_RCVD状态。
第三步:客户端发ACK确认
客户端收到SYN-ACK后,回复一个ACK报文:
- ACK标志位 = 1
- 序列号SEQ = x + 1
- 确认号ACK = y + 1
客户端进入ESTABLISHED状态。服务端收到这个ACK后,也进入ESTABLISHED状态。
关键点: 三次握手完成后,双方都确认了对方的接收能力和发送能力。客户端知道「服务端能收能发」,服务端也知道「客户端能收能发」。这就是为什么需要三次,而不是两次。
避坑指南: 我曾经遇到一个线上问题,客户端一直卡在SYN_SENT状态。抓包发现服务端回了SYN-ACK,但客户端没收到。最后排查是中间防火墙把SYN-ACK报文给丢了。所以遇到握手失败,别光看客户端,也要看服务端有没有回包。
3.3 用Wireshark观察三次握手
理论说完了,咱们实战。我习惯用Wireshark抓一个HTTP请求,然后过滤出三次握手报文。
抓包步骤
- 打开Wireshark,选择网卡开始抓包
- 在浏览器访问一个网站(比如
http://example.com) - 停止抓包,在过滤栏输入
tcp.flags.syn == 1 - 找到第一个SYN报文,右键 → Follow → TCP Stream
你会看到类似这样的三个报文:
# 报文1:客户端 → 服务端
Frame 1: 74 bytes on wire (592 bits)
Ethernet II, Src: Client_MAC, Dst: Server_MAC
Internet Protocol Version 4, Src: 192.168.1.100, Dst: 93.184.216.34
Transmission Control Protocol, Src Port: 54321, Dst Port: 80
Flags: 0x002 (SYN)
Sequence Number: 0 (relative)
Acknowledgment Number: 0
Window Size: 65535
# 报文2:服务端 → 客户端
Frame 2: 74 bytes on wire (592 bits)
Transmission Control Protocol, Src Port: 80, Dst Port: 54321
Flags: 0x012 (SYN, ACK)
Sequence Number: 0 (relative)
Acknowledgment Number: 1
Window Size: 65535
# 报文3:客户端 → 服务端
Frame 3: 66 bytes on wire (528 bits)
Transmission Control Protocol, Src Port: 54321, Dst Port: 80
Flags: 0x010 (ACK)
Sequence Number: 1 (relative)
Acknowledgment Number: 1
Window Size: 65535
我的小技巧: Wireshark默认显示的是「相对序列号」,从0开始。这样看起来更直观。如果你想看真实的绝对序列号,可以在Wireshark里右键 → Protocol Preferences → TCP → 取消勾选「Relative sequence numbers」。
怎么看懂这些报文?
我教你一个快速判断的方法:
- 第一次握手:只有SYN标志位,没有ACK。SEQ是0(相对值),ACK是0。
- 第二次握手:SYN和ACK同时为1。SEQ是0(服务端的相对值),ACK是1(客户端的SEQ+1)。
- 第三次握手:只有ACK标志位。SEQ是1(客户端的SEQ+1),ACK是1(服务端的SEQ+1)。
你想想看,如果第三次握手丢了会怎样?服务端收不到ACK,会一直重发SYN-ACK。默认重试次数是5次,间隔时间指数增长。我在项目里遇到过这种情况,客户端发完ACK后直接开始发数据,但服务端还在SYN_RCVD状态,数据就被丢了。
3.4 常见问题与排查思路
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 客户端一直SYN_SENT | 服务端没回SYN-ACK | 检查服务端端口是否监听、防火墙是否拦截 |
| 服务端一直SYN_RCVD | 客户端没回第三次ACK | 检查客户端是否收到SYN-ACK、路由是否正常 |
| 握手成功但连接异常 | 中间设备篡改SEQ/ACK | 对比两端抓包,看SEQ是否一致 |
| 握手延迟高 | 网络延迟或SYN Flood攻击 | 用ping测延迟,用netstat看半连接数 |
重要提醒: 千万不要在生产环境直接抓包!我见过有人直接在线上服务器抓包,结果磁盘写满导致服务挂了。正确的做法是:用tcpdump抓少量包,或者用镜像端口抓包。
嗯,三次握手的内容就这些。你只要记住SEQ和ACK的关系,再配合Wireshark看一遍实际报文,这个知识点就彻底拿下了。