TCP协议深度解析
章节概述
TCP(Transmission Control Protocol)是互联网的核心协议之一,它提供可靠的、面向连接的字节流传输服务。本章将深入探讨TCP的工作原理,包括连接建立、数据传输、拥塞控制等核心机制。
学习目标:
- 理解TCP三次握手和四次挥手的详细过程
- 掌握TCP状态机和连接管理
- 学会使用ss、netstat等工具分析TCP连接
- 能够诊断和解决常见的TCP问题
核心概念
1. TCP报文结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
关键字段:
- Sequence Number (SEQ):序列号,标识发送的字节流位置
- Acknowledgment Number (ACK):确认号,期望接收的下一个字节
- Flags:控制标志(SYN、ACK、FIN、RST等)
- Window:接收窗口大小
2. TCP三次握手
客户端 服务器
│ │
│ SYN (seq=x) │
├──────────────────────────────────>│
│ │ LISTEN → SYN_RECV
│ SYN+ACK (seq=y, ack=x+1) │
│<──────────────────────────────────┤
│ SYN_SENT → ESTABLISHED │
│ ACK (ack=y+1) │
├──────────────────────────────────>│
│ ESTABLISHED SYN_RECV → ESTABLISHED
│ │
过程说明:
- 第一次握手:客户端发送SYN包,进入SYN_SENT状态
- 第二次握手:服务器回复SYN+ACK,进入SYN_RECV状态
- 第三次握手:客户端发送ACK,双方进入ESTABLISHED状态
为什么需要三次握手?
- 防止旧连接请求的影响
- 同步双方的序列号
- 确认双方的收发能力
3. TCP四次挥手
客户端 服务器
│ │
│ FIN (seq=x) │
├──────────────────────────────────>│
│ FIN_WAIT_1 │ CLOSE_WAIT
│ ACK (ack=x+1) │
│<──────────────────────────────────┤
│ FIN_WAIT_2 │
│ FIN (seq=y) │
│<──────────────────────────────────┤
│ TIME_WAIT │ LAST_ACK
│ ACK (ack=y+1) │
├──────────────────────────────────>│
│ │ CLOSED
│ (等待2MSL) │
│ CLOSED │
过程说明:
- 第一次挥手:主动方发送FIN,进入FIN_WAIT_1
- 第二次挥手:被动方回复ACK,进入CLOSE_WAIT
- 第三次挥手:被动方发送FIN,进入LAST_ACK
- 第四次挥手:主动方回复ACK,进入TIME_WAIT
为什么有TIME_WAIT状态?
- 确保最后的ACK被对方收到
- 防止旧连接的数据包影响新连接
4. TCP状态转换图
+---------+
| CLOSED |
+---------+
|
| (主动打开/监听)
↓
+----------+ +---------+ +----------+
|SYN_SENT | | LISTEN | |SYN_RECV |
+----------+ +---------+ +----------+
| | |
| (收到SYN+ACK) | (收到SYN) | (发送SYN+ACK)
↓ ↓ ↓
+--------------------ESTABLISHED--------------------+
|
(主动关闭) | (被动关闭)
↓
+----------+ +---------+ +----------+
|FIN_WAIT_1| |CLOSE_WAIT|
+----------+ +---------+
| |
↓ ↓
+----------+ +---------+
|FIN_WAIT_2| |LAST_ACK |
+----------+ +---------+
| |
↓ ↓
+----------+ +---------+
|TIME_WAIT | | CLOSED |
+----------+ +---------+
|
↓
+---------+
| CLOSED |
+---------+
5. 滑动窗口机制
发送窗口:
已发送已确认 | 已发送未确认 | 可发送未发送 | 不可发送
←──────────────────────────────────→
发送窗口
接收窗口:
已接收已确认 | 可接收未接收 | 不可接收
←──────────────→
接收窗口
窗口滑动:
- 发送方收到ACK时,窗口向右滑动
- 窗口大小 = min(拥塞窗口, 接收窗口)
6. 拥塞控制算法
慢启动(Slow Start):
cwnd初始值 = 1 MSS
每收到一个ACK: cwnd += 1 MSS
指数增长直到达到ssthresh
拥塞避免(Congestion Avoidance):
cwnd >= ssthresh时
每个RTT: cwnd += 1 MSS
线性增长
快速重传(Fast Retransmit):
收到3个重复ACK
立即重传丢失的数据包
不等待超时
快速恢复(Fast Recovery):
ssthresh = cwnd / 2
cwnd = ssthresh
进入拥塞避免阶段
源码解析
1. TCP连接建立
关键文件: net/ipv4/tcp.c
// TCP connect系统调用
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
int err;
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
// 获取目标地址和端口
daddr = usin->sin_addr.s_addr;
nexthop = daddr;
// 分配本地端口
if (!inet->inet_sport) {
err = inet_hash_connect(&tcp_death_row, sk);
if (err)
return err;
}
// 设置TCP选项
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
// 发送SYN包
err = tcp_connect(sk);
return err;
}
// 发送SYN包
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
// 初始化序列号
tp->write_seq++;
tp->snd_nxt = tp->write_seq;
tp->pushed_seq = tp->write_seq;
// 分配SKB
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
if (unlikely(!buff))
return -ENOBUFS;
// 设置SYN标志
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
// 发送SYN包
err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
// 设置重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
// 进入SYN_SENT状态
tcp_set_state(sk, TCP_SYN_SENT);
return err;
}
2. TCP数据接收
关键文件: net/ipv4/tcp_input.c
// TCP数据接收主函数
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
// 获取TCP头部
th = tcp_hdr(skb);
// 查找socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
// 已建立连接的处理
if (sk->sk_state == TCP_ESTABLISHED) {
ret = tcp_rcv_established(sk, skb, th);
return ret;
}
// 其他状态处理
ret = tcp_v4_do_rcv(sk, skb);
return ret;
}
// 已建立连接的数据接收
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
// 快速路径:顺序数据且无特殊情况
if (tp->rcv_nxt == TCP_SKB_CB(skb)->seq &&
!tcp_paws_discard(sk, skb)) {
// 将数据加入接收队列
__skb_queue_tail(&sk->sk_receive_queue, skb);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
// 发送ACK
tcp_send_ack(sk);
// 唤醒等待的进程
sk->sk_data_ready(sk);
return 0;
}
// 慢速路径:处理乱序、重传等情况
tcp_data_queue(sk, skb);
tcp_ack_snd_check(sk);
return 0;
}
3. 拥塞控制实现
关键文件: net/ipv4/tcp_cong.c
// 拥塞控制接口
struct tcp_congestion_ops {
void (*init)(struct sock *sk);
void (*release)(struct sock *sk);
u32 (*ssthresh)(struct sock *sk);
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
const char *name;
};
// CUBIC拥塞控制算法
static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
struct tcp_sock *tp = tcp_sk(sk);
struct cubictcp *ca = inet_csk_ca(sk);
if (!tcp_is_cwnd_limited(sk))
return;
if (tcp_in_slow_start(tp)) {
// 慢启动阶段
acked = tcp_slow_start(tp, acked);
if (!acked)
return;
}
// 拥塞避免阶段
bictcp_update(ca, tp->snd_cwnd, acked);
tcp_cong_avoid_ai(tp, ca->cnt, acked);
}
️ 实用命令
1. TCP连接观测
ss - Socket统计
# 显示所有TCP连接
ss -tan
# 显示统计摘要
ss -s
# 显示详细信息
ss -ti
# 过滤特定状态
ss -tan state established
ss -tan state time-wait
# 输出字段解释
# State: 连接状态
# Recv-Q: 接收队列未读字节数
# Send-Q: 发送队列未确认字节数
# Local Address:Port: 本地地址和端口
# Peer Address:Port: 对端地址和端口
ss -i 输出示例:
cubic wscale:7,7 rto:240 rtt:31.5/2.4 ato:40 mss:1460 rcvmss:1460
advmss:1460 cwnd:10 bytes_acked:12345 bytes_received:54321
segs_out:100 segs_in:95 send 22.8Mbps lastsnd:1000 lastrcv:1000
lastack:1000 pacing_rate 45.6Mbps rcv_space:14600
2. TCP统计信息
netstat - 网络统计
# 显示TCP统计
netstat -s | grep -i tcp
# 显示TCP连接
netstat -tan
# 显示监听端口
netstat -tln
示例输出:
Tcp:
123456 active connections openings
234567 passive connection openings
12345 failed connection attempts
567 connection resets received
89 connections established
12345678 segments received
23456789 segments send out
1234 segments retransmited
3. TCP参数查看和调整
查看TCP参数:
# 查看所有TCP参数
sysctl -a | grep tcp
# 查看拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 查看窗口参数
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
调整TCP参数:
# 调整拥塞控制算法
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# 调整接收缓冲区
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
# 调整发送缓冲区
sudo sysctl -w net.ipv4.tcp_wmem="4096 16384 4194304"
# 启用TCP fast open
sudo sysctl -w net.ipv4.tcp_fastopen=3
代码示例
1. TCP服务器实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BACKLOG 128
#define BUFFER_SIZE 1024
int main() {
int listen_fd, conn_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
char buffer[BUFFER_SIZE];
// 创建socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
// 设置地址重用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
// 监听
if (listen(listen_fd, BACKLOG) < 0) {
perror("listen");
exit(1);
}
printf("服务器启动,监听端口 %d\n", PORT);
// 接受连接
while (1) {
client_len = sizeof(client_addr);
conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd < 0) {
perror("accept");
continue;
}
printf("接受连接: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 接收数据
int n = recv(conn_fd, buffer, BUFFER_SIZE - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("接收到: %s\n", buffer);
// 回复数据
send(conn_fd, buffer, n, 0);
}
close(conn_fd);
}
close(listen_fd);
return 0;
}
2. TCP客户端实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket");
exit(1);
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// 连接服务器
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
exit(1);
}
printf("连接到服务器 %s:%d\n", SERVER_IP, SERVER_PORT);
// 发送数据
const char *msg = "Hello, TCP!";
send(sock_fd, msg, strlen(msg), 0);
printf("发送: %s\n", msg);
// 接收响应
int n = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("接收: %s\n", buffer);
}
close(sock_fd);
return 0;
}
3. Go语言TCP示例
package main
import (
"bufio"
"fmt"
"net"
"time"
)
// TCP服务器
func tcpServer() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("服务器启动,监听端口 8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("接受连接错误: %v\n", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
fmt.Printf("接受连接: %s\n", conn.RemoteAddr())
// 设置超时
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
// 读取数据
reader := bufio.NewReader(conn)
message, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("读取错误: %v\n", err)
return
}
fmt.Printf("接收到: %s", message)
// 回复数据
conn.Write([]byte(message))
}
// TCP客户端
func tcpClient() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Println("连接到服务器")
// 发送数据
message := "Hello, TCP!\n"
conn.Write([]byte(message))
fmt.Printf("发送: %s", message)
// 接收响应
reader := bufio.NewReader(conn)
response, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("读取错误: %v\n", err)
return
}
fmt.Printf("接收: %s", response)
}
func main() {
// 启动服务器
go tcpServer()
// 等待服务器启动
time.Sleep(1 * time.Second)
// 启动客户端
tcpClient()
// 保持程序运行
time.Sleep(2 * time.Second)
}
动手实验
实验1:抓包分析TCP三次握手
目标: 通过抓包观察TCP连接建立过程
步骤:
- 启动抓包:
# 终端1:启动tcpdump
sudo tcpdump -i lo -nn port 8080 -w tcp_handshake.pcap
# 或使用文本输出
sudo tcpdump -i lo -nn port 8080 -A
- 运行TCP程序:
# 终端2:启动服务器
gcc -o tcp_server tcp_server.c
./tcp_server
# 终端3:运行客户端
gcc -o tcp_client tcp_client.c
./tcp_client
- 分析抓包结果:
# 停止抓包后分析
tcpdump -r tcp_handshake.pcap -nn
# 或使用wireshark
wireshark tcp_handshake.pcap
- 观察要点:
- 第一次握手:SYN标志,seq=x
- 第二次握手:SYN+ACK标志,seq=y, ack=x+1
- 第三次握手:ACK标志,ack=y+1
实验2:观测TCP状态转换
目标: 实时观察TCP连接的状态变化
步骤:
- 创建监控脚本:
#!/bin/bash
# tcp_state_monitor.sh
while true; do
clear
echo "=== TCP状态统计 ==="
ss -tan | awk '{print $1}' | sort | uniq -c | sort -nr
echo ""
echo "=== 端口8080的连接 ==="
ss -tan | grep :8080
sleep 1
done
- 运行监控:
# 终端1:运行监控脚本
chmod +x tcp_state_monitor.sh
./tcp_state_monitor.sh
# 终端2:运行服务器
./tcp_server
# 终端3:多次运行客户端
for i in {1..10}; do ./tcp_client; sleep 1; done
- 观察状态变化:
- LISTEN状态
- SYN_SENT → ESTABLISHED
- ESTABLISHED → FIN_WAIT/CLOSE_WAIT
- TIME_WAIT状态的数量变化
实验3:TCP重传观测
目标: 理解TCP的重传机制
步骤:
- 使用tc模拟丢包:
# 添加丢包规则(丢包率10%)
sudo tc qdisc add dev lo root netem loss 10%
# 查看规则
sudo tc qdisc show dev lo
- 运行TCP程序并抓包:
# 终端1:抓包
sudo tcpdump -i lo -nn port 8080 -w tcp_retrans.pcap
# 终端2:运行服务器和客户端
./tcp_server &
./tcp_client
- 分析重传:
# 查看重传统计
tcpdump -r tcp_retrans.pcap -nn | grep "retransmission"
# 使用wireshark分析
wireshark tcp_retrans.pcap
# 过滤器: tcp.analysis.retransmission
- 清理丢包规则:
sudo tc qdisc del dev lo root
常见问题
Q1: 为什么需要TIME_WAIT状态?
A: TIME_WAIT状态的作用:
- 确保最后的ACK能够到达对方
- 防止旧连接的数据包影响新连接
- 等待时间为2MSL(Maximum Segment Lifetime)
Q2: 如何处理大量TIME_WAIT连接?
A: 解决方案:
- 启用
tcp_tw_reuse
参数 - 使用连接池
- 使用HTTP Keep-Alive
- 调整
tcp_fin_timeout
参数
Q3: TCP如何保证可靠传输?
A: 通过以下机制:
- 序列号和确认号
- 超时重传
- 流量控制(滑动窗口)
- 拥塞控制
Q4: 什么是TCP粘包问题?
A: TCP是字节流协议,不保留消息边界。解决方案:
- 使用固定长度消息
- 使用分隔符
- 使用长度前缀
复习题
选择题
TCP三次握手中,第二次握手发送的标志位是:
- A. SYN
- B. ACK
- C. SYN+ACK
- D. FIN
TIME_WAIT状态持续的时间是:
- A. 1 MSL
- B. 2 MSL
- C. 3 MSL
- D. 永久
TCP拥塞控制的慢启动阶段,拥塞窗口的增长方式是:
- A. 线性增长
- B. 指数增长
- C. 不增长
- D. 随机增长
以下哪个命令可以查看TCP连接的详细信息?
- A. ping
- B. ss
- C. ifconfig
- D. route
TCP快速重传需要收到多少个重复ACK?
- A. 1个
- B. 2个
- C. 3个
- D. 4个
简答题
解释TCP三次握手的详细过程和必要性。
为什么TCP关闭需要四次挥手?
解释TCP滑动窗口机制的工作原理。
比较TCP的慢启动和拥塞避免阶段。
如何诊断TCP连接建立失败的问题?
实战题
网络诊断题: 客户端无法连接到服务器,请设计一个完整的排查流程。
性能优化题: 一个TCP服务出现大量TIME_WAIT连接,导致端口耗尽。请分析原因并提出解决方案。
协议分析题: 通过抓包分析,发现大量TCP重传。请说明可能的原因和解决方法。
扩展阅读
推荐资源
深入方向
- TCP BBR拥塞控制算法
- QUIC协议
- TCP优化技术
- TCP在高延迟网络中的表现
下一章预告: 我们将进入TCP问题排查实战,学习如何诊断和解决实际环境中的各种TCP问题,包括连接失败、延迟抖动、重传等常见问题。