TCP可靠传输的秘密:从滑动窗口到拥塞控制

TCP可靠传输的秘密:从滑动窗口到拥塞控制

目录

  • 一、前言
  • 二、TCP可靠性机制概述
  • 三、ACK确认机制
    • [3.1 序列号和确认号](#3.1 序列号和确认号)
    • [3.2 累积确认](#3.2 累积确认)
    • [3.3 选择确认SACK](#3.3 选择确认SACK)
    • [3.4 捎带确认](#3.4 捎带确认)
  • 四、超时重传机制
    • [4.1 超时重传的基本原理](#4.1 超时重传的基本原理)
    • [4.2 RTO计算](#4.2 RTO计算)
    • [4.3 快速重传](#4.3 快速重传)
    • [4.4 重传次数限制](#4.4 重传次数限制)
  • 五、滑动窗口机制
    • [5.1 为什么需要滑动窗口](#5.1 为什么需要滑动窗口)
    • [5.2 发送窗口](#5.2 发送窗口)
    • [5.3 接收窗口](#5.3 接收窗口)
    • [5.4 零窗口探测](#5.4 零窗口探测)
    • [5.5 滑动窗口与send()的关系](#5.5 滑动窗口与send()的关系)
    • [5.6 Reactor项目实战](#5.6 Reactor项目实战)
  • 六、拥塞控制机制
    • [6.1 拥塞控制 vs 流量控制](#6.1 拥塞控制 vs 流量控制)
    • [6.2 慢启动](#6.2 慢启动)
    • [6.3 拥塞避免](#6.3 拥塞避免)
    • [6.4 快速重传和快速恢复](#6.4 快速重传和快速恢复)
    • [6.5 拥塞控制算法演进](#6.5 拥塞控制算法演进)
    • [6.6 Linux内核源码分析](#6.6 Linux内核源码分析)
  • 七、面试高频题总结
  • 八、总结
  • 参考资料

一、前言

学完TCP三次握手和四次挥手后,我以为TCP就是这样了。但写Reactor项目的时候,遇到了更多问题:

  • send()函数有时候返回值小于发送的长度,为什么?
  • 客户端发送大量数据,服务器处理不过来,会发生什么?
  • TCP如何保证数据不丢、不乱、不重?
  • 什么是滑动窗口?拥塞窗口又是什么?

看王道视频的时候,我对这些概念有了基本认识。但真正理解,还是在写项目遇到问题、查资料、看Linux内核源码之后。特别是当我看到内核中tcp_write_xmit()函数如何决定发送多少数据时,才恍然大悟:原来TCP的可靠性和流量控制,全都在这里体现。

今天把这部分内容系统地整理出来,分享给大家。

本文包含:

  • TCP可靠性的三大机制(ACK确认、超时重传、滑动窗口)
  • 滑动窗口的完整工作流程(发送窗口、接收窗口、零窗口探测)
  • 拥塞控制的四个阶段(慢启动、拥塞避免、快速重传、快速恢复)
  • Linux内核源码分析(简化版,理解核心逻辑)
  • Reactor项目中如何处理send()recv()

二、TCP可靠性机制概述

TCP是可靠传输协议,建立在不可靠的IP层之上。IP层可能会丢包、乱序、重复,但TCP保证应用层看到的数据是有序的、不丢的、不重的。

TCP可靠性的三大支柱:

  1. 序列号(seq)和确认号(ack)

    • 每个字节都有序列号
    • 接收方通过ack确认收到了哪些数据
  2. 超时重传(RTO)

    • 发送方发送数据后启动定时器
    • 超时未收到ACK,重传数据
  3. 滑动窗口(流量控制)

    • 接收方告诉发送方自己能接收多少数据(rwnd)
    • 发送方根据rwnd控制发送速率

这三个机制协同工作,保证了TCP的可靠性。


三、ACK确认机制

3.1 序列号和确认号

TCP的每个字节都有序列号。这是我刚开始学习时最困惑的地方:为什么不是每个包有序列号,而是每个字节?

原因: TCP是面向字节流的,不关心"包"的概念。即使网络层把一个TCP报文分片了,TCP层也不管,只关心字节流。

复制代码
发送方发送数据:

字节流:H  e  l  l  o  W  o  r  l  d
序列号:100 101 102 103 104 105 106 107 108 109

TCP报文:
  seq=100, len=5, data="Hello"
  seq=105, len=5, data="World"

确认号(ack)的含义: 期望接收的下一个字节序号

复制代码
接收方收到seq=100, len=5的数据:
  收到了100, 101, 102, 103, 104这5个字节
  期望下一个字节是105
  发送:ACK, ack=105

重要:ack=105表示"105之前的都收到了"

面试易错点: ack=500表示什么?

很多人答:收到了500号字节。错!

正确答案:收到了499号字节及之前的所有字节,期望接收500号字节。

3.2 累积确认

TCP使用累积确认机制:ack表示这个序号之前的所有字节都收到了。

复制代码
发送方连续发送:
  seq=100, len=100  (100-199)
  seq=200, len=100  (200-299)
  seq=300, len=100  (300-399)

接收方收到:
  seq=100, len=100 → 发送ACK, ack=200
  seq=200, len=100 → 发送ACK, ack=300
  seq=300, len=100 → 发送ACK, ack=400

优势: 减少ACK数量。如果每个字节都确认,开销太大。

问题: 中间丢包时,无法告诉发送方具体哪些收到了。

复制代码
发送方发送:
  seq=100, len=100  (100-199)
  seq=200, len=100  (200-299) → 丢失
  seq=300, len=100  (300-399)

接收方:
  收到seq=100 → 发送ACK, ack=200
  收到seq=300 → 但200-299没收到,只能发送ACK, ack=200
  
发送方:
  收到ack=200
  收到ack=200 (重复)
  → 无法知道300-399是否收到

解决方法:选择确认(SACK)。

3.3 选择确认(SACK)

SACK(Selective Acknowledgment)告诉发送方具体哪些数据收到了。

复制代码
发送方发送:
  seq=100, len=100  (100-199)
  seq=200, len=100  (200-299) → 丢失
  seq=300, len=100  (300-399)
  seq=400, len=100  (400-499)

接收方:
  收到seq=100 → ACK, ack=200
  收到seq=300 → ACK, ack=200, SACK=300-400 (300-399收到了)
  收到seq=400 → ACK, ack=200, SACK=300-400, 400-500
  
发送方:
  ack=200:说明200-299没收到
  SACK=300-400, 400-500:说明300-499收到了
  → 只需重传200-299

Linux中启用SACK:

bash 复制代码
# 查看
cat /proc/sys/net/ipv4/tcp_sack

# 启用(默认已启用)
echo 1 > /proc/sys/net/ipv4/tcp_sack

3.4 捎带确认

TCP的ACK可以和数据一起发送,这叫捎带确认(Piggybacking)。

复制代码
客户端和服务器交互:

客户端 → 服务器:seq=100, len=10, data="GET /index.html"
服务器 → 客户端:seq=200, ack=110, len=1024, data="HTTP/1.1 200 OK..."
                  ↑                  ↑
                  数据               捎带ACK

不需要单独的ACK包

优势: 减少网络流量。

Nagle算法: 为了减少小包,TCP会等待一段时间,把多个小数据合并发送。但这会增加延迟。

TCP_NODELAY: 禁用Nagle算法,立即发送数据。

cpp 复制代码
// 在Reactor项目中禁用Nagle算法
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

四、超时重传机制

4.1 超时重传的基本原理

发送方发送数据后,启动一个定时器。如果在规定时间内没收到ACK,就重传数据。

复制代码
发送方:
  发送seq=100, len=100
  启动定时器,RTO=1秒
  
  1秒后,未收到ACK
  重传seq=100, len=100
  启动定时器,RTO=2秒 (指数退避)
  
  2秒后,未收到ACK
  重传seq=100, len=100
  启动定时器,RTO=4秒
  
  ...

关键问题: RTO(Retransmission Timeout)设置多少?

  • 太短:网络正常但ACK延迟,导致不必要的重传
  • 太长:网络故障时,恢复太慢

4.2 RTO计算

RTO基于RTT(Round-Trip Time,往返时间)动态计算。

RTT测量:

复制代码
发送数据:时刻T1
收到ACK:时刻T2
RTT = T2 - T1

RTO计算(RFC 6298):

复制代码
SRTT(Smoothed RTT,平滑RTT):
  SRTT = (1-α) × SRTT + α × RTT
  α = 1/8

RTTvar(RTT方差):
  RTTvar = (1-β) × RTTvar + β × |SRTT - RTT|
  β = 1/4

RTO:
  RTO = SRTT + 4 × RTTvar
  最小值:200ms
  最大值:60秒

为什么要加4倍方差? 考虑网络抖动。如果RTT波动大,需要更长的超时时间。

面试要点: RTO不能固定,必须根据网络状况动态调整。

4.3 快速重传

如果发送方收到3个重复的ACK,不等超时,立即重传。

复制代码
发送方发送:
  seq=100, len=100
  seq=200, len=100 → 丢失
  seq=300, len=100
  seq=400, len=100
  seq=500, len=100

接收方:
  收到seq=100 → ACK, ack=200
  收到seq=300 → ACK, ack=200 (重复ACK #1)
  收到seq=400 → ACK, ack=200 (重复ACK #2)
  收到seq=500 → ACK, ack=200 (重复ACK #3)

发送方:
  收到3个重复ACK (ack=200)
  立即重传seq=200, len=100
  不等超时

为什么是3个重复ACK? 因为网络可能乱序,1-2个重复ACK可能是乱序导致的。3个重复ACK基本可以确定是丢包。

面试易错点: "3个重复ACK"还是"4个ACK"?

答案:3个重复ACK,加上第1个正常ACK,总共4个。但我们说"收到3个重复ACK就重传"。

4.4 重传次数限制

TCP不会无限重传,有次数限制。

Linux参数:

bash 复制代码
# 查看
cat /proc/sys/net/ipv4/tcp_retries2

# 默认15次

重传时间(指数退避):

复制代码
第1次:1秒后
第2次:2秒后
第3次:4秒后
第4次:8秒后
第5次:16秒后
第6次:32秒后
第7次:64秒后
...
第15次:约16分钟后

总时间:约30分钟

30分钟后: TCP连接终止,应用层收到错误。


五、滑动窗口机制

这是TCP中最难理解的部分。我看王道视频时似懂非懂,写项目时才真正理解。

5.1 为什么需要滑动窗口

停等协议(发送一个,等ACK,再发下一个)效率太低。

复制代码
停等协议:
  时刻T0:发送数据1
  时刻T1:收到ACK1
  时刻T1:发送数据2
  时刻T2:收到ACK2
  ...
  
  利用率 = 数据传输时间 / (数据传输时间 + RTT)
  
  如果RTT=100ms,传输时间=1ms
  利用率 = 1 / (1 + 100) ≈ 1%

滑动窗口: 允许发送方连续发送多个数据,不用等ACK。

复制代码
滑动窗口:
  时刻T0:连续发送数据1, 2, 3, 4, 5
  时刻T1:收到ACK1, ACK2
  时刻T1:继续发送数据6, 7
  ...
  
  利用率大幅提升

5.2 发送窗口

发送窗口把发送缓冲区分成4个部分:

复制代码
发送缓冲区:
┌─────────┬─────────┬─────────┬─────────┐
│已发送已确认│已发送未确认│  可发送  │  不可发送 │
└─────────┴─────────┴─────────┴─────────┘
          ↑                   ↑
          |<--发送窗口------->|

已发送已确认: 已经收到ACK的数据,可以从缓冲区删除。

已发送未确认: 已经发送,但还没收到ACK。如果丢包,需要重传。

可发送: 还没发送,但可以发送(接收方的rwnd允许)。

不可发送: 超出接收方的rwnd,不能发送。

窗口滑动:

复制代码
初始状态:
  已发送已确认:0-99
  已发送未确认:100-199 (窗口大小=100)
  可发送:200-299
  
收到ACK=150:
  已发送已确认:0-149 (窗口右移)
  已发送未确认:150-199
  可发送:200-349 (窗口扩大了50)

5.3 接收窗口

接收窗口表示接收方还能接收多少数据。

复制代码
接收缓冲区:
┌─────────┬─────────┬─────────┐
│  已接收  │  可接收  │ 不可接收 │
│(应用层未读)│        │         │
└─────────┴─────────┴─────────┘
          |<--接收窗口-->|

rwnd(接收窗口大小): 接收方在ACK中告诉发送方。

复制代码
接收方:
  接收缓冲区大小:8KB
  应用层已读取:2KB
  剩余空间:6KB
  
  发送ACK:ack=xxx, rwnd=6144

发送方:

复制代码
收到rwnd=6144
发送窗口大小 = min(自己的拥塞窗口, 对方的rwnd)
              = min(cwnd, 6144)

5.4 零窗口探测

如果接收方的rwnd=0(缓冲区满了),发送方停止发送。但接收方的rwnd什么时候变大?发送方怎么知道?

零窗口探测(Zero Window Probe):

复制代码
时刻T0:
  接收方:rwnd=0 (缓冲区满)
  发送方:收到rwnd=0,停止发送

时刻T1:
  应用层读取数据,缓冲区有空间了
  接收方:rwnd=1024
  但如何通知发送方?
  
TCP的解决方法:
  发送方定期发送"零窗口探测"报文
  - 包含1字节数据
  - 探测接收方的rwnd
  
  接收方回复ACK:ack=xxx, rwnd=1024
  发送方:收到rwnd=1024,继续发送

探测间隔: 指数退避,从5秒到60秒。

5.5 滑动窗口与send()的关系

这是写Reactor项目时必须理解的部分。

send()函数的行为:

cpp 复制代码
int n = send(sockfd, buf, len, 0);

返回值:
  n == len:数据全部拷贝到发送缓冲区
  0 < n < len:发送缓冲区满了,只拷贝了n字节
  n == -1, errno == EAGAIN:发送缓冲区满了,一个字节都没拷贝(非阻塞socket)

为什么会返回n < len?

复制代码
场景:
  发送缓冲区大小:64KB
  已使用:60KB
  剩余:4KB
  
  send(sockfd, buf, 10240, 0)  // 想发送10KB
  
  结果:
    只能拷贝4KB到发送缓冲区
    返回:n = 4096
    
  剩余6KB需要再次send

与滑动窗口的关系:

复制代码
send() → 拷贝到发送缓冲区 → TCP协议栈根据滑动窗口发送

TCP协议栈:
  实际发送窗口 = min(cwnd, rwnd)
  
  如果rwnd=0:
    - send()可能成功(拷贝到发送缓冲区)
    - 但TCP不会发送数据
    - 等待零窗口探测

5.6 Reactor项目实战

处理send()返回值:

cpp 复制代码
// 错误的写法
void send_data(int sockfd, const char* data, int len) {
    send(sockfd, data, len, 0);  // 假设全部发送成功(错误)
}

// 正确的写法
void send_data(int sockfd, const char* data, int len) {
    int total = 0;
    while (total < len) {
        int n = send(sockfd, data + total, len - total, 0);
        
        if (n > 0) {
            total += n;
        } else if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 发送缓冲区满了,等待可写事件
                // 将剩余数据加入发送队列
                add_to_send_buffer(sockfd, data + total, len - total);
                break;
            } else {
                // 真实错误
                perror("send");
                break;
            }
        }
    }
}

// 更好的方法:使用epoll的EPOLLOUT事件
void handle_write_event(int sockfd) {
    // 从发送队列取数据
    const char* data;
    int len;
    get_from_send_buffer(sockfd, &data, &len);
    
    int n = send(sockfd, data, len, 0);
    
    if (n > 0) {
        // 发送了n字节,更新队列
        remove_from_send_buffer(sockfd, n);
        
        if (send_buffer_empty(sockfd)) {
            // 队列空了,取消监听EPOLLOUT
            epoll_ctl(epollfd, EPOLL_CTL_MOD, sockfd, EPOLLIN | EPOLLET);
        }
    } else if (n == -1 && errno == EAGAIN) {
        // 还是满,继续等待EPOLLOUT
    }
}

六、拥塞控制机制

拥塞控制是TCP最复杂的部分。我看王道视频时只记住了慢启动、拥塞避免,但不理解为什么这样设计。后来看了Linux内核源码,才明白其中的智慧。

6.1 拥塞控制 vs 流量控制

很多人混淆这两个概念。

流量控制(Flow Control):

  • 目的:保护接收方
  • 机制:接收方通过rwnd告诉发送方自己能接收多少
  • 控制对象:单个连接的两端

拥塞控制(Congestion Control):

  • 目的:保护网络
  • 机制:发送方根据网络状况调整发送速率
  • 控制对象:整个网络

实际发送窗口:

复制代码
send_window = min(cwnd, rwnd)

cwnd:拥塞窗口(发送方自己维护)
rwnd:接收窗口(接收方告知)

6.2 慢启动

"慢启动"这个名字很容易误导人,其实一点都不慢,是指数增长。

原理:

复制代码
cwnd初始值:1个MSS(Maximum Segment Size,最大报文段长度)

每收到1个ACK:cwnd += 1

结果:每个RTT,cwnd翻倍

时刻T0:cwnd=1,发送1个包
时刻T1:收到1个ACK,cwnd=2,发送2个包
时刻T2:收到2个ACK,cwnd=4,发送4个包
时刻T3:收到4个ACK,cwnd=8,发送8个包
...

1 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 256 → 512 → 1024

类比: 开车上高速,从20km/h开始,每秒翻倍:20 → 40 → 80 → 160。

为什么叫"慢启动"? 相对于一开始就发送大量数据,这算是"慢"的。

何时停止? 达到ssthresh(慢启动阈值)。

复制代码
初始:cwnd=1, ssthresh=64(假设)

cwnd < ssthresh:慢启动(指数增长)
  1 → 2 → 4 → 8 → 16 → 32 → 64

cwnd >= ssthresh:拥塞避免(线性增长)
  64 → 65 → 66 → 67 → 68 → ...

6.3 拥塞避免

进入拥塞避免阶段后,cwnd线性增长。

原理:

复制代码
每收到1个ACK:cwnd += 1/cwnd

结果:每个RTT,cwnd += 1

时刻T0:cwnd=64
时刻T1:cwnd=65 (收到64个ACK,cwnd += 64/64 = 1)
时刻T2:cwnd=66
时刻T3:cwnd=67
...

类比: 接近限速了,缓慢加速:160 → 161 → 162 → 163。

6.4 快速重传和快速恢复

发生丢包时(收到3个重复ACK),TCP认为网络拥塞了。

传统方法(TCP Tahoe):

复制代码
收到3个重复ACK:
  ssthresh = cwnd / 2
  cwnd = 1
  重新慢启动

改进方法(TCP Reno):

复制代码
收到3个重复ACK:
  ssthresh = cwnd / 2
  cwnd = ssthresh + 3
  快速恢复(不回到慢启动)
  
  每收到1个重复ACK:cwnd += 1(膨胀窗口)
  
  收到新ACK:
    cwnd = ssthresh
    进入拥塞避免

示例:

复制代码
时刻T0:cwnd=16, ssthresh=32
时刻T1:收到3个重复ACK(丢包)
        ssthresh = 16 / 2 = 8
        cwnd = 8 + 3 = 11
        快速重传丢失的包
        
时刻T2:收到第4个重复ACK
        cwnd = 12
        
时刻T3:收到第5个重复ACK
        cwnd = 13
        
时刻T4:收到新ACK(重传的包确认了)
        cwnd = 8
        进入拥塞避免

为什么要膨胀窗口? 每个重复ACK说明有一个包离开了网络,可以发送新包。

超时重传: 比3个重复ACK更严重,说明网络严重拥塞。

复制代码
超时:
  ssthresh = cwnd / 2
  cwnd = 1
  重新慢启动

6.5 拥塞控制算法演进

TCP Tahoe(1988):

  • 慢启动、拥塞避免
  • 丢包时回到慢启动

TCP Reno(1990):

  • 加入快速重传、快速恢复
  • 丢包时不一定回到慢启动

TCP New Reno(1999):

  • 改进快速恢复,处理多个丢包

TCP Cubic(2005,Linux默认):

  • 改进cwnd增长函数,使用三次函数
  • 更快达到之前的cwnd
  • 更适合高带宽网络

TCP BBR(2016,Google):

  • 不基于丢包,而是基于带宽和RTT
  • 更适合现代网络

查看当前算法:

bash 复制代码
# Linux
cat /proc/sys/net/ipv4/tcp_congestion_control

# 设置为cubic
echo cubic > /proc/sys/net/ipv4/tcp_congestion_control

6.6 Linux内核源码分析

这部分是为了加深理解,不是让你记住代码,而是理解原理。

简化的内核数据结构:

c 复制代码
struct tcp_sock {
    u32 snd_cwnd;      // 拥塞窗口
    u32 snd_ssthresh;  // 慢启动阈值
    u32 snd_wnd;       // 接收方的rwnd
    // ...
};

// 实际发送窗口
static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
{
    return tp->snd_una + min(tp->snd_wnd, tp->snd_cwnd);
}

慢启动实现(简化):

c 复制代码
// net/ipv4/tcp_cong.c

// 慢启动:每个ACK,cwnd += 1
void tcp_slow_start(struct tcp_sock *tp, u32 acked)
{
    tp->snd_cwnd += acked;
    
    // 限制cwnd不超过ssthresh
    tp->snd_cwnd = min(tp->snd_cwnd, tp->snd_ssthresh);
}

拥塞避免实现(简化):

c 复制代码
// 拥塞避免:每个RTT,cwnd += 1
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w)
{
    if (tp->snd_cwnd_cnt >= w) {
        tp->snd_cwnd++;
        tp->snd_cwnd_cnt = 0;
    } else {
        tp->snd_cwnd_cnt++;
    }
}

快速重传处理(简化):

c 复制代码
// 收到3个重复ACK
void tcp_fastretrans_alert(struct sock *sk, int pkts_acked)
{
    struct tcp_sock *tp = tcp_sk(sk);
    
    if (tp->dup_acks >= 3) {
        // 进入快速恢复
        tp->snd_ssthresh = max(tp->snd_cwnd / 2, 2);
        tp->snd_cwnd = tp->snd_ssthresh + 3;
        
        // 快速重传
        tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
    }
}

发送数据的核心函数(简化):

c 复制代码
// net/ipv4/tcp_output.c

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now,
                            int nonagle, int push_one, gfp_t gfp)
{
    struct tcp_sock *tp = tcp_sk(sk);
    
    while (1) {
        // 计算可发送窗口
        u32 limit = tcp_wnd_end(tp) - tp->snd_nxt;
        
        if (limit <= 0)
            break;  // 窗口满了,不能发送
        
        // 发送数据
        if (tcp_transmit_skb(sk, skb, 1, gfp))
            break;
    }
    
    return true;
}

这就是TCP发送数据的核心逻辑:检查窗口(min(cwnd, rwnd)),有空间就发送。


七、面试高频题总结

必背知识点(10个)

  1. TCP可靠性的三大机制

    • 序列号和确认号
    • 超时重传
    • 滑动窗口
  2. ack=500表示什么?

    • 收到了499及之前的所有字节
    • 期望接收500号字节
  3. 快速重传的条件

    • 收到3个重复ACK
    • 不等超时,立即重传
  4. 滑动窗口的作用

    • 流量控制
    • 提高传输效率
  5. rwnd和cwnd的区别

    • rwnd:接收窗口,保护接收方
    • cwnd:拥塞窗口,保护网络
    • 实际窗口 = min(rwnd, cwnd)
  6. 慢启动的过程

    • 指数增长:1 → 2 → 4 → 8 → 16
    • 达到ssthresh后进入拥塞避免
  7. 拥塞避免的过程

    • 线性增长:每个RTT增加1个MSS
    • cwnd += 1 / cwnd
  8. 快速恢复的过程

    • ssthresh = cwnd / 2
    • cwnd = ssthresh + 3
    • 不回到慢启动
  9. 零窗口探测

    • rwnd=0时,发送方停止发送
    • 定期发送1字节探测
  10. send()返回值的处理

    • 可能小于len,需要循环发送
    • 非阻塞时可能返回EAGAIN

面试标准答案模板

问:TCP如何保证可靠性?

TCP通过三大机制保证可靠性:

  1. 序列号和确认号: 每个字节都有序列号,接收方通过ACK确认收到的数据。ack表示期望接收的下一个字节序号。

  2. 超时重传: 发送方发送数据后启动定时器,如果RTO时间内未收到ACK,就重传数据。RTO基于RTT动态计算。

  3. 滑动窗口: 接收方通过rwnd告诉发送方自己能接收多少数据,发送方根据rwnd和cwnd控制发送速率。

此外,TCP还有快速重传机制:收到3个重复ACK时,不等超时,立即重传丢失的数据。

在我的Reactor项目中,我需要处理send()的返回值,因为发送缓冲区可能满了,send()返回值小于len,需要循环发送剩余数据。

问:拥塞控制和流量控制的区别?

流量控制是保护接收方,拥塞控制是保护网络。

流量控制:接收方通过rwnd(接收窗口)告诉发送方自己能接收多少数据。如果接收缓冲区满了,rwnd=0,发送方停止发送。

拥塞控制:发送方根据网络状况调整cwnd(拥塞窗口)。包括慢启动(指数增长)、拥塞避免(线性增长)、快速重传和快速恢复。

实际发送窗口 = min(cwnd, rwnd),同时考虑了流量控制和拥塞控制。

Linux默认使用TCP Cubic算法,相比传统的Reno算法,更适合高带宽网络。


八、总结

TCP的可靠性和拥塞控制是计网最难也最重要的部分。学习这部分内容,我的体会是:

理论学习:

  1. 先理解基本概念:ACK、重传、滑动窗口
  2. 画图理解:窗口如何滑动、cwnd如何变化
  3. 理解"为什么":为什么是指数增长?为什么要慢启动?

实践验证:

  1. 写代码处理send()的返回值
  2. 用tcpdump抓包,观察真实的窗口变化
  3. 压力测试,观察cwnd的动态调整

面试准备:

  1. 必须理解rwnd和cwnd的区别
  2. 必须能画出慢启动和拥塞避免的曲线
  3. 结合项目讲解如何处理send()

下一篇文章,我们聊HTTP/1.1的新特性:持久连接、缓存机制、分块传输。


参考资料

  • 王道考研《计算机网络》视频课程
  • RFC 5681:TCP Congestion Control
  • RFC 6298:Computing TCP's Retransmission Timer
  • Linux内核源码:net/ipv4/tcp_output.cnet/ipv4/tcp_input.c
  • 《TCP/IP详解 卷1:协议》第17-21章

相关推荐
图图图图爱睡觉4 小时前
主机跟虚拟机ip一直Ping不通,并且虚拟机使用ifconfig命令时,ens33没有ipv4地址,只有ipv6地址
服务器·网络·tcp/ip
lhxcc_fly4 小时前
Linux网络--8、NAT,代理,网络穿透
linux·服务器·网络·nat
开开心心就好5 小时前
电子报纸离线保存:一键下载多报PDF工具
网络·笔记·macos·pdf·word·音视频·phpstorm
wow_DG5 小时前
【运维✨】云服务器公网 IP 迷雾:为什么本机看不到那个地址?
运维·服务器·tcp/ip
是Yu欸6 小时前
【博资考5】网安2025
网络·人工智能·经验分享·笔记·网络安全·ai·博资考
交换机路由器测试之路6 小时前
交换机路由器基础(二)-运营商网络架构和接入网
网络·架构
在坚持一下我可没意见6 小时前
HTTP 协议基本格式与 Fiddler 抓包工具实战指南
java·开发语言·网络协议·tcp/ip·http·java-ee·fiddler
大隐隐于野7 小时前
tcp large-receive-offload
网络
liubaoyi2178 小时前
网络原理--HTTP
网络·http