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 小时前
Glibc的版本在centos 7到9对应关系
linux·运维·centos
MediaTea2 小时前
Python:比较协议
运维·服务器·开发语言·网络·python
夜来小雨2 小时前
BGP高级特性-RR路由反射器
网络·智能路由器
7yewh2 小时前
AM57X Processor SDK Linux - run Installer
linux·嵌入式硬件·硬件架构·嵌入式
敲代码的哈吉蜂2 小时前
haproxy的算法——静态算法
linux·运维·服务器·算法
夜来小雨2 小时前
第二章 网络安全监督
网络·安全
8125035332 小时前
连接追踪:实现细节
网络
Web极客码2 小时前
WordPress 被植入隐藏管理员后门?清理实战分析
服务器·网络·wordpress
Y1rong2 小时前
linux之TCP
linux