怎么理解TCP三次握手和四次挥手

描述

引言: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 序列号计算示例
class TCPSeqAck:
    def __init__(self, isn):
        # ISN: Initial Sequence Number,初始序列号
        self.isn = isn
        self.next_expected = isn + 1

    def send_data(self, data):
        """发送数据,返回序列号"""
        seq = self.next_expected
        self.next_expected += len(data)
        return seq

    def receive_ack(self, ack):
        """收到 ACK,计算确认号"""
        if ack > self.next_expected:
            # 确认号大于期望,可能是重复 ACK 或数据丢失
            return "ACK is larger than expected"
        elif ack == 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
echo 1 > /proc/sys/net/ipv4/tcp_syncookies

# 调整 SYN Backlog
echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
echo 1 > /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 超时(谨慎)
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout

# 启用 TIME_WAIT 复用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

# 启用快速回收(慎用,可能导致问题)
echo 1 > /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: 对端未确认的连接数

# 调整参数
echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
echo 1024 > /proc/sys/net/core/somaxconn

 

5.2 TCP Keepalive

 

# TCP Keepalive 作用
# 检测空闲连接是否仍然存活
# 适用于:长连接、HTTP 长轮询

# 系统级配置
# /proc/sys/net/ipv4/tcp_keepalive_*

# 启用 Keepalive
echo 1 > /proc/sys/net/ipv4/tcp_keepalive_probes

# Keepalive 空闲时间(秒)
echo 7200 > /proc/sys/net/ipv4/tcp_keepalive_time

# 探测间隔(秒)
echo 75 > /proc/sys/net/ipv4/tcp_keepalive_intvl

# 探测次数
echo 9 > /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:临时调整
echo 8192 > /proc/sys/net/core/somaxconn
echo 8192 > /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
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout

# 2. 启用 tcp_tw_reuse(客户端)
echo 1 > /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"
    exit 1
fi

echo "开始抓包..."
echo "过滤器: $FILTER"
echo "持续时间: ${DURATION} 秒"
echo "输出文件: $CAPTURE_FILE"

# 使用 tcpdump 抓包
sudo tcpdump -i eth0 -w "$CAPTURE_FILE" "$FILTER" &
PID=$!

# 等待指定时间
sleep "$DURATION"

# 停止抓包
kill $PID 2>/dev/null
wait $PID 2>/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 队列溢出。

解决

 

# 增加队列大小
echo 4096 > /proc/sys/net/core/somaxconn
echo 8192 > /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:使用连接池
import mysql.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
echo 16777216 > /proc/sys/net/core/rmem_max
echo 16777216 > /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
        └── 大量短连接    -> 使用连接池

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分