运输层基础内容:
TCP参考:
TCP报文段的首部格式
TCP 虽然是面向字节流的,但 TCP 传送的数据单元却是报文段。一个 TCP 报文段分为首部和数据两部分。TCP 报文段首部的前20个字节是固定的,后面有4n字节是根据需要而增加的可选项(n是整数)。因此 TCP 首部的最小长度是20字节。
逐字段详解
1. Source Port(源端口):16位
属性 说明 长度 2字节(16位) 作用 标识发送端应用程序 范围 0-65535 分类 0-1023(知名端口),1024-49151(注册端口),49152-65535(动态/私有端口) 注意:客户端通常使用临时端口(49152-65535),服务器使用知名端口(如80、443)。
2. Destination Port(目的端口):16位
属性 说明 长度 2字节(16位) 作用 标识接收端应用程序 端口与进程的关系:
TCP报文 → 目的端口=80 → 操作系统交给HTTP进程 TCP报文 → 目的端口=443 → 操作系统交给HTTPS进程
3. Sequence Number(序列号):32位
属性 说明 长度 4字节(32位) 作用 标识本数据段第一个字节的序号 范围 0 - 4,294,967,295(2^32-1) 核心作用:
按序交付:接收方能按序列号重组乱序到达的数据
去重:重复的序列号表示重复数据,可丢弃
标识丢失:缺失的序列号表示数据丢失
初始序列号(ISN,Initial Sequence Number):
连接建立时,双方随机生成ISN
第一个SYN包消耗一个序列号
后续数据包的序列号 = ISN + 已发送字节数
示例:
ISN = 1000 发送100字节 → 序列号=1000(本段覆盖1000-1099) 下一个段 → 序列号=1100回绕问题:32位序列号最大约42亿,高速网络可能在短时间内用完。TCP通过时间戳选项解决。
4. Acknowledgment Number(确认号):32位
属性 说明 长度 4字节(32位) 作用 期望下次收到的序列号 有效条件 ACK标志位=1 含义 :确认号=N,表示序号小于N的所有字节都已成功收到,下次期望收到序号N。
示例:
发送方收到 ACK=1100 → 表示对方已收到0-1099,下次期望1100累积确认:一个ACK可以确认之前所有连续的数据,不需要为每个包单独确认。
5. Data Offset(数据偏移):4位
它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度。
属性 说明 长度 4位 单位 32位字(4字节) 作用 指示TCP头部的总长度 最小值 5(5×4=20字节) 最大值 15(15×4=60字节) 计算方式:
TCP头部长度 = Data Offset × 4 字节示例:
Data Offset=5 → 头部20字节(无选项)
Data Offset=8 → 头部32字节(有12字节选项)
6. Reserved(保留位):3位
属性 说明 长度 3位 状态 必须为0 用途 保留给未来使用
7. Flags(标志位):9位
这是TCP最关键的字段之一,每个标志位都是一个布尔开关。
标志 全称 位位置 说明 NS Nonce Sum bit 8 ECN保护标志(RFC 3540) CWR Congestion Window Reduced bit 7 拥塞窗口已减小 ECE ECN Echo bit 6 拥塞通知回显 URG Urgent bit 5 紧急指针有效 ACK Acknowledgment bit 4 确认号有效(除首个SYN外通常为1) PSH Push bit 3 立即推送,不要缓存 RST Reset bit 2 强制重置连接 SYN Synchronize bit 1 同步序列号,用于建立连接 FIN Finish bit 0 发送方已无数据,请求关闭 下面6个主要
控制位,用来说明本报文段的性质
紧急 URG(URGent)当 URG=1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不是按原先的排队顺序来传送,现已很少使用。确认 ACK(ACKnowledgment)仅当 ACK=1 时确认号字段才有效。当 ACK=0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置1。推送 PSH(Push)提示接收方立即将数据交给应用层,不要等缓冲区满了再交。现代TCP栈通常忽略。复位 RST(ReSeT)当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,不经过正常挥手,然后再重新建立运输连接。同步 SYN(SYNnchronization)在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN=1 和 ACK=1。终止 FIN(FINis)连接关闭时使用。FIN=1表示发送方已无数据发送。消耗一个序列号。
8. Window(窗口):16位
属性 说明 长度 2字节(16位) 作用 流量控制,告知对方自己的接收缓冲区剩余大小 单位 字节 范围 0 - 65535 核心作用:
发送方已发未确认的数据量 ≤ 对方的Window零窗口:Window=0时,发送方停止发送,但会定期发送零窗口探测,防止死锁。
窗口缩放 :Window字段只有16位(最大65535),高速网络中不够用。通过Window Scale选项,可将窗口左移最多14位,理论最大窗口约1GB。
9. Checksum(校验和):16位
属性 说明 长度 2字节(16位) 覆盖范围 TCP伪头部 + TCP头部 + TCP数据 算法 16位反码求和 强制 TCP强制要求(UDP可选) 伪头部(Pseudo Header):只在计算校验和时构造,不是TCP报文的一部分。
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Zero | Protocol (6) | TCP Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Source/Destination Address:从IP头部取来
Protocol:固定6(TCP)
TCP Length:TCP头部+数据的长度
为什么需要伪头部:防止数据被错误投递。校验和验证了"这个TCP包确实是发给我的"。
10. Urgent Pointer(紧急指针):16位
属性 说明 长度 2字节(16位) 作用 指向紧急数据的最后一个字节 有效条件 URG标志位=1 单位 字节 含义:Urgent Pointer = 紧急数据在TCP数据流中的偏移量(从序列号开始算)。
示例:
Sequence Number = 1000 Urgent Pointer = 50 → 紧急数据覆盖 1000 到 1049 字节现状:URG机制设计复杂,容易出错,实际使用很少。现代应用通常用带外数据或其他方式处理紧急需求。
11. Options(选项):可变长度(0-40字节)
属性 说明 长度 0-40字节 单位 字节,必须是8的倍数 作用 扩展TCP功能 常见选项:
选项 类型值 长度 说明 MSS 2 4字节 告知对方自己能接收的最大段大小 Window Scale 3 3字节 窗口缩放因子,左移位数 Timestamp 8 10字节 时间戳,用于RTT计算和防回绕 SACK Permitted 4 2字节 告知对方支持SACK SACK 5 变长 选择性确认,告知对方哪些数据已收到 NOP 1 1字节 空操作,用于对齐填充 MSS选项:
Kind=2, Len=4, Value=MSS 告知对方:我能接收的最大TCP段大小(不含TCP头) 以太网典型值:1460字节(1500 - 20IP头 - 20TCP头)Window Scale选项:
Kind=3, Len=3, Shift=7 窗口实际大小 = Window字段 << Shift Shift最大14,理论最大窗口约1GB 仅在SYN包中协商Timestamp选项:
Kind=8, Len=10, TSval=发送方时间戳, TSecr=回显时间戳 作用: 1. RTTM:精确计算RTT 2. PAWS:防止序列号回绕SACK选项:
选择性确认:告诉对方“我收到了1000-2000和3000-4000,但缺2000-3000” 避免重传整个窗口,只重传丢失的部分
12. Padding(填充):可变长度
属性 说明 长度 0-3字节 值 全0 作用 确保TCP头部是32位的整数倍
TCP头部字段速查表
字段 长度 位置(字节偏移) 作用 Source Port 16位 0-1 发送端应用程序标识 Destination Port 16位 2-3 接收端应用程序标识 Sequence Number 32位 4-7 本段第一个字节的序号 Acknowledgment Number 32位 8-11 期望下次收到的序号 Data Offset 4位 12高4位 头部长度(单位32位字) Reserved 3位 12低3位 保留为0 Flags 9位 12-13 控制标志(SYN/ACK/FIN等) Window 16位 14-15 接收窗口大小 Checksum 16位 16-17 校验和(含伪头部) Urgent Pointer 16位 18-19 紧急数据偏移 Options 可变 20起 扩展功能(MSS/时间戳/SACK) Padding 可变 选项后 对齐到32位边界 一句话总结:TCP报文头通过端口号区分应用,通过序列号和确认号实现可靠有序传输,通过窗口字段实现流量控制,通过标志位管理连接状态,通过校验和保证数据完整性,通过选项扩展功能------这20到60字节的头部,承载了TCP所有复杂能力的核心。
难道每个字节都有序列号吗?
你这个问题问得非常精准,抓住了TCP设计中的一个关键细节。
答案是:是的,逻辑上每个字节都有一个独立的序列号。
但千万别理解为"TCP会给每个字节添加一个8字节的序列号字段"那样冗余。它是通过起始序列号 + 长度的方式来标注一个段的。
1. 具体原理:不是标每个字节,而是标"段"的头和尾
假设初始序列号(ISN) = 1000,要发送5000字节的数据。TCP会把它切成多个段(比如每个段1460字节):
段1 :序列号 = 1000,数据长度 = 1460 → 表示它携带了 字节 1000 ~ 2459
段2 :序列号 = 2460,数据长度 = 1460 → 表示携带 字节 2460 ~ 3919
段3 :序列号 = 3920,数据长度 = 1460 → 携带 字节 3920 ~ 5379
段4 :序列号 = 5380,数据长度 = 620 → 携带 字节 5380 ~ 5999
你看,每个段的头部只存了一个起始序列号 (4字节),并没有为每个字节单独存序列号。但因为知道这个段的长度,就可以推导出它包含的所有字节的序列号。
2. 那为什么说"每个字节有序列号"?
因为确认、重传、重排这些可靠机制,都需要精确到字节级。否则无法处理以下场景:
部分ACK:对方收到了字节 1000-2459,但只收到了 2460-3000(后面的丢失了)。它回复 ACK=3001,意思是"我期待你的下一个字节是3001" ------ 这精确到某个字节。
丢包重传:知道丢了具体从哪个字节开始的段。
保序:接收方能根据每个字节的归属编号,把乱序到达的段正确拼接。
所以"每个字节有序列号"是一个逻辑抽象 ,便于理解机制。实现上是通过 起始序列号 + 段长度 来隐式完成的。
3. 一个形象的类比:电影院座位的排号
想象一排电影院座位,从第1排第1座开始连续编号:
你买的票上只写着 "起始座位号:101" 和 "连续5个座位"。
你不会拿到一张写着 101,102,103,104,105 每个座位号的票。
但检票员(TCP接收方)知道:101,102,103,104,105 这些座位都属于你,必须按顺序挨着坐(保序)。
4. 为什么要设计成这样,而不是只给每个段编号?
节省开销:每个字节都附一个序列号,头部会膨胀几十倍。
支持任意大小的段:TCP可以动态调整段大小(根据MSS、窗口等),无需改变序列号机制。
处理部分ACK:如果只给段编号,当接收方收到半个段的内容(仍然可能,因为IP分片等原因),无法表达"我只缺这个段里的第50~100个字节"。
正是因为序列号的粒度是字节,TCP才能提供可靠的、面向字节流的服务。如果粒度是"段",那就变成数据报模式(类似UDP加个确认),无法实现真正的流式可靠性。
那tcp按序拼接时,不也只能按照段来,因为并不是每个字节真的都带一个序列号!你的这个思考非常敏锐,而且完全正确。
在底层实现的"硬拼接"环节,TCP确实只能按照"段"来操作。 你看到的"按字节序号拼接"其实是操作系统和协议栈为了逻辑清晰而做的一个抽象。
1. 为什么书上非要说"按字节"?
因为"段"是运输单位,而应用层读数据时,想要的是连续的字节流,不想关心段的分界。
当你调用
read()时,操作系统要做一件事:把缓冲区里那些可能不连续的"段",拼接成一个连续的字节数组,然后交给应用程序。
输入:缓冲区里有 段A [1000-1099)和 段C [1100-1199) 两个物理上分开的块。(中间缺了段B)
动作 :TCP协议栈检查发现缺了1099到1100这个缝隙,于是不会 做拼接。
read()会阻塞或返回已收到的1000-1099。等段B到达后:
段B (SEQ=1099, Len=1) 到达。注意,段B可能只有1个字节。
操作系统把段B填入那个缝隙。
现在缓冲区里,1000-1099(段A)、1099(段B)、1100-1199(段C)在物理地址上可能依然是三个分开的内存块。
但是,当应用层调用
read(1000, 200)时,操作系统内核会把这些物理上分散的内存块 ,拷贝到应用程序的连续内存中。系统关注的是离散的字节序号断点(1099) ,但在存储层面,它是以段为单位存放这些数据的。
TCP有哪些状态
TCP是一个状态机 ,连接从建立到关闭的全过程,会在11种状态之间转换。理解这些状态,是排查网络问题(如连接超时、端口占用、CLOSE_WAIT堆积)的基础。
一、TCP状态总览(11种)
状态 中文名 角色 说明 CLOSED 关闭 初始 无连接状态(理论状态,实际看不到) LISTEN 监听 服务器 等待客户端连接请求 SYN_SENT 同步已发送 客户端 已发SYN,等待服务器SYN+ACK SYN_RCVD 同步已收到 服务器 已收到SYN,已发SYN+ACK,等待客户端ACK ESTABLISHED 已建立 双方 连接建立成功,可以传输数据 FIN_WAIT_1 终止等待1 主动关闭方 已发FIN,等待ACK或对方的FIN FIN_WAIT_2 终止等待2 主动关闭方 已收到ACK,等待对方FIN CLOSE_WAIT 关闭等待 被动关闭方 收到FIN,已发ACK,等待应用层关闭 LAST_ACK 最后确认 被动关闭方 已发FIN,等待对方最后的ACK TIME_WAIT 时间等待 主动关闭方 收到FIN并发了ACK,等待2MSL后关闭 CLOSING 关闭中 双方 同时关闭时,双方都发了FIN,等待ACK
二、TCP状态转换图
三、各状态详解
1. CLOSED(关闭)
属性 说明 含义 无连接状态,初始状态 特点 理论状态,实际在socket中看不到 转换 LISTEN(服务器),SYN_SENT(客户端)
2. LISTEN(监听)
属性 说明 含义 服务器等待客户端连接请求 特点 只出现在服务器端 转换 收到SYN → SYN_RCVD 查看命令:
netstat -an | grep LISTEN # 或 ss -lnt
3. SYN_SENT(同步已发送)
属性 说明 含义 客户端已发送SYN,等待服务器SYN+ACK 特点 只出现在客户端 转换 收到SYN+ACK → ESTABLISHED;收到RST → CLOSED
4. SYN_RCVD(同步已收到)
属性 说明 含义 服务器已收到SYN,已发SYN+ACK,等待客户端ACK 特点 出现在服务器端 转换 收到ACK → ESTABLISHED;收到RST → CLOSED 半连接队列:SYN_RCVD状态的连接存放在半连接队列(SYN Queue)中,收到ACK后移入全连接队列(Accept Queue)。
5. ESTABLISHED(已建立)
属性 说明 含义 连接建立成功,可以传输数据 特点 双方正常的通信状态 转换 主动关闭 → FIN_WAIT_1;被动关闭 → CLOSE_WAIT 查看命令:
netstat -an | grep ESTABLISHED ss -tn state established
6. FIN_WAIT_1(终止等待1)
属性 说明 含义 主动关闭方已发送FIN,等待ACK或对方的FIN 特点 出现在主动关闭方 转换 收到ACK → FIN_WAIT_2;收到FIN → CLOSING;收到FIN+ACK → TIME_WAIT
7. FIN_WAIT_2(终止等待2)
属性 说明 含义 主动关闭方已收到ACK,等待对方FIN 特点 出现在主动关闭方 转换 收到FIN → TIME_WAIT 注意:如果对方一直不发送FIN(比如程序忘记关闭socket),FIN_WAIT_2状态会一直存在。
8. CLOSE_WAIT(关闭等待)⚠️ 常见问题
属性 说明 含义 被动关闭方收到FIN,已发ACK,等待应用层调用close() 特点 出现在被动关闭方 转换 应用层调用close() → LAST_ACK ⚠️ CLOSE_WAIT堆积是常见问题:
原因:服务器代码没有正确调用
close(),或者进程卡住了后果:大量CLOSE_WAIT连接耗尽文件描述符,无法建立新连接
解决:检查代码,确保所有socket都正确关闭
查看命令:
netstat -an | grep CLOSE_WAIT | wc -l # 统计数量
9. LAST_ACK(最后确认)
属性 说明 含义 被动关闭方已发送FIN,等待对方最后的ACK 特点 出现在被动关闭方 转换 收到ACK → CLOSED;超时 → CLOSED
10. TIME_WAIT(时间等待)⚠️ 常见问题
属性 说明 含义 主动关闭方收到FIN并发了ACK,等待2MSL后关闭 特点 出现在主动关闭方 持续时间 2MSL(Maximum Segment Lifetime),通常2分钟 转换 2MSL超时 → CLOSED TIME_WAIT的作用:
确保最后的ACK能被对方收到:如果ACK丢失,对方会重发FIN,TIME_WAIT状态可以响应这个重发的FIN
让旧连接的数据包在网络中消失:防止旧连接的数据干扰新连接
⚠️ TIME_WAIT堆积:
原因:高并发短连接场景(如Web服务器频繁关闭连接)
影响:占用端口资源,可能导致无可用端口
解决:
调整内核参数
net.ipv4.tcp_tw_reuse(允许复用TIME_WAIT连接)调整
net.ipv4.tcp_tw_recycle(快速回收,已废弃,NAT环境会有问题)使用长连接,减少连接建立/关闭次数
查看命令:
netstat -an | grep TIME_WAIT | wc -l ss -tan state time-wait
11. CLOSING(关闭中)
属性 说明 含义 双方同时发送FIN,同时关闭 特点 罕见,出现在双方几乎同时调用close() 转换 收到ACK → TIME_WAIT 同时关闭场景:
客户端 服务器 | | |-------- FIN ------------------->| | | |<------- FIN --------------------| | | |-------- ACK ------------------->| | | |<------- ACK --------------------|
四、状态转换速查表
当前状态 收到包 下一状态 LISTEN SYN SYN_RCVD SYN_SENT SYN+ACK ESTABLISHED SYN_RCVD ACK ESTABLISHED ESTABLISHED FIN(主动) FIN_WAIT_1 ESTABLISHED FIN(被动) CLOSE_WAIT FIN_WAIT_1 ACK FIN_WAIT_2 FIN_WAIT_1 FIN CLOSING FIN_WAIT_2 FIN TIME_WAIT CLOSE_WAIT close() LAST_ACK LAST_ACK ACK CLOSED TIME_WAIT 2MSL超时 CLOSED
五、排查命令汇总
命令 作用 netstat -an查看所有连接状态 `netstat -an grep LISTEN` `netstat -an grep ESTABLISHED` `netstat -an grep CLOSE_WAIT` `netstat -an grep TIME_WAIT` ss -tln查看TCP监听端口 ss -tan state established查看ESTABLISHED连接 ss -tan state time-wait查看TIME_WAIT连接 lsof -i :端口号查看端口被哪个进程占用
六、常见问题与状态对应
问题现象 相关状态 原因 服务连不上 LISTEN 服务没启动或端口没监听 连接超时 SYN_SENT 网络不通或防火墙丢SYN 半连接攻击 SYN_RCVD SYN Flood,半连接队列满 服务正常但连接被拒 ESTABLISHED 全连接队列满,新连接被丢 大量CLOSE_WAIT CLOSE_WAIT 代码没正确关闭socket 端口被占用 TIME_WAIT 高并发短连接,端口耗尽
三次握手
TCP建立连接的过程被称为三次握手(Three-way Handshake),这是TCP协议中最基础、最重要的机制之一。
为什么需要三次握手?
在理解过程之前,先明确目的:三次握手是为了在不可靠的网络基础上,可靠地建立一条连接,并同步双方的初始序列号。
具体要解决三个问题:
确认双方的收发能力都正常
同步双方的初始序列号(ISN,Initial Sequence Number)
交换TCP选项(如MSS、窗口缩放、SACK等)
流程图如下所示:
TCP 是面向连接、可靠传输 的协议,传输数据前必须先建立连接 ,保证双方发送能力、接收能力都正常。涉及三个核心标志位:
- SYN:同步序列编号(请求连接)
- ACK:确认应答(收到对方报文了)
还有序列号 seq 、确认号 ack:
seq=x:我方本次发送数据包的序号ack=y:我已经收到了你序号为 y-1 的数据,下一次请你发 y完整三次握手全过程(逐轮拆解)
默认:客户端主动发起连接,服务器被动监听
第一次握手:客户端 → 服务器
报文 :
SYN=1,seq=x
- 客户端状态:
CLOSED→ SYN_SENT- 动作:客户端向服务器发送连接请求报文 ,只有 SYN 标志,没有数据。
- 含义: 你好,我想和你建立连接,我的初始序列号是 x。
- 服务器收到后验证:确认服务器的接收能力正常、客户端的发送能力正常。
第二次握手:服务器 → 客户端
报文 :
SYN=1,ACK=1,ack=x+1,seq=y
- 服务器状态:
LISTEN→ SYN_RCVD- 动作:服务器回复双重报文
ACK=1,ack=x+1:确认收到客户端的 SYN,下一次客户端从x+1发SYN=1,seq=y:服务器自己的连接请求,服务器也要同步序列号- 含义: 收到你的连接请求了,同意连接。我的初始序列号是 y。
- 客户端收到后验证:确认客户端发送、接收正常;服务器发送、接收都正常。
第三次握手:客户端 → 服务器
报文 :
ACK=1,ack=y+1,seq=x+1
- 客户端状态:
SYN_SENT→ ESTABLISHED- 服务器收到后:
SYN_RCVD→ ESTABLISHED- 动作:客户端发送确认报文,依然不带数据
ack=y+1:确认收到服务器的 SYN,下次服务器从y+1发seq=x+1:客户端下一轮数据起始序号- 含义: 我也收到你的回复了,连接正式建立完毕。
- 自此双方双向通道打通,开始传输业务数据。
三、极简流程图(死背版)
- 客户端 :
SYN,seq=x- 服务器 :
SYN+ACK,ack=x+1,seq=y- 客户端 :
ACK,ack=y+1,seq=x+1四、面试高频灵魂问题:为什么是三次,不能两次?
1. 防止历史过期连接请求阻塞服务器
最经典标准答案:网络会延迟丢包、旧报文滞留。
假设只有两次握手 :客户端很久以前发的过期 SYN 请求 ,慢悠悠传到服务器。服务器收到 SYN,直接第二次握手 ACK 同意连接 ,进入建立连接状态。但客户端早就不要这个连接了,不会理服务器。服务器就会一直空占连接、消耗内存资源,大量旧包堆积会直接打垮服务器。
三次握手多了客户端最后一次 ACK :过期的旧 SYN,服务器回复 SYN+ACK 后,客户端不会回第三次 ACK 。服务器收不到第三次 ACK,超时就自动释放连接,不会被无效连接占用。
2. 双向序列号同步
TCP 是全双工通信:双方都要发、都要收。
- 客户端需要自己的 seq、确认服务器的 seq
- 服务器需要自己的 seq、确认客户端的 seq双方发送序列号、接收序列号都必须同步,最少需要 3 轮交互才能闭环。
3. 一句话总结
两次握手:服务器无法判断客户端是否收到自己的报文 ,且防不住脏旧连接。三次握手:双方收发能力全部验证完毕,序列号完全同步。
五、状态机全程汇总
- 客户端:
CLOSED→SYN_SENT→ESTABLISHED- 服务器:
LISTEN→SYN_RCVD→ESTABLISHED六、易混点区分
- 三次握手全程都不传输业务数据,只建通道;数据在三次握手完成后才开始发。
- SYN 报文不携带数据 ,但消耗一个序列号;ACK 纯确认报文不携带数据,不消耗序列号。
第一次握手的seq=x,为啥第二次握手回复的ack=x+1?直接回答:因为第一次握手发送的SYN包本身,也要被算进序列号里。它虽然不携带用户数据,但作为一个"控制命令",消耗了一个序列号。所以接收方必须回复
x+1,来表示"我已收到了你的SYN命令,下一个希望收到序列号为x+1的数据"。下面从三个层面来详细解释:
核心规则:SYN和FIN要占一个序列号
这是理解这个问题的根本。TCP协议规定,SYN和FIN标志为1的包,虽然不携带应用层数据,但必须消耗一个序列号。
普通数据包:发送了100字节,序列号就增加100。
SYN包(不携带数据) :序列号只增加1 。这1个序列号,代表的就是"发起连接"这个行为本身。
为什么?
为了让确认机制能覆盖整个数据流,包括连接和关闭事件。接收方回复
Ack=x+1,不仅仅是在说"收到你的序列号x",更是在说:"我确认你发起的**建立连接请求(SYN)**已经成功被我收到了。"
MSS
MSS 是 Maximum Segment Size(最大段大小)的缩写。
它是 TCP 协议中一个非常重要的参数,决定了在不发生 IP 分片的情况下,单个 TCP 段(Segment)里能携带的最大有效载荷(Payload,即实际数据)的字节数。
注意:MSS 只计算 TCP 头部后面的数据部分,不包括 TCP 头部和 IP 头部。
一、为什么需要 MSS?
这要从网络分层和 MTU 说起。
1. 链路层的限制:MTU
以太网中,一个数据帧的最大大小通常是 1500 字节(MTU,Maximum Transmission Unit)。
2. IP 层不分片的原则
一个 IP 包(包括 IP 头和 TCP 段)如果超过 1500 字节,IP 层可以把它拆成多个分片(Fragment),但这会带来问题:
如果任何一个分片丢失,整个 IP 包都要重传
分片会增加路由器的负担,降低效率
更好的做法 :在 TCP 层就控制好每个段的大小,让它装进 IP 包后不超过 MTU,从而避免 IP 分片。
二、MSS 的计算公式
为了让 IP 包不超过 MTU:
IP包总大小 = IP头部 + TCP头部 + TCP数据 ≤ MTU 所以:TCP数据 ≤ MTU - IP头部 - TCP头部因此:
MSS = MTU - IP头部(通常20字节) - TCP头部(通常20字节)默认情况下(以太网 MTU=1500):
MSS = 1500 - 20 - 20 = 1460 字节这是最常见的 MSS 值。
三、MSS 的协商过程
MSS 不是任意指定的,而是在 TCP 三次握手期间 由双方协商决定的。
客户端 → 服务器: SYN, MSS=1460 (我的最大能收1460) 服务器 → 客户端: SYN+ACK, MSS=1400 (抱歉,我只能收1400) 客户端 → 服务器: ACK 最终双方使用 min(1460, 1400) = 1400 作为连接的 MSS关键点:
MSS 选项只在 SYN 包中出现
每一方通告的是自己接收能力(我能收多大的段)
实际发送时,发送方使用对端通告的 MSS 作为自己的发送上限
四、一个常见的困惑:MSS vs MTU
概念 定义 所在层 典型值(以太网) MTU 链路层能传输的最大IP包大小(包含IP头) 链路层/IP层 1500字节 MSS TCP段中数据的最大字节数(不含IP头、TCP头) TCP层 1460字节 关系图:
|<- 链路层帧(最多1518)->| |<- MTU=1500 ->| |<- IP头20 ->|<- TCP头20 ->|<- MSS=1460 ->| |<------ TCP段 ------>|
五、MSS 过小或过大的影响
1. MSS 过大
超过路径 MTU(PMTU,Path MTU),会导致 IP 分片
如果 IP 分片被防火墙丢弃或丢失率高,连接会卡死
2. MSS 过小
传输效率低:同样的数据量要分成更多段
头部开销占比大:每个段都有 TCP + IP 头部(至少 40 字节)
3. 最佳实践
使用 PMTU 发现:TCP 会探测路径上最小的 MTU,动态调整 MSS
开启 DF 标志(Don't Fragment),当包太大时路由器返回 ICMP 错误,TCP 会缩小 MSS
六、实际抓包示例
// 三次握手协商 MSS 14:32:01.123 IP 192.168.1.100.54321 > 93.184.216.34.80: Flags [S], seq 1000000, win 65535, options [mss 1460,nop,wscale 7], length 0 14:32:01.234 IP 93.184.216.34.80 > 192.168.1.100.54321: Flags [S.], seq 2000000, ack 1000001, win 8192, options [mss 1400], length 0 // 后续数据传输的最大段 = min(1460, 1400) = 1400 字节
七、与 MSS 相关的常见问题
Q:为什么有时候抓包看到大于 MSS 的段?
A:可能原因:
使用了 TCP Segmentation Offload (TSO),网卡硬件分包
IP 层分片(但 DF 标志通常禁止)
抓包位置在分片之后
Q:MSS 能超过 1460 吗?
A:可以,如果 MTU 更大(如 jumbo frame,MTU=9000,则 MSS=8960)。但需要整个路径都支持。
Q:MSS 为 0 是什么意思?
A:SYN 包中 MSS=0 告诉对方"不要发任何数据",用于某些特殊场景(如端口扫描探测)。
Q:应用层一次 send 1MB 数据,TCP 会发一个 1MB 的段吗?
A:不会。TCP 会按 MSS 把数据拆成多个段,每个段最大为 MSS。
总结一句话
MSS 是 TCP 协商出的"单个段能装的最大货物量",默认 1460 字节,目的是让 IP 包不超过链路层的 MTU,避免分片,提高效率。
四次挥手
如下:
TCP 是全双工 :连接建立后,客户端↔服务器两条独立通道
- A 给 B 发数据
- B 给 A 发数据两条流互不干扰 ,关闭的时候两边都要单独关闭 ,所以才需要四次挥手。
用到标志位:
- FIN :结束报文,我没有数据要发了,关闭写通道
- ACK:确认应答
序列号依旧:
seq=u发送方序列号ack=v确认号:我收到你v-1的数据了四次挥手完整全过程(逐轮拆解)
默认:客户端主动断开连接,服务器被动关闭
第一次挥手:客户端 → 服务器
FIN=1,seq=u
- 客户端状态:
ESTABLISHED→ FIN_WAIT_1- 含义:
我数据发完了,我这边不再给你发数据了,关闭客户端→服务器的写通道。
- 服务器收到后:知道客户端要断开了,但服务器还有可能要给客户端发剩余数据,不能立刻关。
第二次挥手:服务器 → 客户端
ACK=1,ack=u+1,seq=v
- 服务器状态:
ESTABLISHED→ CLOSE_WAIT- 含义:
收到你的断开请求了,我确认了。你那边可以先关掉了。
- 关键点:此时单向关闭完成 :客户端 → 服务器通道关闭 服务器 → 客户端通道还开着,服务器依旧可以给客户端发数据。
第三次挥手:服务器 → 客户端
(服务器数据全部发送完毕后)
FIN=1,seq=v
- 服务器状态:
CLOSE_WAIT→ LAST_ACK- 含义:
我数据也全部发完了,我这边也不给你发数据了,服务器→客户端通道关闭。
第四次挥手:客户端 → 服务器
ACK=1,ack=v+1,seq=u+1
- 客户端状态:
FIN_WAIT_2→ TIME_WAIT- 服务器收到后:
LAST_ACK→ CLOSED- 含义:
收到你的断开请求,全部确认,双向通道彻底关闭。
- 客户端等待2MSL 时间后,也进入
CLOSED。
三、极简流程图(面试直接背)
- 客户端 :
FIN,seq=u- 服务器 :
ACK,ack=u+1,seq=v- 服务器 :
FIN,seq=v- 客户端 :
ACK,ack=v+1,seq=u+1四、全套状态机完整梳理
客户端状态流转
ESTABLISHED→FIN_WAIT_1(发第一次 FIN)→FIN_WAIT_2(收到第二次 ACK)→TIME_WAIT(收到第三次 FIN,发第四次 ACK)→ 等待 2MSL →CLOSED服务器状态流转
ESTABLISHED→CLOSE_WAIT(收到第一次 FIN,发 ACK)→LAST_ACK(数据发完,发 FIN)→CLOSED(收到第四次 ACK)
五、灵魂高频问题
1. 为什么建立连接 3 次握手,断开要 4 次挥手?
最标准答案:
- 建立连接时 :服务器的
SYN和ACK可以合并在一个报文里一起发给客户端,所以省一步,只要 3 次。- 断开连接时 :服务器收到 FIN 后,可能还有剩余数据要发送 ,不能立刻发 FIN 关闭自己。所以ACK 确认 和自身 FIN 关闭 必须分开发,不能合并,因此多了一轮,变成 4 次。
一句话总结:
建连:服务器 SYN+ACK 合并 → 3 次断连:服务器 ACK、FIN 不能合并 → 4 次
2. 什么是 TIME_WAIT?为什么要等 2MSL?
客户端发完最后一次 ACK 后,不直接关闭,停留 2MSL:
- 保证服务器一定收到了最后这个 ACK
- 防止网络滞留的旧报文流入下一次新连接,造成干扰
3. 什么是 2MSL?
MSL:报文最大生存时间2MSL = 报文去程最大时间 + 返程最大时间足够让网络里所有旧报文过期消失。
可靠传输
TCP 的可靠传输是通过多种机制的协同工作来保证数据能够准确、有序、无重复地送达对端。下面为你系统地讲解这些核心机制。
核心思路 :TCP 将应用层数据分割成多个 TCP 段 (Segment),并为每个段编号。接收方收到后要确认(ACK),发送方若未收到确认会重传。
1. 校验和 (Checksum) -- 保证数据完整
每个 TCP 段的头部和数据的错误检测。发送方计算出一个校验和并填入头部,接收方重新计算,如果一致则说明数据在传输中没有损坏,否则直接丢弃该段(发送方会因未收到 ACK 而重传)。
2. 序列号 (Sequence Number) -- 保证数据有序且不重复
每个字节都有一个独立的序列号。
初始序列号 (ISN) 在建立连接时随机生成。
接收方根据序列号对段进行重排,确保交给应用层的数据顺序正确;同时也用于去重,丢弃已经接收过的段。
3. 确认应答 (ACK) -- 确认已收到
接收方收到数据后,会回复一个 ACK 段 ,其中的 确认号 (Acknowledgment Number) 等于 期望收到的下一个字节的序列号。
例如:收到序列号 0-999 的数据,则回复确认号 1000,表示 "0-999 的字节已收到,下一个请从 1000 开始发送"。
4. 超时重传 (Retransmission Timeout, RTO)
发送方每发一个段,会启动一个定时器。如果在 RTO 时间内没有收到对应的 ACK,就认为该段丢失,并重新发送。
- 关键:RTO 不能是固定值。TCP 使用 加权滑动平均 算法动态估算网络往返时间 (RTT),并据此动态调整 RTO,以适应网络变化。
5. 快速重传 (Fast Retransmit) -- 避免超时等待
触发条件 :发送方收到 三个重复的 ACK(即连续三次确认同一个序列号)。
含义:接收方在告诉发送方 "我缺了从某个序列号开始的数据,请立刻重传"。
优点:不必等到超时,能更快修复丢包。
6. 流量控制 (Flow Control) -- 防止接收方被淹没
通过 滑动窗口 实现。
接收方在 ACK 中会携带 接收窗口 (rwnd),告诉发送方 "我的缓冲区还能接收多少字节"。
发送方发送的总字节数(已发未确认部分)不能超过对方的接收窗口。
注意:rwnd 是动态变化的,当接收方应用层读取数据后,窗口会增大。
7. 拥塞控制 (Congestion Control) -- 防止网络过载
这是 TCP 保证网络不崩溃的重要机制,与流量控制不同(后者针对接收方能力)。主要包含四个阶段:
阶段 算法 说明 慢启动 指数增长拥塞窗口 (cwnd) 初始 cwnd 很小,每收到一个 ACK,cwnd 翻倍(实际是每个 RTT 翻倍),直到达到慢启动阈值 (ssthresh) 拥塞避免 线性增长 cwnd 到达 ssthresh 后,每收到一个 ACK 增加一个 MSS 的 1/cwnd(即每个 RTT 约增加 1 MSS),避免过快增长引发拥塞 快速重传 + 快速恢复 不回到慢启动 检测到丢包(例如 3 个重复 ACK)时:设置 ssthresh = cwnd/2,然后 cwnd = ssthresh + 3(快速恢复),线性增长,避免剧烈波动 超时丢包 重置到慢启动 若发生超时,则 ssthresh = cwnd/2,cwnd = 1 MSS,重新慢启动(较激进,说明网络拥塞严重) 整体交互示例
假设发送方从序列号 100 开始发送:
发送:数据 100-199 (seq=100), 200-299 (seq=200), 300-399 (seq=300)
接收方收到 200-299 和 300-399,但没收到 100-199
接收方回复 ACK=100(重复三次)
发送方触发快速重传,立即重传 100-199
接收方收齐后回复 ACK=400
窗口滑动,继续发送
总结
TCP 的可靠传输是 "校验 + 序列化 + 确认 + 重传 + 窗口控制" 的组合:
校验和 → 防数据损坏
序列号/ACK → 保序、去重、确认
超时/快速重传 → 丢包恢复
流量控制 (rwnd) → 不同接收方能力匹配
拥塞控制 (cwnd) → 适应网络负载
这些机制一起让 TCP 在上层应用看来,像是一条可靠的、顺序无误的字节流通道。
滑动窗口和流量控制
参考:
tcpip协议第15讲:tcp滑动窗口、拥塞窗口以及拥塞控制原理介绍_哔哩哔哩_bilibili
神奇的滑动窗口 | TCP流量控制_哔哩哔哩_bilibili
所谓的"滑动",就是接收缓存的大小不断变化;"窗口"就是还能继续接收数据的缓存大小。
流量控制是接收方用来限制发送方发送速度的机制,目的是防止接收方的缓冲区被填满,导致数据被丢弃。
1. 为什么需要流量控制?
因为TCP是全双工 通信,双方都有发送和接收的能力。但接收方的应用程序可能处理数据比较慢(比如还在处理上一批数据,还没来得及调用
read)。如果不管不顾地狂发数据,接收方的内核接收缓冲区满了之后,后续到达的数据包就只能被丢弃。
流量控制要解决的就是这个问题:发送方发送数据的速度,必须匹配接收方应用程序读取数据的速度。
2. 核心机制:滑动窗口协议
TCP使用滑动窗口 来实现流量控制。每个TCP报文段头部都有一个
Window字段,用来告知对端:"我现在还能接收多少字节的数据"。接收窗口 (rwnd)
定义:接收方当前可用的缓冲区大小。
作用 :发送方不能发送超过
rwnd字节的未确认数据。发送方的发送窗口
发送方实际能发送的数据量由两个窗口共同决定:
有效发送窗口 = min(rwnd, cwnd)
rwnd:接收窗口(流量控制)
cwnd:拥塞窗口(拥塞控制)
3. 详细工作过程
3.1 正常情况
接收方通告窗口大小 = 4000字节
发送方发送2000字节(未确认)
接收方收到数据,但应用程序只读了1000字节,缓冲区还有1000字节空闲
接收方回复ACK,同时通告窗口 = 2000字节(4000 - 2000 + 1000)
发送方根据新的窗口大小继续发送
3.2 零窗口 (Zero Window)
当接收方缓冲区满了,就会通告窗口 = 0。
此时发送方会停止发送数据 ,并启动一个持续计时器 。计时器超时后,发送方会发送一个零窗口探测报文,试探接收方的窗口是否已经打开。
3.3 糊涂窗口综合征 (Silly Window Syndrome)
这是一个需要避免的问题。当接收方每次只读入少量数据(比如1字节),然后通告一个很小的窗口(比如1字节),发送方也真的发送1字节数据,这样会导致网络效率极低。
解决方案:
接收方:不通告小窗口。只有当缓冲区可用空间达到一定阈值(如MSS或缓冲区的1/2)时,才通告非零窗口。
发送方:不发送小数据段。可以累积数据直到满足一定条件(如达到MSS)再发送。Nagle算法就是做这个的。
4. 与拥塞控制的对比
对比项 流量控制 拥塞控制 目的 解决接收方处理不过来的问题 解决网络(中间路由器)处理不过来的问题 控制变量 接收窗口 (rwnd) 拥塞窗口 (cwnd) 作用于 单个TCP连接 所有TCP流(全局视角) 反馈来源 接收方的ACK包中的Window字段 网络中的丢包、延迟增加、ECN标记 最终发送窗口 = min(rwnd, cwnd)
5. 实际抓包观察
你可以用Wireshark抓包来观察流量控制过程:
查看
tcp.window_size字段,看窗口如何动态变化关注
Win=0的包(零窗口)关注
TCP ZeroWindow和TCP ZeroWindowProbe等提示这些都能直观地看到流量控制的运作。
总的来说,流量控制是TCP可靠性的重要一环------它通过接收方控制窗口大小,避免了因接收方处理能力不足导致的数据丢失。
拥塞窗口和拥塞控制
参考:
tcpip协议第15讲:tcp滑动窗口、拥塞窗口以及拥塞控制原理介绍_哔哩哔哩_bilibili
"我为人人,人人为我"的TCP拥塞控制_哔哩哔哩_bilibili拥塞控制如下所示:
你之前已经了解了流量控制 (解决接收方处理不过来的问题),而拥塞控制 解决的是另一个完全不同的问题:网络本身(中间路由器)处理不过来了。
这两者经常被混淆,我把它们放在一起对比,你就能看得很清楚了。
一、核心思想
拥塞控制的目标:避免发送方发送数据过快,导致网络中的路由器缓存溢出,从而引发丢包。
关键变量 :拥塞窗口 (cwnd, Congestion Window)。
发送方能发送的数据量 = min(接收窗口rwnd, 拥塞窗口cwnd)
rwnd 是接收方的限制(流量控制)
cwnd 是网络本身的限制(拥塞控制)
二、拥塞窗口(cwnd)的直观理解
想象一条高速公路(网络):
流量控制:收费站(接收方)说"我一次只能处理10辆车",这是接收方的能力。
拥塞控制:道路本身(网络)说"这条路上同时跑100辆车会堵死",这是网络的容量。
**拥塞窗口(cwnd)**就是发送方猜测的"目前网络能容纳的、尚未被确认的数据量"。
cwnd 小 → 发送方认为网络比较空(或者正处在拥塞状态)
cwnd 大 → 发送方认为网络比较通畅
重点 :cwnd 是发送方自己维护的变量,不会在TCP包里传输。抓包看不到cwnd的值,只能通过发送模式推断。
三、拥塞控制的四个核心算法
1. 慢启动 (Slow Start)
目标:快速探测网络的可用带宽。
机制 :初始时
cwnd = 1 MSS或10 MSS(现代TCP),每收到一个ACK,cwnd翻倍。
发送过程: 第1轮:发1个包 → 收1个ACK → cwnd = 2 第2轮:发2个包 → 收2个ACK → cwnd = 4 第3轮:发4个包 → 收4个ACK → cwnd = 8 第4轮:发8个包 → cwnd = 16 ...停止条件 :当
cwnd >= ssthresh(慢启动阈值)时,进入拥塞避免。
2. 拥塞避免 (Congestion Avoidance)
目标:缓慢增长,避免突然拥塞。
机制 :每收到一个ACK,cwnd增加
1/cwnd个MSS。实际上,每个RTT(一轮)cwnd增加1个MSS。
cwnd = 8 发8个包 → 收8个ACK → 每个ACK增加1/8 → 总共增加1 → cwnd = 9 发9个包 → 收9个ACK → cwnd = 10 ...特点:线性增长,温和地试探网络的极限。
3. 快速重传 (Fast Retransmit)
触发条件 :发送方收到 3个重复的ACK。
含义:接收方明确表示"我缺了某个包,请立刻重传"。
动作 :不等超时,立即重传丢失的包。这是对丢包的快速响应。
4. 快速恢复 (Fast Recovery)
这是快速重传之后的状态,不同的TCP实现略有差异,但核心思想是:
收到3个重复ACK时,网络还没有完全瘫痪(因为还能收到ACK)。
动作:
ssthresh = cwnd / 2
cwnd = ssthresh + 3(+3是因为收到了3个重复ACK,表示有3个包已离开网络)进入拥塞避免(不是慢启动)
结果:丢包后,发送速率减半,但不会降到1,避免过度反应。
四、丢包后的两种情况对比
事件 发送方动作 cwnd变化 ssthresh变化 收到3个重复ACK 快速重传 + 快速恢复 cwnd = cwnd/2 + 3 ssthresh = cwnd/2 超时(RTO到期) 超时重传 cwnd = 1 MSS ssthresh = cwnd/2(丢包前的一半) 区别:
重复ACK → 丢包可能是个别现象,网络还能工作 → 降半速
超时 → 网络可能严重拥塞 → 回到慢启动
六、流量控制 vs 拥塞控制(重要区分)
维度 流量控制 (Flow Control) 拥塞控制 (Congestion Control) 解决的问题 接收方处理不过来(缓冲区满) 网络处理不过来(路由器队列满) 控制变量 接收窗口 (rwnd) 拥塞窗口 (cwnd) 谁决定的 接收方(在ACK中通告) 发送方(自己推算) 抓包能看到吗 ✅ 能(TCP头部Window字段) ❌ 不能(只能推断) 代表机制 滑动窗口、零窗口探测 慢启动、拥塞避免、快速恢复 最终发送窗口 min(rwnd, cwnd) min(rwnd, cwnd) 一句话总结区别:
流量控制是接收方说"你慢点,我快吃不消了";拥塞控制是网络说"你慢点,路上已经堵死了"。发送方两者都要听,取最小值。
七、常见的拥塞控制算法演进
算法 特点 适用场景 Tahoe 早期版本:慢启动+拥塞避免,超时或重复ACK都回到慢启动 历史版本 Reno 增加快速恢复(3个重复ACK只降半速) 有线网络 NewReno 改进快速恢复,能处理多个丢包 有线网络 CUBIC 使用三次函数增长,更激进 Linux默认,高速网络 BBR 基于带宽和延迟模型,而非丢包 高延迟、高丢包网络(如跨国、卫星) 目前Linux默认通常是 CUBIC ,Google大力推广的 BBR 在很多场景表现更好。
八、实际推断cwnd的方法(抓包)
虽然不能直接看到cwnd,但可以通过以下方式推断:
Time-Sequence图:
横轴:时间
纵轴:序列号
斜率 = 发送速率
斜率变化点 = cwnd变化点
计算发送速率:
cwnd ≈ 发送速率 × RTT在Wireshark中:
Statistics → TCP Stream Graph → Throughput观察慢启动:
- 每个RTT内发出的包数量翻倍
观察拥塞避免:
- 每个RTT内发出的包数量增加1
观察丢包响应:
- 收到3个重复ACK后,发送速率立即减半
总结一句话
拥塞控制是TCP的"交通警察"------发送方通过维护拥塞窗口cwnd,使用慢启动、拥塞避免、快速重传、快速恢复等算法,动态感知网络负载,在保证不造成网络瘫痪的前提下,尽可能高地利用带宽。它与流量控制(接收方的能力限制)共同决定了TCP的实际发送速度。




