Linux 网络 (4)

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 还能收多少
  1. 谁来填这个值? 窗口大小字段是由接收方在 TCP 报文头中填写的,然后通过 ACK 报文发送给发送方。它的含义是:"我当前的接收缓冲区还能接收多少字节的数据,请你不要超过这个量发送。

  2. 发送方看到的是谁的状态? 发送方收到这个值后,知道的是接收方的接收缓冲区还剩多少空间,而不是自己的发送缓冲区是否满了。

    • 如果窗口大小 > 0:接收方还有空间,发送方可以继续发送。
    • 如果窗口大小 = 0:接收方缓冲区已满,发送方必须暂停发送,直到接收方更新窗口大小(零窗口探测机制)。
  3. **发送方自己的发送缓冲区呢?**发送方的发送缓冲区是否满,是由发送方自己管理的,比如:

    • 未被确认的数据量是否超过了拥塞窗口和接收窗口的最小值。(后面介绍)
    • 发送队列是否还有空闲空间。窗口大小字段本身不直接反映发送方自己的缓冲区状态,但它限制了发送方可以发送的数据量,从而间接影响发送缓冲区的使用。

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 状态下被重新绑定。

  1. 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)先到达,会按以下流程处理:


  1. 接收方的行为(主机 B)
  • 确认逻辑 :TCP 是按序确认 的,接收方只会确认按序到达的最后一个字节
    • 当 1~1000 丢失,但 1001~2000 到达时,接收方发现前面的 1~1000 缺失,因此不会确认到 2000,而是持续发送确认号为1001的 ACK,表示 "我期望下一个字节是 1001"。
    • 但是如果1000到2000只有这一个缺失(比如1200) 这一个补上之后 就是2001而不是1201
    • 这种机制被称为 "高速重发控制"(也叫 "快重传
  • 缓存乱序段 :接收方会将先到的 1001~2000 暂存到乱序缓冲区,等待缺失的 1~1000 到达后,再按序交付给应用层。
  1. 发送方的行为(主机 A)
  • 快速重传机制 :当发送方收到3 个重复的 ACK (多次确认1001)时,会立即判定 1~1000 段丢失,触发快速重传,重传该段,而无需等待超时定时器。
  • 超时重传兜底:如果快速重传未触发(如重复 ACK 不足 3 次),发送方会在超时定时器到期后,重传丢失的段。
  1. 恢复流程

  2. 重传的 1~1000 段到达主机 B。

  3. 接收方检查乱序缓冲区,发现 1001~2000 已缓存,于是将 1~2000 按序交付给应用层。

  4. 接收方发送确认号为2001的 ACK,告知发送方 "下一个期望字节是 2001"。

  5. 发送方收到新的确认后,继续发送后续数据段,恢复正常传输。

滑动窗口本质上是一个连续的字节范围,我们可以把它清晰地分成三部分:

  1. 已发送但未确认的部分(包括丢包 / 发送失败的)

    • 这部分是已经从窗口里发出去的段,但接收方还没确认。
    • 如果其中某个段丢了,它依然待在这部分里,直到被重传并确认。
    • 这部分是窗口的 "左半段",从窗口左边界到 "已发送的最高序号"。
  2. 可发送但尚未发送的部分

    • 这部分是窗口内还没发出去的段,但因为窗口大小允许,只要发送方有数据,就可以立刻发送。
    • 这部分是窗口的 "右半段",从 "已发送的最高序号 + 1" 到窗口右边界。
  3. 窗口外,暂时不能发送的部分

    • 这部分是窗口右边界之外的字节,因为窗口大小限制,现在还不能发送,必须等窗口向
    • 右滑动后才能进入可发送范围。
  • 左边界:已经被接收方确认的最后一个字节序号。
  • 右边界:当前允许发送的最大字节序号,由接收方通告的窗口大小决定。

滑动的就是这两个边界,尤其是左边界,它的滑动是最核心的动作:

  • 左边界向右滑动 → 表示旧的段已被确认,可以从发送缓冲区中移除,窗口 "向前移动"。
  • 右边界向右滑动 → 表示接收方缓冲区有了更多空间,允许发送更多新数据。
    窗口越大, 则网络的吞吐率就越高;
  • 窗口变大:接收方有空位 → 你多发
  • 窗口不变:接收方速度稳定 → 正常发
  • 窗口变小:接收方快满了 → 你少发 / 停发

窗口变大(右边界向右扩)

  • 谁做的:接收方

  • 原因 :应用层读走了很多数据,接收缓冲区空出很多空间

  • 动作:接收方把 ACK 里的窗口字段填得更大

  • 结果 :左边界往右滑 + 窗口变大→ 右边界向右滑得更远→ 发送方可以一次发更多数据

窗口不变(右边界跟着右滑,宽度不变)

  • 谁做的:接收方

  • 原因 :接收缓冲区剩余空间基本没变

  • 动作:接收方通告的窗口大小和上次一样

  • 结果 :左边界往右滑→ 右边界也往右滑,窗口宽度保持不变→ 发送节奏稳定,一次能发的数据量不变

窗口变小(右边界向右滑得少,甚至收缩)

  • 谁做的:接收方

  • 原因 :接收方处理不过来,接收缓冲区快满了

  • 动作:接收方把窗口字段填小(甚至变成 0)

  • 结果 :左边界往右滑→ 右边界只滑一点点,窗口变窄→ 发送方必须少发,甚至暂停发送

7.流量控制

流量控制(Flow Control)的核心,就是让发送端的发送速度,匹配接收端的处理能力,避免接收端缓冲区被撑爆,导致丢包、重传等连锁问题。

简单说:

  • 接收端处理数据的速度有限
  • 如果发送端一股脑猛发,接收端缓冲区满了,后续数据就会被丢弃
  • 所以 TCP 设计了一套机制,让接收端 "告诉" 发送端:"我现在还能收多少,你别发太快"

流量控制的核心机制:窗口大小

流量控制的核心,就是 TCP 首部里的16 位窗口大小字段,它是接收端 "通告" 给发送端的 "接收能力"。

  1. 谁来填窗口大小?接收端根据自己接收缓冲区的剩余空间计算,然后在 ACK 报文中告诉发送端。

    • 窗口大小越大 → 接收端还有很多空间,吞吐量越高
    • 窗口大小越小 → 接收端快满了,发送端要减速
    • 窗口大小 = 0 → 接收端缓冲区已满,发送端必须暂停发送
  2. **发送端怎么响应?**发送端收到窗口大小后,会严格遵守:

    • 窗口 > 0:按窗口大小控制发送节奏
    • 窗口 = 0:停止发送,但需要定期发送窗口探测包,询问接收端 "现在有空了吗?"

我们以主机 A(发送)→ 主机 B(接收)为例:

  1. 初始阶段

    • A 发送 1~1000 字节,B 回复 ACK:"下一个是 1001,窗口 3000" → 表示 B 还能收 3000 字节
    • A 继续发 1001~2000、2001~3000、3001~4000,B 的窗口逐渐缩小:2000 → 1000 → 0
    • 当窗口变为 0 时,B 的缓冲区满了,A 必须暂停发送
  2. 零窗口探测(关键)

    • 窗口为 0 后,A 不能一直傻等,否则会 "死锁"
    • A 会定期发送窗口探测包(比如图里的 4001~4001),探测 B 的缓冲区是否有空余
    • B 收到探测包后,如果缓冲区有空间了,会回复 "窗口更新通知",告诉 A 新的窗口大小(比如图里的 2000)
  3. 恢复发送

    • A 收到窗口更新(2000)后,知道 B 又能收了,继续发送 4001~5000、5001~6000
    • 注意:如果窗口更新通知在传输中丢失,A 会再次发送探测包,确保不会永远卡住

8.拥塞控制

拥塞控制在以下 4 种核心场景下起作用:

  1. 连接刚建立时:用慢启动(指数增长)探路,避免初始发送过载。
  2. 收到 3 个重复 ACK:判定轻度丢包,触发快速重传 + 快速恢复,线性恢复传输。
  3. 超时重传(RTO):判定严重拥塞,窗口重置为 1,重新慢启动。
  4. 窗口超过慢启动阈值后:进入拥塞避免,线性增长,防止网络过载。

流量控制是防止接收端缓冲区溢出 (端到端),而拥塞控制是防止网络本身过载(端到网络)。

网络里有很多路由器和链路,如果发送端一股脑猛发,会导致:

  • 路由器队列爆满 → 大量丢包
  • 丢包触发重传 → 网络压力更大(雪上加霜)

所以 TCP 设计了一套 **"先探路、再加速、遇堵就退"** 的机制,这就是拥塞控制。


  1. 拥塞窗口(cwnd, Congestion Window)

    • 发送端维护的一个窗口,用来限制自己的发送量。
    • 实际发送窗口 = min (拥塞窗口 cwnd, 接收端通告窗口 rwnd)
    • 单位:MSS(最大段大小,比如 1 个段 = 1000 字节)
  2. 慢启动阈值(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...)。

为什么要线性增长?

  • 避免指数增长太快导致网络瞬间过载,用 "加法增大" 的方式稳步提升吞吐量。

五、遇到拥塞了怎么办?两种处理方式

  1. 超时重传(RTO,Retransmission Timeout)
  • 判定 :长时间没收到 ACK,认为网络严重拥塞
  • 动作
    1. ssthresh = cwnd / 2(乘法减小)
    2. cwnd = 1(重置到初始状态)
    3. 重新进入慢启动。
  1. 快速重传 + 快速恢复(3 个重复 ACK)
  • 判定 :收到 3 个重复 ACK,认为网络轻度拥塞(某个段丢了,但后续段还在传)。
  • 动作
    1. ssthresh = cwnd / 2(乘法减小)
    2. cwnd = ssthresh(或 ssthresh + 3,不同实现略有差异)
    3. 直接进入拥塞避免(线性增长),而不是重置到 1,恢复更快。

9.延迟应答

延迟应答是 TCP 接收端的一种优化策略:不立刻对每个报文发 ACK,而是稍微等一会儿,等收到更多数据或超时后再统一应答

核心目标:

  • 让返回的窗口更大(因为应用层可能已经消费了缓冲区数据),提升吞吐量。
  • 减少 ACK 报文的数量,降低网络开销。

为了避免发送端长时间收不到 ACK 导致超时重传,延迟应答有两个硬限制:

  1. 数量限制:每收到 N 个报文就必须应答一次(一般 N=2,即每 2 个段发一次 ACK)。
  2. 时间限制:超过最大延迟时间(一般 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
  • 接收端收到段后,会返回一个确认号 ,表示 "我已经按序收到了所有到这个序号之前的字节,下一个期望的字节是这个确认号"。
    • 如果 1001~2000 这个段丢了,即使后面 2001~3000 的段先到了,接收端也只会一直确认 1001,不会确认到 3001。
  • 发送端收到3 个重复的确认号 (比如一直确认 1001),就立刻知道 1001~2000 这个段丢了,触发快速重传,把它补发回来。

怎么知道哪些切片要合并、不会搞混?

序号的连续性 + 内核自动重组

  • TCP 的序号是按字节编号 的,不是按段编号的。每个字节都有唯一的序号,不管怎么切片,序号都是连续的。
    • 比如:不管是一次发 1000 字节,还是分 10 次发 100 字节,序号都是从 1 开始连续递增的。
  • 接收端的内核会维护一个接收缓冲区 ,收到段后:
    1. 先按序号把段排序,不管它们是按什么顺序到达的。
    2. 把先到但序号不连续的段(比如 2001~3000)暂存在乱序缓冲区里。
    3. 等缺失的段(1001~2000)补发回来后,内核会自动把 1~3000 的字节按顺序合并成连续的字节流,再交给应用层。
  • 应用层调用 read 时,看到的已经是内核重组好的连续字节,完全不用关心切片的边界,自然不会搞混。

12.粘包问题

粘包问题里的 "包",指的是应用层的数据包

  • TCP 是面向字节流的协议,传输层只按序号把报文段排好放进内核缓冲区,没有为应用层保留 "包边界"
  • 应用层从缓冲区读到的是一串连续的字节,无法区分哪段字节对应一个完整的应用层数据包,这就是 "粘包"。
  1. 定长包 约定每个应用包的大小固定,应用层每次按这个固定长度从缓冲区读取即可。例:按 sizeof(Request) 依次读取固定大小的请求结构体。

  2. **变长包(包头带长度)**在应用包的头部增加一个 "总长度" 字段,应用层先读长度字段,再按长度读取完整包体。

  3. **变长包(分隔符)**在应用包之间使用明确的分隔符(如换行符、特殊标记),只要分隔符不与正文冲突即可,应用层按分隔符切分字节流。


UDP 不存在粘包问题

  • UDP 是面向报文的协议,每个 UDP 报文都有明确的长度字段。
  • 内核交付给应用层时,要么是一个完整的 UDP 报文,要么不收,不会出现 "半个报文" 的情况,因此应用层能清晰识别每个报文的边界。

13.TCP****异常情况

一、进程终止

进程终止时,内核会自动释放文件描述符,并发送 FIN 报文,和正常调用 close() 关闭连接的流程一致,属于优雅关闭

二、机器重启

机器重启时,内核会清理所有连接并发送 FIN,和进程终止的情况相同,也是优雅关闭。

三、机器掉电 / 网线断开(突然中断)

这种情况属于非优雅关闭

  • 发送端突然中断,来不及发送 FIN,接收端一开始仍认为连接存在。

  • 当接收端尝试向该连接写入数据时,会发现连接已失效,发送 RST 复位报文。

  • TCP 自带保活定时器(keepalive):长时间无数据交互时,会定期探测对方,若对方无响应,内核会自动释放连接。

3.listen****的第二个参数

cpp 复制代码
int listen(int sockfd, int backlog);

第二个参数 backlog连接队列的长度上限,核心是管控 TCP 三次握手过程中两类队列的容量,先搞懂两个关键队列:

  1. 半连接队列(SYN 队列):存放 "已收到客户端 SYN,但还没完成三次握手" 的连接(状态:SYN_RCVD)。
  2. 全连接队列(Accept 队列) :存放 "已完成三次握手(状态:ESTABLISHED),但应用层还没调用 accept() 取走" 的连接。

当全连接队列满了,新完成三次握手的连接会被内核直接拒绝,内核向客户端发送 RST 报文,客户端会报 Connection refused 错误。

  • Linux 2.2 是分界点 (主流系统都遵循):
    • 2.2 之前:backlog = 半连接队列 + 全连接队列 的总上限。
    • 2.2 之后:backlog 仅指定全连接队列 的上限;半连接队列的上限由内核参数 tcp_max_syn_backlog 控制(默认约 1024)。
  • 实际生效的上限还受系统内核参数 tcp_somaxconn 限制(默认 128):即使你把 backlog 设为 256,实际上限也会被钳位到 128,需修改该参数才能突破。
相关推荐
寂柒1 小时前
信号量——基于环形队列的生产消费模型
linux·ubuntu
一袋米扛几楼982 小时前
【密码学】CrypTool2 工具是什么?
服务器·网络·密码学
林姜泽樾5 小时前
Linux入门第十二章,创建用户、用户组、主组附加组等相关知识详解
linux·运维·服务器·centos
xiaokangzhe6 小时前
Linux系统安全
linux·运维·系统安全
feng一样的男子6 小时前
NFS 扩展属性 (xattr) 提示操作不支持解决方案
linux·go
南棱笑笑生6 小时前
20260310在瑞芯微原厂RK3576的Android14查看系统休眠时间
服务器·网络·数据库·rockchip
yy55276 小时前
LNAMP 网络架构与部署
网络·架构
Highcharts.js7 小时前
Highcharts React v4.2.1 正式发布:更自然的React开发体验,更清晰的数据处理
linux·运维·javascript·ubuntu·react.js·数据可视化·highcharts
Godspeed Zhao7 小时前
现代智能汽车系统——CAN网络2
网络·汽车
c++之路7 小时前
Linux网络协议与编程基础:TCP/IP协议族全解析
linux·网络协议·tcp/ip