引言:TCP 是运维的基石
作为运维工程师,无论是排查网络故障、分析日志,还是配置负载均衡器,都需要对 TCP 协议有深入理解。很多"疑难杂症"的根源,往往在于对 TCP 状态转换和连接管理理解不够透彻。
本文从 TCP 协议头部开始,详细讲解三次握手和四次挥手的每个细节,配合 Wireshark 抓包分析,帮助初中级运维工程师建立完整的 TCP 知识体系。
前置知识:OSI 七层模型基础、IP 基础概念
实验环境:Linux 系统、Wireshark、tcpdump
1 TCP 协议头部解析
1.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 | |C|E|U|A|P|R|S|F| | | Offset| Flags |W|C|R|C|S|S|Y|I| Window | | | |R|E|G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options and Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1.2 各字段详解
# 使用 tcpdump 查看 TCP 头部 # 捕获 SYN 包 sudo tcpdump -i eth0'tcp[tcpflags] == tcp-syn'-c 5 -v # 输出解析 # IP (tos 0x0, ttl 64, id 0, proto TCP (6), length 60) # 192.168.1.100.45678 > 93.184.216.34.80: Flags [S], seq 0:0, win 65535 # ^^^^ 源端口 ^^^^ 目标端口 ^^^^ SYN 标志
关键字段说明:
| 字段 | 长度 | 作用 |
|---|---|---|
| Source Port | 16bit | 客户端随机选择的端口 |
| Destination Port | 16bit | 服务端监听端口 |
| Sequence Number | 32bit | 数据字节流编号 |
| Acknowledgment Number | 32bit | 期望收到的下一个字节编号 |
| Data Offset | 4bit | TCP 头部长度(4字节为单位) |
| Flags | 9bit | 控制标志 |
| Window | 16bit | 滑动窗口大小 |
| Checksum | 16bit | 头部+数据校验和 |
| Urgent Pointer | 16bit | 紧急数据指针 |
1.3 TCP 标志位详解
# TCP 标志位(9个) # - URG: 紧急指针有效 # - ACK: 确认号有效 # - PSH: 推送数据给应用层 # - RST: 重置连接 # - SYN: 同步序号(建立连接) # - FIN: 结束连接 # - ECE: ECN 显式拥塞通知 # - CWR: 拥塞窗口减小 # - NS: 保留(Nonce Sum) # tcpdump 显示标志位组合 # [S] = SYN # [S.] = SYN + ACK # [.] = ACK # [F] = FIN # [F.] = FIN + ACK # [R] = RST # [R.] = RST + ACK # [P.] = PSH + ACK # [S.] [.] [R] = 三次握手序列 # 捕获所有带 RST 标志的包 sudo tcpdump -i eth0'tcp[tcpflags] & tcp-rst != 0'-n # 捕获带 PSH+ACK 的数据包 sudo tcpdump -i eth0'tcp[tcpflags] == tcp-ack and tcp[tcpflags] & tcp-psh != 0'-n
1.4 序列号与确认号机制
# TCP 序列号计算示例
classTCPSeqAck:
def__init__(self, isn):
# ISN: Initial Sequence Number,初始序列号
self.isn = isn
self.next_expected = isn +1
defsend_data(self, data):
"""发送数据,返回序列号"""
seq = self.next_expected
self.next_expected += len(data)
returnseq
defreceive_ack(self, ack):
"""收到 ACK,计算确认号"""
ifack > self.next_expected:
# 确认号大于期望,可能是重复 ACK 或数据丢失
return"ACK is larger than expected"
elifack == self.next_expected:
return"Full ACK - all data received"
else:
return"Partial ACK"
# 示例
tcp = TCPSeqAck(isn=1000)
seq1 = tcp.send_data(b"Hello") # seq=1000, 发送 "Hello" (5 bytes)
print(f"发送数据,序列号:{seq1}, 下一个期望:{tcp.next_expected}")
# 对方返回 ACK
result = tcp.receive_ack(1005) # 确认号 1005
print(f"收到 ACK 1005:{result}")
2 三次握手深度解析
2.1 为什么需要三次握手?
客户端 服务端 | | | 问题:客户端发出的 SYN 可能在网络中滞留 | | | | 滞留的旧 SYN 到达服务端 | | --> 服务端 认为是新连接请求 | | --> 服务端 分配资源等待客户端响应 | | --> 但客户端 早已关闭连接 | | | | 解决方案:第三次握手让客户端确认 | | --> 客户端 收到 SYN+ACK | | --> 客户端 检查序列号是否是自己发出的 | | --> 确认后才发送 ACK | | | | 如果是旧 SYN,客户端会发送 RST | | |
三次握手核心目的:
验证双方发送和接收能力正常
协商初始序列号(ISN)
防止旧连接请求干扰新连接
2.2 三次握手详细过程
时间线 客户端 服务端 | | | 1. CLOSED | 1. LISTEN | | | --> 选择客户端 ISN (c_isn) | | --> 发送 SYN | T1 |------------------------------------>| | SEQ=c_isn | | Flags=[SYN] | | 状态: SYN_SENT | | | | 2. 收到 SYN | | --> 选择服务端 ISN (s_isn) | --> 发送 SYN+ACK T2 |<------------------------------------| | SEQ=s_isn | | ACK=c_isn+1 | | Flags=[SYN,ACK] | | 状态: SYN_RECEIVED | | | | 3. 收到 SYN+ACK | | --> 验证 ACK 是否正确 | | --> 发送 ACK | T3 |------------------------------------>| | SEQ=c_isn+1 | | ACK=s_isn+1 | | Flags=[ACK] | | 状态: ESTABLISHED | | | | 4. 收到 ACK | | --> 验证 ACK | | --> 状态: ESTABLISHED | | | 双向通信建立完成 | | |
2.3 Wireshark 三次握手实战
# 捕获 HTTP 握手包 sudo tcpdump -i eth0'tcp port 80 and tcp[tcpflags] & tcp-syn != 0'-w /tmp/handshake.pcap # 在另一个终端发起请求 curl -I http://example.com # 停止捕获,用 Wireshark 分析 wireshark /tmp/handshake.pcap & # 或用 tshark 命令行分析 tshark -r /tmp/handshake.pcap -Y"tcp.flags.syn==1 or tcp.flags.synack==1 or tcp.flags.ack==1"
Wireshark 抓包结果解析:
Frame 1: 62 bytes on wire, 62 bytes captured Ethernet, Src: VMware_xxxx, Dst: Intel_xxxx Internet Protocol Version 4, Src: 192.168.1.100, Dst: 93.184.216.34 Transmission Control Protocol, Src Port: 45678, Dst Port: 80 Source Port: 45678 Destination Port: 80 [Stream index: 0] Sequence number: 0 (relative sequence number) Acknowledgment number: 0 0110 .... = Header Length: 32 bytes (8) Flags: 0x002 (SYN) Window size value: 65535 Checksum: 0xabcd [unverified] Options: (12 bytes) Maximum segment size: 1460 bytes WS: 7 No-Operation (NOP) No-Operation (NOP) Timestamps: TSval 1234567890, TSecr 0 No-Operation (NOP) No-Operation (NOP) SackOK: sack permits Frame 2: 62 bytes TCP Flags: 0x012 (SYN, ACK) Sequence number: 0 (relative sequence number) Acknowledgment number: 1 (relative ack number) Options: (12 bytes) Maximum segment size: 1460 bytes Timestamps: TSval 987654321, TSecr 1234567890 ...
2.4 ISN 随机化与安全性
# ISN (Initial Sequence Number) 随机化原理 # RFC 793 定义:ISN = M + F(localip, localport, remoteip, remoteport, secret) # F 是一个哈希函数,产生 32 位随机值 # 查看系统 ISN 生成策略 cat /proc/sys/net/ipv4/tcp_syncookies # 1 = 启用 SYN Cookie # 查看当前连接的 ISN ss -ti # State Recv-Q Send-Q Local Address:Port Peer Address:Port Process # ESTAB 0 0 192.168.1.100:45678 93.184.216.34:80 # ts sack reno wscale:7,7 --> TS val 1234567890 ecr 0 # ISN 预测攻击演示(不要在生产环境操作) # ISN 应该每次都随机,防止攻击者预测下一个 ISN
2.5 三次握手异常场景
# 场景 1:SYN 泛洪攻击 # 攻击者发送大量 SYN,但不完成第三次握手 # 服务端资源被耗尽 # 防御措施 # 查看 SYN Flood 状态 netstat -an | grep SYN_RECV | wc -l # 启用 SYN Cookie echo1 > /proc/sys/net/ipv4/tcp_syncookies # 调整 SYN Backlog echo2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog echo1 > /proc/sys/net/ipv4/tcp_synack_retries # 场景 2:连接超时 # 网络延迟过大导致握手超时 ss -o state syn-sent # Timer:(connect timeout) # 场景 3:端口未监听 # 发送 SYN 后收到 RST # 目的端口没有进程监听 # tcpdump 观察 sudo tcpdump -i eth0'tcp[tcpflags] & tcp-rst != 0'-n
3 四次挥手深度解析
3.1 为什么是四次挥手?
客户端 服务端 | | | 关闭连接的原因: | | 1. TCP 是全双工(bidirectional) | | 2. 双方各自独立关闭发送通道 | | 3. 每一方都需要发送 FIN 并收到 ACK | | | | 为什么挥手需要 4 个包? | | | | 主动关闭方 --> FIN --> 被动关闭方 | (关闭发送通道) | 被动关闭方 --> ACK --> 主动关闭方 | | | | 被动关闭方 --> FIN --> 主动关闭方 | (对方也关闭发送通道) | 主动关闭方 --> ACK --> 被动关闭方 | | |
关键点:
主动关闭方发送 FIN,表示"我不会再发送数据了"
被动关闭方收到 FIN 后返回 ACK,但此时可能仍有数据要发送
被动关闭方数据发送完毕后,才发送自己的 FIN
TIME_WAIT 状态确保最后的 ACK 能到达对方
3.2 四次挥手详细过程
时间线 客户端 服务端 | | | 假设当前状态: ESTABLISHED | | | | 客户端应用进程调用 close() | | --> 发送 FIN,进入 FIN_WAIT_1 | T1 |------------------------------------>| | SEQ=1000, ACK=2000 | | Flags=[FIN,ACK] | | 状态: FIN_WAIT_1 | | | | 2. 收到 FIN | | --> 发送 ACK | T2 |<------------------------------------| | SEQ=2000, ACK=1001 | | Flags=[ACK] | | 状态: CLOSE_WAIT | | (服务端等待应用进程处理完数据) | | | | 3. 收到 ACK | | --> 进入 FIN_WAIT_2 | | 状态: FIN_WAIT_2 | | (等待服务端的 FIN) | | | | 4. 应用进程 | | 处理完数据 | | --> 调用 close| | --> 发送 FIN | T3 |<------------------------------------| | SEQ=2000, ACK=1001 | | Flags=[FIN,ACK] | | 状态: LAST_ACK | | | | 5. 收到 FIN | | --> 发送 ACK | T4 |------------------------------------>| | SEQ=1001, ACK=2001 | | Flags=[ACK] | | 状态: TIME_WAIT | | | | 6. 等待 2MSL 后 | | --> 进入 CLOSED | | | | 7. 收到 ACK | | --> 进入 CLOSED| | |
3.3 TIME_WAIT 状态详解
# TIME_WAIT 的作用 # 1. 确保最后的 ACK 能到达对方 # - ACK 可能丢失 # - 服务端 会重发 FIN # - 如果客户端已关闭,服务端无法收到 ACK # 2. 等待网络中所有旧数据包消散 # - 防止延迟的旧数据包被新连接误收 # - MSL (Maximum Segment Lifetime) = 2分钟 # 查看 TIME_WAIT 连接数 netstat -an | grep TIME_WAIT | wc -l # 查看各状态连接数 ss -s # Total: 256 (kernel 512) # TCP: 6 (kernel 6) # ... # TIME_WAIT 超时时间 # Linux 默认 60 秒(2MSL,通常 MSL=30秒) cat /proc/sys/net/ipv4/tcp_fin_timeout # 输出:60 # 调整 TIME_WAIT 超时(谨慎) echo30 > /proc/sys/net/ipv4/tcp_fin_timeout # 启用 TIME_WAIT 复用 echo1 > /proc/sys/net/ipv4/tcp_tw_reuse # 启用快速回收(慎用,可能导致问题) echo1 > /proc/sys/net/ipv4/tcp_tw_recycle
3.4 CLOSE_WAIT 状态问题
# CLOSE_WAIT 问题的原因 # 服务端收到客户端 FIN 后返回 ACK # 但服务端的应用程序没有调用 close() # 导致连接一直处于 CLOSE_WAIT 状态 # 排查 CLOSE_WAIT netstat -an | grep CLOSE_WAIT | head -20 # 输出示例: # Proto Recv-Q Send-Q Local Address Foreign Address State # tcp 0 0 0.0.0.0:3306 192.168.1.100:45678 CLOSE_WAIT # 定位问题进程 ss -tlnp | grep :3306 # 查看哪些进程持有连接 # 示例:Python 应用程序未关闭连接 python3 << 'EOF' import socket def handle_client(client_sock, addr): # 问题:函数结束时未调用 client_sock.close() # 这会导致 CLOSE_WAIT data = client_sock.recv(1024) client_sock.sendall(b"OK") # 正确做法: def handle_client_correct(client_sock, addr): try: data = client_sock.recv(1024) client_sock.sendall(b"OK") finally: client_sock.close() # 确保关闭 EOF
3.5 四次挥手异常场景
# 场景 1:RST 强制关闭 # 一方发送 RST,另一方立即关闭 # 触发 RST 的情况 # - 访问不存在的连接(如服务器崩溃后重启) # - SO_LINGER 设置为 0 # - 故意abort连接 # 查看 RST 包 sudo tcpdump -i eth0'tcp[tcpflags] & tcp-rst != 0'-n # 场景 2:FIN_WAIT_2 超时 # 客户端进入 FIN_WAIT_2,但服务端不发送 FIN # 默认 60 秒后自动关闭 # 场景 3:大量 TIME_WAIT # 高并发短连接场景 # 解决方案: # 1. 调整 tcp_fin_timeout # 2. 启用 tcp_tw_reuse # 3. 使用 SO_LINGER # 4. 客户端使用 HTTP Keep-Alive # 场景 4:LAST_ACK 状态长时间存在 # 服务端发送 FIN 后未收到 ACK # 可能网络问题或对端崩溃
4 TCP 状态转换图
4.1 完整状态转换图
应用层调用
|
v
+-----------+
| CLOSED |
+-----------+
|
| 被动打开 (listen)
v
+-----------+
+----------->| LISTEN |<-----------+
| +-----------+ |
| | |
| 主动发送 SYN | | 收到 SYN
| | | 发送 SYN+ACK
v v |
+-----------+ +-----------+ |
| SYN_SENT | | SYN_RCVD |<-----------+
+-----------+ +-----------+ 收到 ACK
| |
| |
| 收到 SYN+ACK | 收到 ACK
| |
v v
+---------------------------+
| ESTABLISHED |
| (数据传输状态) |
+---------------------------+
| ^
| |
| 主动 close | 被动 close
| 发送 FIN | 收到 FIN
v |
+-----------+ |
|FIN_WAIT_1 | |
+-----------+ |
| |
| 收到 ACK | 收到 FIN
| (半关闭) | 发送 ACK
v |
+-----------+ |
|FIN_WAIT_2 |<-----------+
+-----------+ 收到 ACK
|
| 收到 FIN
| 发送 ACK
v
+-----------+
|TIME_WAIT |
+-----------+
|
| 2MSL 超时
v
+-----------+
| CLOSED |
+-----------+
服务端状态:
LISTEN -> SYN_RCVD -> ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED
客户端状态:
CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
4.2 状态查看命令
# 查看所有 TCP 状态
ss -ant
# 各状态含义
# LISTEN: 监听中
# SYN_SENT: 客户端已发送 SYN
# SYN_RECEIVED: 服务端收到 SYN
# ESTABLISHED: 连接已建立
# FIN_WAIT_1: 主动关闭,已发送 FIN
# FIN_WAIT_2: 收到 ACK,等待对方 FIN
# CLOSE_WAIT: 被动关闭,收到 FIN,等待应用关闭
# CLOSING: 双方同时关闭
# LAST_ACK: 最后确认状态
# TIME_WAIT: 等待 2MSL
# 统计各状态数量
ss -ant | awk'{print $1}'| sort | uniq -c | sort -rn
# 查看特定状态
ss -ant state time-wait
ss -ant state close-wait
ss -ant state syn-sent
# 查看进程对应的连接
ss -tlnp
# 输出示例:
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
5 连接管理实战
5.1 半连接队列与全连接队列
# 半连接队列 (SYN Queue) # 服务端收到 SYN 后进入此队列 # 大小由 tcp_max_syn_backlog 控制 cat /proc/sys/net/ipv4/tcp_max_syn_backlog # 默认值:128 (Linux 2.6+) # 全连接队列 (Accept Queue) # 完成三次握手后,accept() 之前进入此队列 # 大小由 listen() 的 backlog 参数决定 # 查看队列溢出 netstat -s | grep -i"overflow|listen" # TCPBacklogDrop: 12345 # 查看当前队列状态 ss -ltn # Recv-Q: 当前 accept 队列中的连接数 # Send-Q: 对端未确认的连接数 # 调整参数 echo2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog echo1024 > /proc/sys/net/core/somaxconn
5.2 TCP Keepalive
# TCP Keepalive 作用 # 检测空闲连接是否仍然存活 # 适用于:长连接、HTTP 长轮询 # 系统级配置 # /proc/sys/net/ipv4/tcp_keepalive_* # 启用 Keepalive echo1 > /proc/sys/net/ipv4/tcp_keepalive_probes # Keepalive 空闲时间(秒) echo7200 > /proc/sys/net/ipv4/tcp_keepalive_time # 探测间隔(秒) echo75 > /proc/sys/net/ipv4/tcp_keepalive_intvl # 探测次数 echo9 > /proc/sys/net/ipv4/tcp_keepalive_probes # 应用程序设置 Keepalive python3 << 'EOF' import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Linux 特定选项 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7200) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 75) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 9) EOF # Java 设置 # socket.setKeepAlive(true); # -Dtcp.keepalive.time=7200 # -Dtcp.keepalive.intvl=75 # -Dtcp.keepalive.probes=9
5.3 TCP 保活机制
# 查看连接空闲时间 ss -ti state established | grep -E"timer|Idle" # 查看 Keepalive 状态 cat /proc/net/sockstat # sk_refcnt: 引用计数 # timer: 定时器状态 # 示例:Nginx 配置 keepalive # /etc/nginx/nginx.conf http { upstream backend { server 127.0.0.1:8080; keepalive 32; # 保持的空闲连接数 } server { location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection""; # 或 # proxy_set_header Connection "keep-alive"; } } }
5.4 连接复用与优化
# tcp_timestamps 允许精确的 RTT 计算和 PAWS cat /proc/sys/net/ipv4/tcp_timestamps # 1 = 启用 # tcp_sack 允许选择性确认 cat /proc/sys/net/ipv4/tcp_sack # 1 = 启用 # tcp_window_scaling 允许窗口缩放 cat /proc/sys/net/ipv4/tcp_window_scaling # 1 = 启用 # 查看当前连接的窗口大小 ss -ti # wscale: 发送窗口缩放因子 # rcv_wscale: 接收窗口缩放因子 # 调整 MTU 和 MSS # MSS = MTU - IP头(20) - TCP头(20) # 以太网 MTU 通常 1500 # MSS 典型值:1460 # 查看 MSS ss -i | grep -E"rcv_space|snd_wnd"
6 常见 TCP 问题排查
6.1 连接超时
# 排查步骤 # 1. 检查网络连通性 ping -c 5 target_host # 2. 检查路由 traceroute target_host # 或 mtr target_host # 3. 检查目标端口是否开放 nc -zv target_host 80 # 或 nmap -p 80 target_host # 4. 检查本地端口范围 cat /proc/sys/net/ipv4/ip_local_port_range # 通常:32768 60999 # 5. 抓包分析 sudo tcpdump -i eth0 host target_host and port 80 -w /tmp/timeout.pcap # 6. 分析三次握手 tshark -r /tmp/timeout.pcap -Y"tcp.flags.syn==1"-T fields -e frame.time -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport
6.2 连接被重置
# 排查 RST 原因 # 1. 端口未监听 sudo tcpdump -i eth0'tcp[tcpflags] & tcp-rst != 0'-n # 2. 防火墙拦截 sudo iptables -L -n | grep DROP sudo iptables -L -n | grep REJECT # 3. 服务崩溃 journalctl -u nginx | tail -50 systemctl status nginx # 4. 常见 RST 场景 # - 连接请求发送到未监听的端口 # - SO_LINGER 设置为 0 # - 服务器重启 # - 应用调用 close() 而对方未读取数据
6.3 连接队列满
# 现象:连接建立成功但无法通信 # 检查半连接队列溢出 netstat -s | grep -i"SYN" # TCPRcvCoalesce: 12345 # TCPOFODrop: 123 # 检查全连接队列溢出 ss -ltn | grep Recv-Q # 如果 Recv-Q 持续等于 backlog,说明队列满 # 增加队列大小 # 方法 1:临时调整 echo8192 > /proc/sys/net/core/somaxconn echo8192 > /proc/sys/net/ipv4/tcp_max_syn_backlog # 方法 2:永久调整 # /etc/sysctl.conf cat >> /etc/sysctl.conf << 'EOF' net.core.somaxconn = 8192 net.ipv4.tcp_max_syn_backlog = 8192 net.ipv4.ip_local_port_range = 32768 60999 EOF sysctl -p # 方法 3:Nginx 配置 # nginx.conf # listen 80 backlog=8192;
6.4 TIME_WAIT 过多
# 现象:连接数达到上限
# 检查 TIME_WAIT 数量
ss -ant | awk'{print $1}'| sort | uniq -c | sort -rn
# 如果 TIME_WAIT 过多(> 10000)
# 解决方案:
# 1. 调整 tcp_fin_timeout
echo30 > /proc/sys/net/ipv4/tcp_fin_timeout
# 2. 启用 tcp_tw_reuse(客户端)
echo1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 3. 使用 SO_LINGER 强制关闭
# 应用程序代码
struct linger ling;
ling.l_onoff = 1;
ling.l_linger = 0;
setsockopt(sock, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
close(sock);
# 注意:这会导致 RST,可能造成数据丢失
# 4. 客户端使用 HTTP Keep-Alive
# Nginx 作为反向代理时:
# upstream 中配置 keepalive
# 5. 观察实际影响
ss -s
# 如果实际连接数不高,TIME_WAIT 多是正常的
7 Wireshark 高级分析
7.1 常用过滤表达式
# 基本过滤 tcp.port == 80 # 端口 80 tcp.srcport == 12345 # 源端口 tcp.dstport == 443 # 目标端口 ip.addr == 192.168.1.100 # IP 地址 tcp.flags.syn == 1 # SYN 标志 tcp.flags.ack == 1 # ACK 标志 tcp.flags.fin == 1 # FIN 标志 tcp.flags.rst == 1 # RST 标志 # 组合过滤 tcp.port == 80 and ip.addr == 192.168.1.100 tcp.flags.syn == 1 and tcp.flags.ack == 0 # 纯粹的 SYN tcp.flags.syn == 1 and tcp.flags.ack == 1 # SYN+ACK # 序列号过滤 tcp.seq == 1000 # 特定序列号 tcp.ack == 2000 # 特定确认号 # 时间过滤 frame.time_relative < 1 # 相对时间 < 1秒 # 专家信息 tcp.analysis.retransmission # 重传 tcp.analysis.duplicate_ack # 重复 ACK tcp.analysis.out_of_order # 乱序 tcp.analysis.fast_retransmission # 快速重传
7.2 跟随 TCP 流
# Wireshark 中跟随 TCP 流 # 右键点击包 -> Follow -> TCP Stream # tshark 命令行跟随流 # 1. 找到流索引 tshark -r /tmp/capture.pcap -q -z"conv,tcp"| head -20 # 2. 跟随特定流 tshark -r /tmp/capture.pcap -Y"tcp.stream eq 0"-T fields -e data # 3. 导出完整 HTTP 会话 tshark -r /tmp/capture.pcap -Y"http"-T fields -e ip.src -e http.request.method -e http.request.uri
7.3 TCP 统计分析
# 流统计 tshark -r /tmp/capture.pcap -q -z"io,stat,0.1,tcp.len"| head -30 # 连接统计 tshark -r /tmp/capture.pcap -q -z"conv,tcp" # 重传统计 tshark -r /tmp/capture.pcap -q -z"io,stat,0.1,tcp.analysis.retransmission" # 绘制时间序列图 # Wireshark -> Statistics -> TCP Stream Graphs # - Time-Sequence Graph (Stevens) # - Throughput Graph # - Round Trip Time Graph
7.4 抓包脚本
#!/bin/bash
# tcp_capture.sh - TCP 抓包脚本
CAPTURE_FILE="/tmp/tcp_capture_$(date +%Y%m%d_%H%M%S).pcap"
FILTER="$1"
DURATION="${2:-60}"# 默认 60 秒
if[ -z"$FILTER"];then
echo"用法:$0<过滤器> [持续秒数]"
echo"示例:$0'tcp port 80' 120"
exit1
fi
echo"开始抓包..."
echo"过滤器:$FILTER"
echo"持续时间:${DURATION}秒"
echo"输出文件:$CAPTURE_FILE"
# 使用 tcpdump 抓包
sudo tcpdump -i eth0 -w"$CAPTURE_FILE""$FILTER"&
PID=$!
# 等待指定时间
sleep"$DURATION"
# 停止抓包
kill$PID2>/dev/null
wait$PID2>/dev/null
echo"抓包完成"
echo"文件大小:$(du -h "$CAPTURE_FILE" | cut -f1)"
# 快速统计
echo""
echo"=== 抓包统计 ==="
echo"总包数:$(sudo tcpdump -r "$CAPTURE_FILE" 2>/dev/null | wc -l)"
echo"SYN 包:$(sudo tcpdump -r "$CAPTURE_FILE" 'tcp[tcpflags] == tcp-syn' 2>/dev/null | wc -l)"
echo"FIN 包:$(sudo tcpdump -r "$CAPTURE_FILE" 'tcp[tcpflags] == tcp-fin' 2>/dev/null | wc -l)"
echo"RST 包:$(sudo tcpdump -r "$CAPTURE_FILE" 'tcp[tcpflags] == tcp-rst' 2>/dev/null | wc -l)"
8 实战案例分析
案例一:Web 服务偶发性连接失败
现象:用户反馈网站偶尔打不开,刷新后正常
排查过程:
# 1. 在服务端抓包 sudo tcpdump -i eth0 -w /tmp/http_issue.pcap'tcp port 80'& sleep 30 # 等待问题复现 kill%1 # 2. 分析抓包文件 tshark -r /tmp/http_issue.pcap -Y"tcp.flags.syn==1"-T fields -e frame.time_relative -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport -e tcp.len | head -20 # 3. 查找 SYN 重传 tshark -r /tmp/http_issue.pcap -Y"tcp.analysis.retransmission"| wc -l # 如果有重传,说明网络或服务端有问题 # 4. 检查服务端 backlog ss -ltn | grep :80 # Recv-Q 应该接近 0,Send-Q 应该较小 # 5. 查看系统连接限制 cat /proc/sys/net/core/somaxconn cat /proc/sys/net/ipv4/tcp_max_syn_backlog
根因:服务端 somaxconn 过小,高峰期 SYN 队列溢出。
解决:
# 增加队列大小 echo4096 > /proc/sys/net/core/somaxconn echo8192 > /proc/sys/net/ipv4/tcp_max_syn_backlog # Nginx 配置 # worker_processes auto; # worker_connections 4096; # listen 80 backlog=4096;
案例二:数据库连接池耗尽
现象:应用日志显示 "Too many connections"
# 1. 检查 MySQL 连接数
mysql -u root -p -e"SHOW PROCESSLIST;"| wc -l
mysql -u root -p -e"SHOW STATUS LIKE 'Threads_connected';"
# 2. 检查 TIME_WAIT
netstat -an | grep TIME_WAIT | wc -l
# 大量 TIME_WAIT 说明连接没有正确复用
# 3. 检查连接来源
netstat -ant | awk'{print $5}'| cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# 4. 查看慢查询
mysql -u root -p -e"SHOW GLOBAL STATUS LIKE 'Slow_queries';"
根因:应用使用短连接,每次请求都创建新连接,高并发时耗尽连接池。
解决:
# 方案 1:使用连接池 importmysql.connector.pool pool = mysql.connector.pooling.MySQLConnectionPool( pool_name="mypool", pool_size=10, host="localhost", database="test" ) # 使用连接 conn = pool.get_connection() cursor = conn.cursor() # ... 操作后归还连接 conn.close() # 实际归还到池中
案例三:API 请求超时
现象:移动端 API 调用超时率高
# 1. 检查网络质量 ping -c 20 api.example.com # 查看 RTT 抖动 # 2. 分析 TCP 重传 sudo tcpdump -i eth0 -w /tmp/api_tcp.pcap'host api.example.com'& # 复现问题 tshark -r /tmp/api_tcp.pcap -Y"tcp.analysis.retransmission"| wc -l # 3. 检查 TCP 窗口 # 在 Wireshark 中查看 Window Size 变化 # 如果 Window 接近 0,说明接收端 buffer 满 # 4. 查看服务处理时间 # 应用日志中的请求处理时间 # nginx access log 的 response_time
根因:服务端处理慢,TCP 窗口收缩,客户端收不到数据而超时。
解决:
# 1. 增加服务端 buffer echo16777216 > /proc/sys/net/core/rmem_max echo16777216 > /proc/sys/net/core/wmem_max # 2. Nginx 配置 # proxy_buffering on; # proxy_buffer_size 128k; # proxy_buffers 4 256k; # 3. 应用优化 # - 增加处理线程 # - 使用异步处理 # - 优化数据库查询
9 总结与速查表
三次握手状态机
客户端 服务端 CLOSED ──────────────────────────> LISTEN | | | 1. 发送 SYN | | SEQ=c_isn | | ─────────────────────────────> | | | | SYN_SENT LISTEN | | | <───────────────────────────── | | 2. 收到 SYN+ACK | | SEQ=s_isn | | ACK=c_isn+1 | | | | SYN_RCVD | | | | 3. 发送 ACK | | ACK=s_isn+1 | | ──────────────────────────────> | | | | ESTABLISHED ESTABLISHED | |
四次挥手状态机
主动方 被动方 ESTABLISHED ──────────────────────> ESTABLISHED | | | 1. 发送 FIN | | SEQ=x | | ──────────────────────────────> | | | | FIN_WAIT_1 CLOSE_WAIT | | | <───────────────────────────── | | 2. 收到 ACK | | ACK=x+1 | | | | FIN_WAIT_2 CLOSE_WAIT | | | | 3. 数据发送完毕 | | 调用 close() | | 发送 FIN | | SEQ=y | <───────────────────────────── | | 4. 收到 FIN | | SEQ=y | | 发送 ACK | | ACK=y+1 | | | | TIME_WAIT LAST_ACK | | | | | <───────────────────────────── | | 5. 收到 ACK | | | | CLOSED CLOSED
TCP 状态速查
| 状态 | 客户端 | 服务端 | 说明 |
|---|---|---|---|
| LISTEN | ✓ | 等待连接 | |
| SYN_SENT | ✓ | 已发送 SYN | |
| SYN_RCVD | ✓ | 收到 SYN | |
| ESTABLISHED | ✓ | ✓ | 连接建立 |
| FIN_WAIT_1 | ✓ | 已发 FIN | |
| FIN_WAIT_2 | ✓ | 收到 ACK | |
| CLOSE_WAIT | ✓ | 收到 FIN | |
| TIME_WAIT | ✓ | 等待 2MSL | |
| LAST_ACK | ✓ | 最后确认 |
常用命令速查
# 查看连接状态 ss -ant # 所有 TCP 连接 ss -ti # 连接详细信息(带 timer) ss -tlnp # 监听端口 # 抓包分析 tcpdump -i eth0'tcp port 80'-w a.pcap tshark -r a.pcap -Y"tcp"-T fields # 查看网络参数 cat /proc/sys/net/ipv4/tcp_* sysctl -a | grep tcp # 连接统计 netstat -s | grep -i tcp ss -s
故障排查流程
TCP 连接异常
|
├── 三次握手问题
| ├── SYN 没发出 -> 检查网络
| ├── SYN 没收到 -> 检查防火墙
| ├── SYN+ACK 没收到 -> 抓包分析
| └── ACK 没发出 -> 抓包分析
|
├── 四次挥手问题
| ├── FIN 没发出 -> 检查应用 close()
| ├── TIME_WAIT 多 -> 调整参数或连接复用
| └── CLOSE_WAIT 多 -> 应用未关闭连接
|
├── 连接中断
| ├── RST 原因 -> 抓包分析
| ├── 重传过多 -> 检查网络质量
| └── 超时 -> 检查延迟
|
└── 性能问题
├── 窗口为 0 -> 增加 buffer
├── 队列满 -> 增加 somaxconn
└── 大量短连接 -> 使用连接池
-
网络
+关注
关注
14文章
8327浏览量
95546 -
TCP
+关注
关注
8文章
1432浏览量
83757 -
模型
+关注
关注
1文章
3810浏览量
52253
原文标题:初中级运维怎么理解 TCP 三次握手和四次挥手?
文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
TCP/IP协议工作过程的三次握手和四次挥手
TCP三次握手过程及四次挥手过程说明
TCP三次握手和四次挥手以及11种状态资料下载
如何使用WireShark进行TCP三次握手
TCP建立连接概述及三次握手、四次挥手的流程
怎么理解TCP三次握手和四次挥手
评论