第4章 TCP与可靠传输
学习目标
- 理解TCP三次握手和四次挥手过程
- 掌握滑动窗口和流量控制机制
- 了解拥塞控制算法(Cubic、BBR)
- 能够排查TCP连接和性能问题
🔬 原理
TCP三次握手
连接建立过程:
客户端 服务器
SYN seq=x →
← SYN-ACK seq=y ack=x+1
ACK ack=y+1 →
详细过程:
- 客户端:发送SYN包,序列号=x,状态=SYN_SENT
- 服务器:发送SYN-ACK包,序列号=y,确认号=x+1,状态=SYN_RCVD
- 客户端:发送ACK包,确认号=y+1,状态=ESTABLISHED
- 服务器:收到ACK,状态=ESTABLISHED
状态转换:
CLOSED → SYN_SENT → ESTABLISHED
CLOSED → LISTEN → SYN_RCVD → ESTABLISHED
TCP四次挥手
连接关闭过程:
客户端 服务器
FIN seq=x →
← ACK ack=x+1
← FIN seq=y
ACK ack=y+1 →
详细过程:
- 主动关闭方:发送FIN包,状态=FIN_WAIT_1
- 被动关闭方:发送ACK包,状态=CLOSE_WAIT
- 被动关闭方:发送FIN包,状态=LAST_ACK
- 主动关闭方:发送ACK包,状态=TIME_WAIT
状态转换:
ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
滑动窗口机制
发送窗口:
- 已发送未确认:等待ACK的数据
- 可发送未发送:窗口内可发送的数据
- 窗口大小:由接收方通告
接收窗口:
- 已接收:已确认的数据
- 可接收:窗口内可接收的数据
- 窗口大小:接收缓冲区剩余空间
流量控制:
- 接收方通过窗口大小控制发送方
- 窗口为0时暂停发送
- 窗口更新时恢复发送
拥塞控制算法
Cubic算法:
- 慢启动:指数增长(1→2→4→8...)
- 拥塞避免:线性增长
- 快速重传:3个重复ACK触发
- 快速恢复:减半窗口后继续
BBR算法:
- 基于带宽和RTT的主动探测
- 不依赖丢包信号
- 适合高带宽延迟网络
关键参数:
# 查看拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 查看TCP参数
sysctl -a | grep tcp
️ 实现
Linux TCP实现
TCP连接结构:
struct tcp_sock {
struct inet_connection_sock inet_conn;
/* TCP状态 */
u32 rcv_nxt; // 下一个期望接收的序列号
u32 snd_nxt; // 下一个发送的序列号
u32 snd_una; // 最早未确认的序列号
/* 窗口管理 */
u32 rcv_wnd; // 接收窗口
u32 snd_wnd; // 发送窗口
u32 snd_cwnd; // 拥塞窗口
/* 重传机制 */
u32 rto; // 重传超时时间
u32 rtt; // 往返时间
u32 rttvar; // RTT方差
/* 队列管理 */
struct sk_buff_head write_queue; // 发送队列
struct sk_buff_head out_of_order_queue; // 乱序队列
};
三次握手实现:
// 客户端发送SYN
int tcp_connect(struct sock *sk) {
struct tcp_sock *tp = tcp_sk(sk);
// 初始化序列号
tp->snd_nxt = tp->snd_una = tp->write_seq;
// 发送SYN包
tcp_send_syn(sk);
// 设置重传定时器
tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);
return 0;
}
// 服务器处理SYN
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb) {
struct tcp_sock *tp = tcp_sk(sk);
// 分配新的socket
struct sock *newsk = tcp_create_openreq_child(sk, req);
// 发送SYN-ACK
tcp_send_synack(newsk);
return 0;
}
拥塞控制实现
Cubic算法:
static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked) {
struct tcp_sock *tp = tcp_sk(sk);
struct bictcp *ca = inet_csk_ca(sk);
if (!tcp_is_cwnd_limited(sk))
return;
if (tp->snd_cwnd <= tp->snd_ssthresh) {
// 慢启动阶段
tp->snd_cwnd = min(tp->snd_cwnd + acked, tp->snd_cwnd_clamp);
} else {
// 拥塞避免阶段
u32 bic_target = cubic_target_cwnd(tp);
u32 cubic_target = cubic_target_cwnd(tp);
tp->snd_cwnd = min(tp->snd_cwnd + 1, cubic_target);
}
}
🛠️ 命令
TCP连接管理
# 查看TCP连接状态
ss -tanp
# 查看TCP统计信息
ss -s
# 查看TCP详细信息
ss -ti
# 查看监听端口
ss -tuln
TCP参数调优
# 查看TCP参数
sysctl -a | grep tcp
# 重要参数
net.core.somaxconn = 128 # 全连接队列大小
net.ipv4.tcp_max_syn_backlog = 512 # 半连接队列大小
net.ipv4.tcp_tw_reuse = 1 # TIME_WAIT复用
net.ipv4.tcp_congestion_control = cubic # 拥塞控制算法
网络性能测试
# 带宽测试
iperf3 -s # 服务端
iperf3 -c server_ip # 客户端
# 延迟测试
ping -c 10 target_ip
# 连接测试
nc -zv target_ip port
代码
简单TCP服务器
// tcp_server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>
int server_socket;
void signal_handler(int sig) {
printf("\nShutting down server...\n");
close(server_socket);
exit(0);
}
int main() {
// 注册信号处理
signal(SIGINT, signal_handler);
// 创建socket
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("socket");
return 1;
}
// 设置SO_REUSEADDR
int reuse = 1;
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse)) < 0) {
perror("setsockopt");
close(server_socket);
return 1;
}
// 绑定地址
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(server_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
close(server_socket);
return 1;
}
// 监听
if (listen(server_socket, 128) < 0) {
perror("listen");
close(server_socket);
return 1;
}
printf("Server listening on port 8080\n");
// 接受连接
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_socket = accept(server_socket,
(struct sockaddr*)&client_addr,
&client_len);
if (client_socket < 0) {
perror("accept");
continue;
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 处理客户端请求
char buffer[4096];
int bytes_received;
while ((bytes_received = read(client_socket, buffer, sizeof(buffer))) > 0) {
// 回显数据
write(client_socket, buffer, bytes_received);
}
close(client_socket);
printf("Client disconnected\n");
}
close(server_socket);
return 0;
}
TCP客户端
// tcp_client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <server_ip> <port>\n", argv[0]);
return 1;
}
// 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
// 连接服务器
struct sockaddr_in server_addr = {
.sin_family = AF_INET,
.sin_port = htons(atoi(argv[2])),
.sin_addr.s_addr = inet_addr(argv[1])
};
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock);
return 1;
}
printf("Connected to %s:%s\n", argv[1], argv[2]);
// 发送数据
char message[] = "Hello, TCP Server!";
if (send(sock, message, strlen(message), 0) < 0) {
perror("send");
close(sock);
return 1;
}
// 接收响应
char buffer[1024];
int bytes_received = recv(sock, buffer, sizeof(buffer), 0);
if (bytes_received < 0) {
perror("recv");
close(sock);
return 1;
}
buffer[bytes_received] = '\0';
printf("Server response: %s\n", buffer);
close(sock);
return 0;
}
编译运行:
gcc tcp_server.c -o tcp_server
gcc tcp_client.c -o tcp_client
# 运行服务器
./tcp_server
# 运行客户端(另一个终端)
./tcp_client 127.0.0.1 8080
🧪 实验
实验1:观察TCP三次握手
目标:通过抓包观察TCP连接建立过程
步骤:
# 1. 启动抓包(一个终端)
sudo tcpdump -i lo -nn 'port 8080' -S
# 2. 启动服务器(另一个终端)
./tcp_server
# 3. 连接客户端(第三个终端)
./tcp_client 127.0.0.1 8080
# 4. 观察抓包输出
# 应该看到:
# SYN seq=x
# SYN-ACK seq=y ack=x+1
# ACK ack=y+1
预期结果:
- 理解三次握手过程
- 观察序列号和确认号
- 掌握TCP状态转换
实验2:TCP窗口机制观察
目标:理解滑动窗口和流量控制
步骤:
# 1. 查看TCP窗口参数
cat /proc/sys/net/ipv4/tcp_rmem
cat /proc/sys/net/ipv4/tcp_wmem
# 2. 使用iperf3测试
iperf3 -s # 服务端
iperf3 -c localhost -t 30 # 客户端
# 3. 观察窗口变化
ss -ti | grep ESTABLISHED
# 4. 调整窗口大小
sudo sysctl -w net.core.rmem_max=134217728
sudo sysctl -w net.core.wmem_max=134217728
预期结果:
- 理解窗口大小对性能的影响
- 观察流量控制机制
- 掌握TCP参数调优
实验3:拥塞控制算法对比
目标:对比不同拥塞控制算法的性能
步骤:
# 1. 测试Cubic算法
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic
iperf3 -s &
iperf3 -c localhost -t 30
# 2. 测试BBR算法
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
iperf3 -s &
iperf3 -c localhost -t 30
# 3. 模拟网络延迟
sudo tc qdisc add dev lo root netem delay 50ms
iperf3 -c localhost -t 30
# 4. 清理
sudo tc qdisc del dev lo root
预期结果:
- 理解不同算法的特点
- 观察延迟对性能的影响
- 掌握拥塞控制调优
实验4:TCP连接状态分析
目标:分析TCP连接的各种状态
步骤:
# 1. 查看当前连接状态
ss -tanp
# 2. 创建大量连接
for i in {1..100}; do
./tcp_client 127.0.0.1 8080 &
done
# 3. 观察连接状态
ss -tanp | grep -E "(SYN_SENT|ESTABLISHED|TIME_WAIT)"
# 4. 查看TCP统计
cat /proc/net/snmp | grep -i tcp
预期结果:
- 理解各种TCP状态
- 观察连接建立和关闭过程
- 掌握连接管理技巧
排错
常见问题排查
问题1:连接被拒绝
# 检查服务是否监听
ss -tuln | grep 8080
# 检查防火墙
sudo iptables -L -n | grep 8080
# 检查服务状态
systemctl status service_name
问题2:大量TIME_WAIT连接
# 查看TIME_WAIT数量
ss -tan | grep TIME_WAIT | wc -l
# 启用TIME_WAIT复用
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 调整TIME_WAIT超时
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
问题3:频繁重传
# 查看重传统计
ss -ti | grep -i retrans
# 检查网络质量
ping -c 10 target_ip
# 调整重传参数
sudo sysctl -w net.ipv4.tcp_retries2=8
排错清单
- [ ] 检查服务监听状态(ss -tuln)
- [ ] 验证防火墙规则(iptables、ufw)
- [ ] 确认网络连通性(ping、telnet)
- [ ] 查看TCP统计信息(/proc/net/snmp)
- [ ] 检查连接队列(somaxconn、backlog)
- [ ] 分析重传情况(ss -ti)
- [ ] 查看系统日志(dmesg、/var/log/syslog)