目录
一、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:IPv4SOCK_STREAM:TCP0:自动选择协议返回:文件描述符 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);
内核做两件事:
- 将 socket 状态从
CLOSED→LISTEN - 创建两个队列:
- 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);
内核行为(超级重要):
- 数据从用户态拷贝到 内核 socket 发送缓冲区
- 内核协议栈负责:
- 分段
- 滑动窗口
- 拥塞控制
- 重传
- 确认
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);
触发 四次挥手:
- 发送 FIN
- 等待对方 ACK
- 对方发送 FIN
- 回复 ACK连接关闭
五、TCP三次握手四次挥手
1.三次握手
- 客户端 → SYN → 服务端(SYN_SENT)
- 服务端 → SYN+ACK → 客户端(SYN_RCVD)
- 客户端 → ACK → 服务端(ESTABLISHED)
完成后,连接进入 ACCEPT 队列,等待 accept 取走。
2.四次挥手
- 主动关闭方 → FIN(FIN_WAIT1)
- 被动关闭方 → ACK(FIN_WAIT2)
- 被动关闭方 → FIN(LAST_ACK)
- 主动关闭方 → ACK(TIME_WAIT)
TIME_WAIT:等待 2MSL,确保对方收到最后一个 ACK。
六、TCP注意事项
TCP 是字节流 → 必踩坑:粘包 / 半包
所以会出现:
- 发送 2 次,接收 1 次(粘包)
- 发送 1 次,接收 2 次(半包)
解决方案(工程标准)
- 固定长度头 + 数据体(最常用): [4字节数据长度][数据体...]
- 分隔符(如 \n)
- 固定长度
1. 阻塞 vs 非阻塞
- 阻塞:
accept/read/write没数据就等待 - 非阻塞:立刻返回
-1,errno=EAGAIN
设置非阻塞:
fcntl(fd, F_SETFL, O_NONBLOCK);
2. 高并发必须用:IO 多路复用
selectpoll- 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 / 2cwnd = 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:总内存达到此值,内核开始压力模式,尝试回收缓冲high:TCP 总内存上限超过后内核会 拒绝分配新的 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. 三个阶段
-
< low
- 正常状态
- 内核不做任何内存回收、不限制 TCP 缓冲
-
≥ low 且 < pressure
- 仍算正常
- 只是开始计数,还不回收
-
≥ pressure
- 进入 TCP 内存压力模式(memory pressure)
- 内核开始主动回收:
- 回收空闲的 send/recv buffer
- 降低部分连接的缓冲区大小
- 减缓新 buffer 分配
-
≥ high
- 达到 TCP 内存上限
- 内核拒绝分配新的 TCP 内存
- 表现:
connect()失败send()报错- 新建连接被拒
- dmesg 里出现
Out of socket memory
2、什么行为会触发回收?
满足任一就会触发 pressure → 回收:
-
并发连接非常多
- 每个连接占一点
tcp_rmem/tcp_wmem - 总和超过
tcp_mem pressure
- 每个连接占一点
-
单个连接缓冲区很大
tcp_rmem/tcp_wmem的 max 设得很大- 少量长连接、大吞吐就把总量撑爆
-
大量 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_rmem、tcp_wmem:→ 控制单个连接的缓冲区上下限tcp_mem:→ 控制全局总和,只有它能触发内核回收
简单说:
- 单个连接再大,也不会触发回收
- 所有连接加起来超了 pressure,才触发回收
5、高并发服务器的正确配置思路
- 把
tcp_rmem/tcp_wmem的 max 调小- 单个连接不要占用太多内存
- 把
tcp_mem整体调大- 给内核足够空间支撑大量连接
- 避免让
alloc长期处于pressure以上- 否则会出现:
- 网络延迟变大
- 丢包、重传增多
- 新建连接不稳定
- 否则会出现:

1263

被折叠的 条评论
为什么被折叠?



