Linux开发学习——TCP网络

目录

一、TCP/IP四层网络模型

1.数据单元+数据流向

二、TCP本质(内核视角)

三、TCP通信标准流程

服务端(被动连接)

客户端(主动连接)

四、核心API详解

1. socket() — 创建套接字

2. bind() — 绑定地址端口

3. listen() — 开启监听

4. accept() — 接受连接

5. connect() — 客户端发起连接

6. send() / write() — 发送数据

7. recv() / read() — 接收数据

8. close() — 关闭连接

五、TCP三次握手四次挥手

1.三次握手

2.四次挥手

六、TCP注意事项

解决方案(工程标准)

1. 阻塞 vs 非阻塞

2. 高并发必须用:IO 多路复用

3. TIME_WAIT 过多怎么办?

4. 如何保证不丢包?

七、TCP内核协议栈

1.内核核心结构:struct tcp_sock

2.滑动窗口(流量控制)

2.1作用

2.2发送方窗口(发送缓冲区)

2.3接收方窗口(接收缓冲区)

2.4内核发送数据的真实判断

3.拥塞控制(内核核心算法)

3.1.作用

3.2.四个阶段(Linux CUBIC标准)

3.2.1慢启动Slow Start

3.2.2拥塞避免Congestion Avoidance

3.2.3快速重传 Fast Retransmit

3.2.4快速回复 Fast Recovery

3.3.Linux默认:CUBIC算法

4.重传机制(内核最复杂部分)

4.1重传队列 rtxqueue

4.2两种重传出发条件

A. 超时重传(RTO)

B. 快速重传(3 个重复 ACK)

4.3RTO计算(内核动态)

4.4乱序处理

5.伪代码实现

6.linux下的配置

1. net.ipv4.tcp_rmem

2. net.ipv4.tcp_wmem

3. net.ipv4.tcp_mem

用最简单的话总结


一、TCP/IP四层网络模型

  • 应用层

    • 协议:HTTP/HTTPS、FTP、DNS、SSH、SMTP、DHCP
    • 作用:为应用程序提供网络服务接口
  • 传输层

    • 协议:TCP、UDP
    • 作用:端到端的数据传输、端口寻址、可靠 / 不可靠交付
  • 网络层

    • 协议:IP(IPv4/IPv6)、ICMP、ARP、OSPF、BGP
    • 作用:主机寻址、路由选择、数据包转发
  • 网络接口层(链路层)

    • 协议:以太网、Wi-Fi、PPP、MAC 帧
    • 作用:物理介质传输、硬件寻址、帧封装

1.数据单元+数据流向

总结:

→ 加 TCP/UDP 头 → 段 / 数据报

→ 加 IP 头 → 数据包

→ 加帧头帧尾 →

→ 转为比特流发到物理线路

二、TCP本质(内核视角)

TCP 是面向连接、可靠、流式、全双工的传输协议。

Linux 内核中:

  • 一个 TCP 连接 = 一个 socket(本质是文件描述符 fd)
  • TCP 连接由 四元组 唯一标识:源 IP + 源端口 + 目标 IP + 目标端口
  • 内核维护 TCP 状态机、滑动窗口、拥塞控制、重传队列
  • 用户程序只需要调用系统 API,收发数据由内核协议栈完成

三、TCP通信标准流程

服务端(被动连接)

socket() → bind() → listen() → accept() → read()/write() → close()

客户端(主动连接)

socket() → connect() → read()/write() → close()

四、核心API详解

1. socket() — 创建套接字

int socket(int domain, int type, int protocol);
  • AF_INET:IPv4
  • SOCK_STREAM:TCP
  • 0:自动选择协议返回:文件描述符 fd(-1 失败)

内核作用:创建 struct socket + struct sock,分配端口资源,初始化 TCP 控制块。


2. bind() — 绑定地址端口

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

作用:告诉内核 “这个 socket 使用这个 IP 和端口”。

结构:

struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // 端口(必须网络字节序 htons)
    struct in_addr sin_addr;     // IP
    char           sin_zero[8];
};

3. listen() — 开启监听

int listen(int sockfd, int backlog);

内核做两件事:

  1. 将 socket 状态从 CLOSEDLISTEN
  2. 创建两个队列:
    • SYN 队列(半连接队列,保存三次握手未完成)
    • ACCEPT 队列(全连接队列,保存已完成握手)

backlog:全连接队列最大长度。


4. accept() — 接受连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 全连接队列 取一个已完成握手的连接
  • 返回新的 fd(客户端连接 socket)
  • 原监听 fd 继续监听

5. connect() — 客户端发起连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

内核行为:

  • 发送 SYN
  • 等待 SYN+ACK
  • 回复 ACK三次握手完成

6. send() / write() — 发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t write(int sockfd, const void *buf, size_t len);

内核行为(超级重要):

  1. 数据从用户态拷贝到 内核 socket 发送缓冲区
  2. 内核协议栈负责:
    • 分段
    • 滑动窗口
    • 拥塞控制
    • 重传
    • 确认
  3. send 返回不代表发送到对方,只代表拷贝到内核缓冲区成功

7. recv() / read() — 接收数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t read(int sockfd, void *buf, size_t len);

内核行为:

  • 内核 socket 接收缓冲区 拷贝数据到用户态
  • 如果缓冲区为空,阻塞 / 返回 EAGAIN

8. close() — 关闭连接

int close(int fd);

触发 四次挥手

  1. 发送 FIN
  2. 等待对方 ACK
  3. 对方发送 FIN
  4. 回复 ACK连接关闭

五、TCP三次握手四次挥手

1.三次握手

  1. 客户端 → SYN → 服务端(SYN_SENT)
  2. 服务端 → SYN+ACK → 客户端(SYN_RCVD)
  3. 客户端 → ACK → 服务端(ESTABLISHED)

完成后,连接进入 ACCEPT 队列,等待 accept 取走。

2.四次挥手

  1. 主动关闭方 → FIN(FIN_WAIT1)
  2. 被动关闭方 → ACK(FIN_WAIT2)
  3. 被动关闭方 → FIN(LAST_ACK)
  4. 主动关闭方 → ACK(TIME_WAIT)

TIME_WAIT:等待 2MSL,确保对方收到最后一个 ACK。

六、TCP注意事项

TCP 是字节流 → 必踩坑:粘包 / 半包

所以会出现:

  • 发送 2 次,接收 1 次(粘包)
  • 发送 1 次,接收 2 次(半包)

解决方案(工程标准)

  1. 固定长度头 + 数据体(最常用): [4字节数据长度][数据体...]
  2. 分隔符(如 \n)
  3. 固定长度

1. 阻塞 vs 非阻塞

  • 阻塞accept/read/write 没数据就等待
  • 非阻塞:立刻返回 -1errno=EAGAIN

设置非阻塞:

fcntl(fd, F_SETFL, O_NONBLOCK);

2. 高并发必须用:IO 多路复用

  • select
  • poll
  • epoll(Linux 最高效)

3. TIME_WAIT 过多怎么办?

setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

4. 如何保证不丢包?

TCP 自带:

  • 序列号
  • 确认应答 ACK
  • 超时重传
  • 滑动窗口用户不需要处理。

七、TCP内核协议栈

1.内核核心结构:struct tcp_sock

struct tcp_sock {
    struct sock sk;

    // === 滑动窗口 ===
    u32 snd_una;    /* 最早未被 ACK 的字节 */
    u32 snd_nxt;    /* 下一个要发送的字节 */
    u32 snd_wnd;    /* 发送窗口(对方通告)*/
    u32 rcv_nxt;    /* 下一个期望收到的字节 */
    u32 rcv_wnd;    /* 接收窗口(本地通告)*/

    // === 拥塞控制 ===
    u32 snd_cwnd;   /* 拥塞窗口(核心)*/
    u32 snd_ssthresh; /* 慢启动阈值 */
    u8  cong_state; /* 拥塞状态 */

    // === 重传队列 ===
    struct sk_buff_head out_of_order; /* 乱序队列 */
    struct tcp_rtx_queue rtxqueue;   /* 重传队列 */
    u32 rto;          /* 重传超时时间 */
};

发送窗口 = min (对方通告窗口 snd_wnd, 拥塞窗口 cwnd)

2.滑动窗口(流量控制)

2.1作用

  • 防止发送方发送过快,导致接收方缓冲区溢出
  • 接收方控制的流量控制

2.2发送方窗口(发送缓冲区)

内核关键变量:

  • snd_una:左边沿(未确认起始)
  • snd_nxt:下一个发送位置
  • snd_wnd:窗口右边沿

只要 snd_nxt < snd_una + snd_wnd,就能发数据

2.3接收方窗口(接收缓冲区)

  • 接收端通过 TCP 头的 window 字段 告诉发送端:“我还能收多少”
  • 接收窗口大小 = 接收空闲缓冲区大小
  • 内核维护 rcv_wnd,随数据读取动态更新

2.4内核发送数据的真实判断

static inline int tcp_send_space(struct tcp_sock *tp)
{
    // 可用发送空间 = 窗口 - 已发送未确认
    return tp->snd_una + min(tp->snd_wnd, tp->snd_cwnd) - tp->snd_nxt;
}

3.拥塞控制(内核核心算法)

3.1.作用

  • 防止发送过快导致路由器 / 网络拥塞
  • 发送方根据网络状况自适应
  • 与滑动窗口是独立机制,共同限制发送速率

3.2.四个阶段(Linux CUBIC标准)

3.2.1慢启动Slow Start
  • 刚建立连接时
  • cwnd 初始很小(通常 10 个 MSS)
  • 每收到一个 ACK,cwnd += 1 MSS
  • 速度指数增长
  • 直到 cwnd >= ssthresh,进入拥塞避免
3.2.2拥塞避免Congestion Avoidance
  • cwnd 线性增长:每经过一个 RTT,cwnd += 1 MSS
  • 平稳试探网络容量
3.2.3快速重传 Fast Retransmit
  • 收到 3 个重复 ACK(DupACK)
  • 说明报文丢失但管道仍通
  • 不等待 RTO,立刻重传丢失报文
3.2.4快速回复 Fast Recovery
  • 出现丢包后:
    ssthresh = cwnd / 2
    cwnd     = ssthresh + 3
    
  • 跳过慢启动,直接进入拥塞避免

3.3.Linux默认:CUBIC算法

  • 高带宽、长延迟网络下更稳定
  • 不再单纯依赖 ACK 驱动
  • 用时间与丢包事件调整 cwnd

内核入口:net/ipv4/tcp_cubic.c

4.重传机制(内核最复杂部分)

4.1重传队列 rtxqueue

  • 所有已发送但未被 ACK 的报文都链在重传队列
  • 每个报文有超时时间 RTO
  • ACK 到达时,从队列删除

4.2两种重传出发条件

A. 超时重传(RTO)
  • 一定时间内没有任何 ACK
  • 认为网络严重拥塞或断开
  • 进入 慢启动
  • ssthresh = cwnd / 2
  • cwnd = 1 MSS
  • 最悲观、最保守
B. 快速重传(3 个重复 ACK)
  • 连续收到 3 个相同 ACK
  • 说明某一包丢了,后面的包都乱序到达
  • 立刻重传,不等待 RTO
  • 进入 快速恢复

4.3RTO计算(内核动态)

RTO 不是固定值,基于 SRTT(平滑往返时间) 计算:

RTO = SRTT + 4 * RTTVAR

内核会随每个 ACK 动态更新,网络越稳定 RTO 越小。

4.4乱序处理

  • 收到不按序的包 → 放入 out_of_order 队列
  • 不立刻丢包,等待后续包填补空洞
  • 填补成功 → 正常 ACK
  • 无法填补 → 触发重传

5.伪代码实现

void tcp_send_packet(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    // 1. 计算可用窗口
    win = min(tp->snd_wnd, tp->snd_cwnd);
    avail = tp->snd_una + win - tp->snd_nxt;

    if (avail <= 0) return; // 流控/拥塞,不能发

    // 2. 从发送队列取数据
    len = min(avail, MSS);
    skb = alloc_skb(len);

    // 3. 构建 TCP 头(带窗口、序号)
    tcp_header(skb, tp->snd_nxt, tp->rcv_wnd);

    // 4. 加入重传队列,设置超时
    tcp_enqueue_rtx(sk, skb);
    tcp_set_rto(sk, skb);

    // 5. 发送到 IP 层
    ip_send(skb);

    tp->snd_nxt += len;
}

void tcp_ack_received(struct sock *sk, u32 ack_seq)
{
    struct tcp_sock *tp = tcp_sk(sk);

    // 1. 移动发送窗口左边沿
    if (after(ack_seq, tp->snd_una))
        tp->snd_una = ack_seq;

    // 2. 从重传队列删除已 ACK 包
    tcp_rtx_queue_prune(sk, ack_seq);

    // 3. 更新 RTT / RTO
    tcp_update_rtt(sk);

    // 4. 拥塞控制:调整 cwnd
    tcp_cong_ack(sk);

    // 5. 滑动窗口:发送新数据
    tcp_send_packet(sk);
}

6.linux下的配置

1. net.ipv4.tcp_rmem

全称:TCP Read Memory(TCP 接收缓冲区)

格式:

tcp_rmem = min default max

单位:页(page),1 page = 4096 字节(通常)

  • min:每个 TCP 连接 最小 保留的接收缓冲区大小
  • default:系统默认给每个连接分配的接收缓冲区大小
  • max单个 TCP 连接 最大 能用的接收缓冲区

作用:控制 单个 socket 接收缓存 的大小。如果你要做高并发,max 不能太大,否则几千条连接就把内存吃光。


2. net.ipv4.tcp_wmem

全称:TCP Write Memory(TCP 发送缓冲区)

格式同上:

tcp_wmem = min default max

单位:页

  • 控制 单个 socket 发送缓存
  • 同样:并发高时,max 要小一点

3. net.ipv4.tcp_mem

全称:TCP overall Memory(整个 TCP 协议栈的内存总量)

格式:

tcp_mem = low pressure high

单位:

这是 整个 Linux 内核 TCP 栈 能用的内存总上限,不是单个连接。

  • low:TCP 总内存低于此值,内核不做任何限制
  • pressure:总内存达到此值,内核开始压力模式,尝试回收缓冲
  • highTCP 总内存上限超过后内核会 拒绝分配新的 TCP 缓存,表现就是:
    • 新建连接失败
    • 丢包
    • 出现 Out of socket memory 类内核日志

用最简单的话总结

  • tcp_rmem / tcp_wmem→ 控制 单个连接 的收发缓冲区→ 调小 = 能支持更多并发连接→ 调大 = 单个连接速度更快、吞吐更高

  • tcp_mem→ 控制 整个 TCP 协议栈 总共能用多少内存→ 高并发服务器必须调大,否则连接一多直接被内核掐掉

八、Linux内核TCP内存回收机制

1、三个阈值与回收逻辑

net.ipv4.tcp_mem = low  pressure  high

单位:页(page),1 page = 4KB

1.1. 三个阶段

  1. < low

    • 正常状态
    • 内核不做任何内存回收、不限制 TCP 缓冲
  2. ≥ low 且 < pressure

    • 仍算正常
    • 只是开始计数,还不回收
  3. ≥ pressure

    • 进入 TCP 内存压力模式(memory pressure)
    • 内核开始主动回收
      • 回收空闲的 send/recv buffer
      • 降低部分连接的缓冲区大小
      • 减缓新 buffer 分配
  4. ≥ high

    • 达到 TCP 内存上限
    • 内核拒绝分配新的 TCP 内存
    • 表现:
      • connect() 失败
      • send() 报错
      • 新建连接被拒
      • dmesg 里出现 Out of socket memory

2、什么行为会触发回收?

满足任一就会触发 pressure → 回收

  1. 并发连接非常多

    • 每个连接占一点 tcp_rmem/tcp_wmem
    • 总和超过 tcp_mem pressure
  2. 单个连接缓冲区很大

    • tcp_rmem/tcp_wmem 的 max 设得很大
    • 少量长连接、大吞吐就把总量撑爆
  3. 大量 TIME_WAIT

    • 大量短连接、大量 TIME_WAIT 套接字
    • 每个也占少量 TCP 内存

3、怎么观察是否触发回收?

# 查看当前 TCP 内存使用(页)
cat /proc/net/sockstat

看这一行:

TCP: inuse 128 orphan 2 tw 314 alloc 1024
  • alloc:当前 TCP 协议栈占用的总页数

对比:

  • alloc < tcp_mem[low] → 无压力
  • alloc ≥ tcp_mem[1]已触发回收
  • alloc ≥ tcp_mem[2]严重受限,接近拒绝连接

4、和 tcp_rmem /tcp_wmem 的关系

  • tcp_rmemtcp_wmem:→ 控制单个连接的缓冲区上下限
  • tcp_mem:→ 控制全局总和只有它能触发内核回收

简单说:

  • 单个连接再大,也不会触发回收
  • 所有连接加起来超了 pressure,才触发回收

5、高并发服务器的正确配置思路

  1. tcp_rmem/tcp_wmem 的 max 调小
    • 单个连接不要占用太多内存
  2. tcp_mem 整体调大
    • 给内核足够空间支撑大量连接
  3. 避免让 alloc 长期处于 pressure 以上
    • 否则会出现:
      • 网络延迟变大
      • 丢包、重传增多
      • 新建连接不稳定
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值