netstat
netstat是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态
常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
pidof
在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id
我们前面学了一点点传输层的接口 但是没有详细学习传输层
1.UDP协议端格式
1. 16 位源端口号(Source Port)
- 作用 :标识发送该 UDP 数据报的应用程序端口。
- 当接收方需要回复时,会使用这个端口号作为目标端口,将响应数据送回正确的应用程序。
2. 16 位目的端口号(Destination Port)
- 作用 :标识接收该 UDP 数据报的目标应用程序端口。
- 网络层(IP)负责将数据报送到目标主机,传输层(UDP)则通过目的端口号,将数据交付给主机上正确的应用程序。
3. 16 位 UDP 长度(Length)
- 作用 :表示整个 UDP 数据报的总长度(单位:字节),包括8 字节报头 和数据部分。
- 最小值:8 字节(仅报头,无数据);最大值:65535 字节(受 16 位字段限制)。
- 注意:该字段存在一定冗余,因为 IP 层的总长度字段也可推算出 UDP 长度(UDP 长度 = IP 总长度 - IP 头长度)。
4. 16 位 UDP 检验和(Checksum)
- 作用:用于检测数据报在传输过程中是否发生错误(如比特翻转、丢失、篡改等)。
- 计算时会包含一个伪首部(由 IP 源地址、目的地址、协议类型、UDP 长度等组成),可检测到 IP 层的错误交付。
- 注意:在 IPv4 中,检验和是可选的(字段为 0 表示不启用);在 IPv6 中则是强制启用的。
5. 数据部分(Data)
- 作用:承载应用层的有效数据,长度由 UDP 长度字段决定(数据长度 = UDP 长度 - 8 字节报头)。
- 可以为空(此时 UDP 长度为 8),但通常用于传输应用数据(如 DNS 查询、视频流片段等)。
UDP****的特点
UDP传输的过程类似于寄信.
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层
返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量;
面向数据报
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
UDP****的缓冲区
UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后
续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果
缓冲区满了, 再到达的UDP数据就会被丢弃;
UDP的socket既能读, 也能写, 这个概念叫做 全双工
UDP****使用注意事项
我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首 部).
然而64K在当今的互联网环境下, 是一个非常小的数字.
就需要在应用层手动的分包, 多次发送如果我们需要传输的数据超过 , 并在接收端手动拼装;
基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议;
2.TCP****协议段格式

源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
32位序号/32位确认号: 后面详细讲;
4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节);
TCP 头部 = 固定头部(20 字节) + 选项部分(0~40 字节)
所以TCP头部最大长度是15 * 4 = 60
6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了
, 我们称携带FIN标识的为结束报文段
16位窗口大小: 后面再说
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 包含TCP数据部分.
16位紧急指针: 标识哪部分数据是紧急数据;
40字节头部选项: 暂时忽略
1.确认应答机制
编号(Seq)
发送方给每一段数据按顺序编号,比如 "第 1 段(Seq=1000)、第 2 段(Seq=2000)...",每个序号代表这段数据的起始字节位置。
确认(ACK)
接收方收到一段后,回复 "我已经按序收到了所有字节到 X,下一个期望接收的字节是 X+1",这个回复就是 ACK。也就是32位确认序号
- 关键:TCP 是累积确认,只认 "按序",不认 "先到"。
- 例子:如果 Seq=1000 的段丢了,但 Seq=2000 的段先到了,接收方会暂存 2000 段,但因为前面的 1000 段缺失,无法按序交付,所以会一直回复 ACK=1001,意思是 "我还在等 1000 开始的那段,下一个要从 1001 开始"。
- 也就是一般情况下都是按序到达
重传
如果发送方超时没收到 ACK,或者收到多次重复的 ACK(比如多次 ACK=1001),就认为对应编号的段丢了,重新发送对应编号的段(比如重传 Seq=1000 的段)。
终极总结
ACK 就是接收方给发送方的 "回执单",但 TCP 只认按序到达的数据 ------ 哪怕后面的数据先到,只要前面的丢了,就会一直要求重传前面的,直到按序补齐,确保数据完整有序。

2.超时重传机制

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了

因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包,
并且把重复的丢弃掉.
, 就可以很容易做到去重的效果. 这时候我们可以利用前面提到的序列号
那么, 如果超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
3.连接管理机制

我们前面学到的6位标志位:
在这里就能用到
虽然图上标的是报文
但是服务器和客户端每一次发送都是含有标志位的TCP报文
此外其他标志位也有作用
比如URG
他可以让一些紧急报文可以插队 而不是按序到达
URG = 1:表示报文中包含紧急数据,此时 16 位的紧急指针字段才有效。
URG = 0:紧急指针字段被忽略,报文中没有需要优先处理的紧急数据。
16位紧急指针
紧急指针里存的不是紧急数据,而是一个 16 位的偏移量(offset)。
它的作用是:和当前报文段的 ** 序号(Seq)** 相加,得到紧急数据最后一个字节的序号
紧急数据和普通数据混在一起,并没有独立的缓冲区:
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
PSH:标志位就是搭配16位窗口大小运作的
16位窗口大小
16 位窗口大小字段,本质上是接收方告诉发送方的 "自己还能收多少",所以:
- 窗口大小就是告诉对方,自己还能接收多少
- A 发报文给 B:窗口大小 = A 告诉 B:我 A 还能收多少
- B 发报文给 A:窗口大小 = B 告诉 A:我 B 还能收多少
-
谁来填这个值? 窗口大小字段是由接收方在 TCP 报文头中填写的,然后通过 ACK 报文发送给发送方。它的含义是:"我当前的接收缓冲区还能接收多少字节的数据,请你不要超过这个量发送。
-
发送方看到的是谁的状态? 发送方收到这个值后,知道的是接收方的接收缓冲区还剩多少空间,而不是自己的发送缓冲区是否满了。
- 如果窗口大小 > 0:接收方还有空间,发送方可以继续发送。
- 如果窗口大小 = 0:接收方缓冲区已满,发送方必须暂停发送,直到接收方更新窗口大小(零窗口探测机制)。
-
**发送方自己的发送缓冲区呢?**发送方的发送缓冲区是否满,是由发送方自己管理的,比如:
- 未被确认的数据量是否超过了拥塞窗口和接收窗口的最小值。(后面介绍)
- 发送队列是否还有空闲空间。窗口大小字段本身不直接反映发送方自己的缓冲区状态,但它限制了发送方可以发送的数据量,从而间接影响发送缓冲区的使用。
4.TIME_WAIT****状态

TIME_WAIT 是主动关闭 TCP 连接的一方,在四次挥手完成后必须进入的状态,持续时间为 2 个 MSL(最大报文生存时间),之后才会彻底关闭连接(进入 CLOSED 状态)。
产生场景:比如你用 Ctrl+C 终止了 server 进程,server 作为主动关闭连接的一方,就会进入 TIME_WAIT 状态。
MSL 是什么:MSL 是 TCP 报文在网络中能存活的最长时间,超过这个时间报文会被路由器丢弃。
为什么 TIME_WAIT 要等 2MSL?
这是 TCP 协议为了保证连接可靠关闭和数据完整性,两个作用:
防止旧连接的报文干扰新连接
如果 TIME_WAIT 时间太短,旧连接的迟到报文可能还没消失。
当新连接使用相同的五元组(源 IP、源端口、目的 IP、目的端口、协议)时,就可能收到旧连接的报文,导致数据错误。
等待 2MSL,能保证两个传输方向(主动关闭方 → 被动关闭方,被动关闭方 → 主动关闭方)的所有旧报文都已超时消失,新连接不会被旧数据干扰。
保证最后一个 ACK 可靠到达
主动关闭方(比如 server)发送最后一个 ACK 后,如果这个 ACK 在网络中丢失,被动关闭方(比如 client)会超时重发 FIN 报文。
主动关闭方在 TIME_WAIT 期间(2MSL 内)如果收到重发的 FIN,会重新发送 ACK,确保被动关闭方能正常关闭连接。
如果没有 TIME_WAIT,主动关闭方直接关闭,被动关闭方收不到 ACK,会一直重发 FIN,导致连接资源无法释放。
三、TIME_WAIT 带来的问题
端口占用导致 bind 失败
当 server 主动关闭连接后,端口会处于 TIME_WAIT 状态,此时再次启动 server 并 bind 同一个端口,会报错 bind error: Address already in use,因为端口还被 TIME_WAIT 连接占用。
例子:先启动 server,再启动 client,用 Ctrl+C 终止 server,立即重启 server 就会触发这个错误。
高并发场景下的资源耗尽
高并发服务中,server 会主动清理大量不活跃的短连接,产生大量 TIME_WAIT 连接。
每个 TIME_WAIT 连接都会占用一个五元组(源 IP、源端口、目的 IP、目的端口、协议)。
如果新连接的五元组和 TIME_WAIT 连接的五元组重复,新连接就无法建立,导致服务能力下降。
cpp
int opt = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt");
exit(1);
}
四、解决 TIME_WAIT 导致 bind 失败的方法
核心方案是使用 SO_REUSEADDR 套接字选项,允许端口在 TIME_WAIT 状态下被重新绑定。
- SO_REUSEADDR 的作用
允许 socket 在 bind 时,即使目标端口正处于 TIME_WAIT 状态,也能成功绑定到该端口(前提是新 socket 的 IP 或端口组合不冲突,或者同一 IP 下的不同 socket 类型)。
注意:SO_REUSEADDR 只是解决 bind 时的地址占用问题,不会减少 TIME_WAIT 连接的数量,只是让端口可以被快速复用。
5.CLOSE_WAIT****状态

为什么 "没有 close" 会产生 CLOSE_WAIT?
- 对端主动发 FIN,通知 "我要关了"
- 本地内核收到 FIN,自动回 ACK,进入 CLOSE_WAIT
- 此时内核已经知道对端要关了,但应用层还没调用
close(),所以内核不能主动发 FIN 去完成关闭 - 于是连接就一直卡在 CLOSE_WAIT,直到应用层调用
close(),内核才会发送 FIN,进入 LAST_ACK,最终关闭
CLOSE_WAIT 会带来什么问题?
- 资源泄漏:每个 CLOSE_WAIT 连接都会占用一个文件描述符和内存,大量堆积会导致文件描述符耗尽,服务无法接受新连接
- 连接假死:对端以为连接已经关闭,但本地还占着资源,导致状态不一致
6.滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.
这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时 间重叠在一起了

. 上图的窗口大小就是 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值 4000个字节(四个
段).
发送前四个段的时候, 不需要等待任何ACK, 直接发送;
收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区来记录当前还有哪些数据没有应答; 只有确
认应答过的数据, 才能从缓冲区删掉;
窗口越大, 则网络的吞吐率就越高;
在 TCP 的可靠传输机制中,如果中间数据段(比如 1~1000)丢失,但后续段(如 1001~2000)先到达,会按以下流程处理:
- 接收方的行为(主机 B)
- 确认逻辑 :TCP 是按序确认 的,接收方只会确认按序到达的最后一个字节。
- 当 1~1000 丢失,但 1001~2000 到达时,接收方发现前面的 1~1000 缺失,因此不会确认到 2000,而是持续发送确认号为
1001的 ACK,表示 "我期望下一个字节是 1001"。 - 但是如果1000到2000只有这一个缺失(比如1200) 这一个补上之后 就是2001而不是1201
- 这种机制被称为 "高速重发控制"(也叫 "快重传
- 当 1~1000 丢失,但 1001~2000 到达时,接收方发现前面的 1~1000 缺失,因此不会确认到 2000,而是持续发送确认号为
- 缓存乱序段 :接收方会将先到的 1001~2000 暂存到乱序缓冲区,等待缺失的 1~1000 到达后,再按序交付给应用层。
- 发送方的行为(主机 A)
- 快速重传机制 :当发送方收到3 个重复的 ACK (多次确认
1001)时,会立即判定 1~1000 段丢失,触发快速重传,重传该段,而无需等待超时定时器。 - 超时重传兜底:如果快速重传未触发(如重复 ACK 不足 3 次),发送方会在超时定时器到期后,重传丢失的段。
-
恢复流程
-
重传的 1~1000 段到达主机 B。
-
接收方检查乱序缓冲区,发现 1001~2000 已缓存,于是将 1~2000 按序交付给应用层。
-
接收方发送确认号为
2001的 ACK,告知发送方 "下一个期望字节是 2001"。 -
发送方收到新的确认后,继续发送后续数据段,恢复正常传输。
滑动窗口本质上是一个连续的字节范围,我们可以把它清晰地分成三部分:
-
已发送但未确认的部分(包括丢包 / 发送失败的)
- 这部分是已经从窗口里发出去的段,但接收方还没确认。
- 如果其中某个段丢了,它依然待在这部分里,直到被重传并确认。
- 这部分是窗口的 "左半段",从窗口左边界到 "已发送的最高序号"。
-
可发送但尚未发送的部分
- 这部分是窗口内还没发出去的段,但因为窗口大小允许,只要发送方有数据,就可以立刻发送。
- 这部分是窗口的 "右半段",从 "已发送的最高序号 + 1" 到窗口右边界。
-
窗口外,暂时不能发送的部分
- 这部分是窗口右边界之外的字节,因为窗口大小限制,现在还不能发送,必须等窗口向
- 右滑动后才能进入可发送范围。
- 左边界:已经被接收方确认的最后一个字节序号。
- 右边界:当前允许发送的最大字节序号,由接收方通告的窗口大小决定。
滑动的就是这两个边界,尤其是左边界,它的滑动是最核心的动作:
- 左边界向右滑动 → 表示旧的段已被确认,可以从发送缓冲区中移除,窗口 "向前移动"。
- 右边界向右滑动 → 表示接收方缓冲区有了更多空间,允许发送更多新数据。
窗口越大, 则网络的吞吐率就越高; - 窗口变大:接收方有空位 → 你多发
- 窗口不变:接收方速度稳定 → 正常发
- 窗口变小:接收方快满了 → 你少发 / 停发
窗口变大(右边界向右扩)
-
谁做的:接收方
-
原因 :应用层读走了很多数据,接收缓冲区空出很多空间
-
动作:接收方把 ACK 里的窗口字段填得更大
-
结果 :左边界往右滑 + 窗口变大→ 右边界向右滑得更远→ 发送方可以一次发更多数据
窗口不变(右边界跟着右滑,宽度不变)
-
谁做的:接收方
-
原因 :接收缓冲区剩余空间基本没变
-
动作:接收方通告的窗口大小和上次一样
-
结果 :左边界往右滑→ 右边界也往右滑,窗口宽度保持不变→ 发送节奏稳定,一次能发的数据量不变
窗口变小(右边界向右滑得少,甚至收缩)
-
谁做的:接收方
-
原因 :接收方处理不过来,接收缓冲区快满了
-
动作:接收方把窗口字段填小(甚至变成 0)
-
结果 :左边界往右滑→ 右边界只滑一点点,窗口变窄→ 发送方必须少发,甚至暂停发送
7.流量控制
流量控制(Flow Control)的核心,就是让发送端的发送速度,匹配接收端的处理能力,避免接收端缓冲区被撑爆,导致丢包、重传等连锁问题。
简单说:
- 接收端处理数据的速度有限
- 如果发送端一股脑猛发,接收端缓冲区满了,后续数据就会被丢弃
- 所以 TCP 设计了一套机制,让接收端 "告诉" 发送端:"我现在还能收多少,你别发太快"
流量控制的核心机制:窗口大小
流量控制的核心,就是 TCP 首部里的16 位窗口大小字段,它是接收端 "通告" 给发送端的 "接收能力"。
-
谁来填窗口大小? 由接收端根据自己接收缓冲区的剩余空间计算,然后在 ACK 报文中告诉发送端。
- 窗口大小越大 → 接收端还有很多空间,吞吐量越高
- 窗口大小越小 → 接收端快满了,发送端要减速
- 窗口大小 = 0 → 接收端缓冲区已满,发送端必须暂停发送
-
**发送端怎么响应?**发送端收到窗口大小后,会严格遵守:
- 窗口 > 0:按窗口大小控制发送节奏
- 窗口 = 0:停止发送,但需要定期发送窗口探测包,询问接收端 "现在有空了吗?"
我们以主机 A(发送)→ 主机 B(接收)为例:
-
初始阶段
- A 发送 1~1000 字节,B 回复 ACK:"下一个是 1001,窗口 3000" → 表示 B 还能收 3000 字节
- A 继续发 1001~2000、2001~3000、3001~4000,B 的窗口逐渐缩小:2000 → 1000 → 0
- 当窗口变为 0 时,B 的缓冲区满了,A 必须暂停发送
-
零窗口探测(关键)
- 窗口为 0 后,A 不能一直傻等,否则会 "死锁"
- A 会定期发送窗口探测包(比如图里的 4001~4001),探测 B 的缓冲区是否有空余
- B 收到探测包后,如果缓冲区有空间了,会回复 "窗口更新通知",告诉 A 新的窗口大小(比如图里的 2000)
-
恢复发送
- A 收到窗口更新(2000)后,知道 B 又能收了,继续发送 4001~5000、5001~6000
- 注意:如果窗口更新通知在传输中丢失,A 会再次发送探测包,确保不会永远卡住
8.拥塞控制
拥塞控制在以下 4 种核心场景下起作用:
- 连接刚建立时:用慢启动(指数增长)探路,避免初始发送过载。
- 收到 3 个重复 ACK:判定轻度丢包,触发快速重传 + 快速恢复,线性恢复传输。
- 超时重传(RTO):判定严重拥塞,窗口重置为 1,重新慢启动。
- 窗口超过慢启动阈值后:进入拥塞避免,线性增长,防止网络过载。
流量控制是防止接收端缓冲区溢出 (端到端),而拥塞控制是防止网络本身过载(端到网络)。
网络里有很多路由器和链路,如果发送端一股脑猛发,会导致:
- 路由器队列爆满 → 大量丢包
- 丢包触发重传 → 网络压力更大(雪上加霜)
所以 TCP 设计了一套 **"先探路、再加速、遇堵就退"** 的机制,这就是拥塞控制。
-
拥塞窗口(cwnd, Congestion Window)
- 由发送端维护的一个窗口,用来限制自己的发送量。
- 实际发送窗口 = min (拥塞窗口 cwnd, 接收端通告窗口 rwnd)
- 单位:MSS(最大段大小,比如 1 个段 = 1000 字节)
-
慢启动阈值(ssthresh, Slow Start Threshold)
- 一个临界值,用来区分 "慢启动" 和 "拥塞避免" 两个阶段。
- 初始值通常等于接收端最大窗口大小(比如 65535 字节)。
三、慢启动(Slow Start):先探路,快速涨
规则:
- 初始时,
cwnd = 1(只能发 1 个段)。 - 每收到 1 个 ACK,
cwnd += 1→ 指数增长(1→2→4→8→16...)。 - 当
cwnd < ssthresh时,执行慢启动。
为什么叫 "慢启动"?
- 初始只发 1 个段,看起来 "慢",但增长速度是指数级的,能快速摸清网络容量。
例子:
- 初始:cwnd=1 → 发 1 段(1~1000)
- 收到 ACK:cwnd=2 → 发 2 段(1001~2000、2001~3000)
- 收到 2 个 ACK:cwnd=4 → 发 4 段(3001~4000、4001~5000、5001~6000、6001~7000)
- ...... 指数增长,直到 cwnd 达到 ssthresh。
四、拥塞避免(Congestion Avoidance):稳着涨,线性增
规则:
- 当
cwnd ≥ ssthresh时,进入拥塞避免。 - 每个 RTT(往返时间)内,
cwnd += 1→ 线性增长(比如 8→9→10→11...)。
为什么要线性增长?
- 避免指数增长太快导致网络瞬间过载,用 "加法增大" 的方式稳步提升吞吐量。
五、遇到拥塞了怎么办?两种处理方式
- 超时重传(RTO,Retransmission Timeout)
- 判定 :长时间没收到 ACK,认为网络严重拥塞。
- 动作 :
ssthresh = cwnd / 2(乘法减小)cwnd = 1(重置到初始状态)- 重新进入慢启动。
- 快速重传 + 快速恢复(3 个重复 ACK)
- 判定 :收到 3 个重复 ACK,认为网络轻度拥塞(某个段丢了,但后续段还在传)。
- 动作 :
ssthresh = cwnd / 2(乘法减小)cwnd = ssthresh(或ssthresh + 3,不同实现略有差异)- 直接进入拥塞避免(线性增长),而不是重置到 1,恢复更快。
9.延迟应答
延迟应答是 TCP 接收端的一种优化策略:不立刻对每个报文发 ACK,而是稍微等一会儿,等收到更多数据或超时后再统一应答。
核心目标:
- 让返回的窗口更大(因为应用层可能已经消费了缓冲区数据),提升吞吐量。
- 减少 ACK 报文的数量,降低网络开销。
为了避免发送端长时间收不到 ACK 导致超时重传,延迟应答有两个硬限制:
- 数量限制:每收到 N 个报文就必须应答一次(一般 N=2,即每 2 个段发一次 ACK)。
- 时间限制:超过最大延迟时间(一般 200ms)就必须应答,不管收了多少报文。
假设:
- 接收端缓冲区大小:1M
- 收到数据:500K
- 应用层处理速度:10ms 内就把 500K 消费掉,缓冲区又空了
如果立刻应答:
- 返回窗口 = 1M - 500K = 500K
- 发送端只能再发 500K 数据,效率低
如果延迟 200ms 应答:
- 应用层已经把 500K 消费完,缓冲区空了
- 返回窗口 = 1M
- 发送端可以发 1M 数据,吞吐量翻倍
10.捎带应答
在延迟应答的基础上,如果应用层刚好有数据要回传给发送端(比如客户端问 "How are you",服务器回 "Fine, thank you"),就可以把 ACK 和这个应用数据合并到一个报文里一起发送,这就是捎带应答。
好处:
- 节省了一个单独的 ACK 报文,进一步降低网络开销。
- 提升了传输效率,因为一次报文既完成了确认,又传递了应用数据。
- 三次握手的其中一次就是捎带应答

11.面向字节流
简单点理解TCP有独立的接收缓冲区和发送缓冲区
并且TCP是字节流从缓冲区 读取的
而不是像udp一样一次读取一个完整的报文
TCP报文过长的时候会把数据切片 组成多个报文
应用层拿到的是拼好的
怎么知道有没有切片丢失?
靠 序号(Sequence Number) + 确认号(Acknowledgment Number):
- 每个 TCP 段的头部都有一个序号 ,它代表这个段里第一个字节 的编号。
- 比如:1~1000 字节的段,序号是
1;1001~2000 字节的段,序号是1001。
- 比如:1~1000 字节的段,序号是
- 接收端收到段后,会返回一个确认号 ,表示 "我已经按序收到了所有到这个序号之前的字节,下一个期望的字节是这个确认号"。
- 如果 1001~2000 这个段丢了,即使后面 2001~3000 的段先到了,接收端也只会一直确认
1001,不会确认到 3001。
- 如果 1001~2000 这个段丢了,即使后面 2001~3000 的段先到了,接收端也只会一直确认
- 发送端收到3 个重复的确认号 (比如一直确认 1001),就立刻知道 1001~2000 这个段丢了,触发快速重传,把它补发回来。
怎么知道哪些切片要合并、不会搞混?
靠 序号的连续性 + 内核自动重组:
- TCP 的序号是按字节编号 的,不是按段编号的。每个字节都有唯一的序号,不管怎么切片,序号都是连续的。
- 比如:不管是一次发 1000 字节,还是分 10 次发 100 字节,序号都是从 1 开始连续递增的。
- 接收端的内核会维护一个接收缓冲区 ,收到段后:
- 先按序号把段排序,不管它们是按什么顺序到达的。
- 把先到但序号不连续的段(比如 2001~3000)暂存在乱序缓冲区里。
- 等缺失的段(1001~2000)补发回来后,内核会自动把 1~3000 的字节按顺序合并成连续的字节流,再交给应用层。
- 应用层调用
read时,看到的已经是内核重组好的连续字节,完全不用关心切片的边界,自然不会搞混。
12.粘包问题
粘包问题里的 "包",指的是应用层的数据包。
- TCP 是面向字节流的协议,传输层只按序号把报文段排好放进内核缓冲区,没有为应用层保留 "包边界"。
- 应用层从缓冲区读到的是一串连续的字节,无法区分哪段字节对应一个完整的应用层数据包,这就是 "粘包"。
-
定长包 约定每个应用包的大小固定,应用层每次按这个固定长度从缓冲区读取即可。例:按
sizeof(Request)依次读取固定大小的请求结构体。 -
**变长包(包头带长度)**在应用包的头部增加一个 "总长度" 字段,应用层先读长度字段,再按长度读取完整包体。
-
**变长包(分隔符)**在应用包之间使用明确的分隔符(如换行符、特殊标记),只要分隔符不与正文冲突即可,应用层按分隔符切分字节流。
UDP 不存在粘包问题。
- UDP 是面向报文的协议,每个 UDP 报文都有明确的长度字段。
- 内核交付给应用层时,要么是一个完整的 UDP 报文,要么不收,不会出现 "半个报文" 的情况,因此应用层能清晰识别每个报文的边界。
13.TCP****异常情况
一、进程终止
进程终止时,内核会自动释放文件描述符,并发送 FIN 报文,和正常调用 close() 关闭连接的流程一致,属于优雅关闭。
二、机器重启
机器重启时,内核会清理所有连接并发送 FIN,和进程终止的情况相同,也是优雅关闭。
三、机器掉电 / 网线断开(突然中断)
这种情况属于非优雅关闭:
-
发送端突然中断,来不及发送
FIN,接收端一开始仍认为连接存在。 -
当接收端尝试向该连接写入数据时,会发现连接已失效,发送
RST复位报文。 -
TCP 自带保活定时器(keepalive):长时间无数据交互时,会定期探测对方,若对方无响应,内核会自动释放连接。
3.listen****的第二个参数
cpp
int listen(int sockfd, int backlog);
第二个参数 backlog 是连接队列的长度上限,核心是管控 TCP 三次握手过程中两类队列的容量,先搞懂两个关键队列:
- 半连接队列(SYN 队列):存放 "已收到客户端 SYN,但还没完成三次握手" 的连接(状态:SYN_RCVD)。
- 全连接队列(Accept 队列) :存放 "已完成三次握手(状态:ESTABLISHED),但应用层还没调用
accept()取走" 的连接。
当全连接队列满了,新完成三次握手的连接会被内核直接拒绝,内核向客户端发送 RST 报文,客户端会报 Connection refused 错误。
- Linux 2.2 是分界点 (主流系统都遵循):
- 2.2 之前:
backlog= 半连接队列 + 全连接队列 的总上限。 - 2.2 之后:
backlog仅指定全连接队列 的上限;半连接队列的上限由内核参数tcp_max_syn_backlog控制(默认约 1024)。
- 2.2 之前:
- 实际生效的上限还受系统内核参数
tcp_somaxconn限制(默认 128):即使你把backlog设为 256,实际上限也会被钳位到 128,需修改该参数才能突破。
