目录
- TCP 是什么:面向连接 + 可靠 + 字节流
- 三次握手:为什么不是两次
- 四次挥手与 TIME_WAIT:谁等谁
- 序列号/确认号与去重、排序、确认
- 重传机制:超时重传与快速重传
- 滑动窗口与流量控制
- 拥塞控制:慢启动/拥塞避免/快重传/快恢复
- 保活机制与长连接
- 半连接队列、全连接队列与 SYN 攻击
- RST 的语义:什么时候会被动断开
- 常见高频面试问答(含易错点)
- 抓包与排错清单
- 参考与延伸阅读
TCP 是什么:面向连接 + 可靠 + 字节流
- 面向连接:通信前需要建立连接(三次握手)。
- 可靠交付:通过序列号、确认号、校验和、重传、窗口等保证不丢、不重、按序到达(应用层“看起来”按序)。
- 字节流:没有消息边界,数据是连续字节流,分段与拼包在传输层完成。
简图(字节流与按序):
[App 写入字节] => [TCP 分段 seq=100..] => [网络] => [TCP 重组、去重、排序] => [App 读到连续字节]
三次握手:为什么不是两次
三次握手的目标:
- 交换初始序列号(ISN)并建立双向通信能力。
- 让客户端和服务端都“确认”对方的收发能力都正常。
流程(简化):
CLOSE -> SYN-SENT --SYN(x)--> LISTEN
SYN-RCVD <--SYN(y),ACK(x+1)-- LISTEN
ESTABLISHED --ACK(y+1)--> ESTABLISHED
为什么不能两次?
- 若两次握手,服务端无法判断当前请求是否“历史连接”重放;第三次 ACK 让客户端基于上下文确认“我确实与这个服务端建立了当前连接”,避免“僵尸连接”占用资源。
我常用的比喻:
- 第一次:我能发(SYN)。
- 第二次:我能收能发(SYN+ACK)。
- 第三次:我也能收(ACK),双方都齐活。
注:半连接队列记录“收到 SYN 尚未完成握手”的请求;若第三次 ACK 不到,条目会因超时被清理。
四次挥手与 TIME_WAIT:谁等谁
为什么“挥手”通常是四次?
- 关闭是“单向”的:一方
FIN
只表示“我不再发了”,对方读到FIN
但仍可继续发送,故需要两对报文确保双方各自完成“发送通道”的关闭。
典型序列:
(主动关闭) FIN -> ACK (被动方进 CLOSE_WAIT)
...对方数据发送完...
FIN -> ACK (主动方进 TIME_WAIT)
TIME_WAIT 的意义:
- 等 2MSL,确保:
- 最后的 ACK 若丢失,对方重发 FIN,我还能重发 ACK;
- 旧连接的迟到报文不会影响未来新的同四元组连接。
调优提示:
- 服务端侧可让“短连接风格”的一方尽量成为被动关闭者,降低其 TIME_WAIT 压力(视业务/栈实现)。
序列号/确认号与去重、排序、确认
我面试常用“翻书”比喻:
能力 | 解决的问题 | 类比 |
---|---|---|
序列号(seq) | 去重与排序 | 页码防重排 |
确认号(ack) | 成功到达的确认 | 勾选“已读” |
窗口(win) | 节流与并行度 | 读写节拍 |
发送端维护已发送未确认的数据集合,接收端通过累计 ACK 告知“我已经连续收到哪一位点前的所有字节”。
重传机制:超时重传与快速重传
- 超时重传(RTO):发送后启动定时器,超时无 ACK 则重发;RTO 自适应基于 RTT 与抖动(通常 > RTT 的一定倍数)。
- 快速重传:收到 3 个重复 ACK(如
ACK=101,101,101
),判定某段可能丢了,提前重传,减少等待 RTO 的成本。
要点:
- 重传报文的
seq
与原始相同,可能合并为更大段(取决于实现)。 - 局部乱序也会触发重复 ACK,但不等同于丢包;拥塞控制会进一步介入(见下)。
滑动窗口与流量控制
- 接收窗口(rwnd):接收端缓冲可用空间,通过报文通告给发送端,防止“接收方处理不过来”。
- 发送窗口(swnd):发送端根据
min(cwnd, rwnd)
决定实际并发在途数据量。 - 零窗口探测:若
rwnd=0
,发送端定期发探测报文,等待窗口开放。
可视化(简略):
|--- 已确认 ---|--- 已发送未确 ---|--- 可发送窗口 ---|
^ base ^ nextSeq
拥塞控制:慢启动/拥塞避免/快重传/快恢复
- cwnd:拥塞窗口,代表“网络可能承受的并发在途量”的猜测值。
- 慢启动:从 1 MSS 开始指数增长,阈值
ssthresh
之前翻倍,之后线性增加(拥塞避免)。 - 快重传/快恢复:
- 3 次重复 ACK 触发:
ssthresh = cwnd/2
,cwnd = ssthresh
或ssthresh + 3*MSS
(依实现),进入快速恢复,避免回到 1 MSS 的冷启动。
- 3 次重复 ACK 触发:
- 超时:说明更严重拥塞,通常将
cwnd
置 1 MSS,重新慢启动。
保活机制与长连接
- KeepAlive:长时间无数据时周期性发送探测包;若多次无响应,判定连接死亡并关闭。默认关/开与探测周期依 OS 而异,可配置。
- 应用层心跳:例如 HTTP/2、WebSocket 自带 ping/pong,更灵活。
半连接队列、全连接队列与 SYN 攻击
- 半连接队列(SYN 队列):收到
SYN
,发送SYN+ACK
,等待第三次握手的ACK
。若超时未到达则过期剔除。 - 全连接队列(Accept 队列):三次握手完成的连接,等待应用层
accept()
取走。 - 防护思路:
- SYN Cookies、缩短 SYN/ACK 重传与过期时间、增大队列。
- 使用负载均衡/防火墙清洗异常 SYN 洪泛。
RST 的语义:什么时候会被动断开
- RST 表示“连接非法/不存在或异常状态”,常见触发:
- 目标端口无进程监听;
- 应用层提前关闭 socket,仍收到对端数据;
- 抓包/半开异常导致状态机不同步。
- 面试提示:用 RST 终止连接不会进入 TIME_WAIT(与 FIN 流程不同)。
常见高频面试问答(含易错点)
- 为什么是三次握手不是两次?
- 需要确认“双方收发能力”,并防历史连接的重放,占用资源。
- 为什么挥手要四次?
- 关闭是单向的,两个方向分别 FIN/ACK。
- TIME_WAIT 为什么在主动关闭方?
- 负责兜住最后 ACK 丢失与旧报文消散。
- 3 个重复 ACK 一定是丢包吗?
- 不一定,可能乱序;但为降低时延会触发快速重传并调整拥塞窗口。
rwnd
与cwnd
谁说了算?
- 实际可发窗口取两者较小值:
min(cwnd, rwnd)
。
- 半连接队列爆了怎么办?
- 开启 SYN Cookies、调大队列、缩短重试、前置抗 DDoS。
- 为什么 TCP 是字节流,UDP 是报文?
- TCP 为可靠按序的连续字节,UDP 不保证顺序、丢失可见,保留消息边界。
抓包与排错清单
- 观察三次握手:过滤
tcp.flags.syn==1 && tcp.flags.ack==0
。 - 快速重传:看是否出现 3 次重复 ACK 与
Dup ACK
标记。 - RTO 触发:同一
seq
的报文重发,时间间隔接近 RTO。 - 零窗口:
Window Size Value = 0
与周期性探测包。 - 拥塞事件:
[TCP Previous segment not captured]
、[Retransmission]
、窗口骤降。
Wireshark 过滤示例:
# 仅看 TCP 握手
tcp.flags.syn==1 || tcp.flags.fin==1 || tcp.flags.reset==1
# 指定四元组
ip.addr==A && ip.addr==B && tcp.port in {PORT1,PORT2}
参考与延伸阅读
- RFC 793、RFC 5681:TCP 规范与拥塞控制
- 《Linux高性能服务器编程》《TCP/IP 详解 卷一》
- Wireshark 官方文档与实践