Linux 基础体系 · 第 18/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 网络栈基础:网卡、协议栈、Socket、端口和连接状态

一、先建立整体模型

Linux 网络通信可以分成几层:

flowchart LR
    A[应用程序] --> B[Socket API]
    B --> C[内核 Socket 与传输层]
    C --> D[TCP/UDP]
    D --> E[IP 路由与网络层]
    E --> F[邻居发现 ARP/NDP]
    F --> G[链路层与 qdisc]
    G --> H[网卡驱动/NIC]
    H --> I[交换机、路由器或对端主机]

这些名称分别解决不同问题:

  • 网卡:向物理或虚拟网络发送、接收二层帧。
  • 协议栈:按照协议规则处理帧、IP 包和 TCP/UDP 报文。
  • Socket:应用程序使用的内核通信对象和编程接口。
  • 端口:传输层用于标识进程通信端点的数值。
  • 连接状态:尤其是 TCP 为可靠连接维护的状态机状态。

一个 TCP 请求从客户端访问服务器,典型路径是:

  1. 客户端程序调用 connect() 或发送数据。
  2. 内核根据 Socket 和路由表构造 TCP 报文。
  3. IP 层选择源地址、下一跳和出口网卡。
  4. 链路层通过 ARP 或 IPv6 NDP 获得下一跳的链路层地址。
  5. 网卡驱动把数据交给网卡发送。
  6. 对端网卡接收帧,经过协议栈校验并交给对应 Socket。
  7. 服务端程序通过 accept()recv() 或更高层框架读取数据。

其中任一环节失败,都可能表现为“网络不通”,但故障原因完全不同。例如:

  • 没有网卡地址,可能无法正确选择源地址;
  • 没有路由,数据包无法决定出口;
  • 防火墙丢弃 SYN,客户端可能超时;
  • 服务端没有监听端口,客户端通常收到 ECONNREFUSED
  • TCP 已建立,但应用没有读取数据,发送方仍可能因接收窗口缩小而阻塞。

因此,“端口开了”并不等于“应用一定可用”。


二、网卡:Linux 与网络设备之间的边界

2.1 网卡不只是物理设备

在 Linux 中,应用通常不直接操作硬件网卡,而是通过内核中的 网络设备(netdevice) 访问它。物理网卡可能显示为 eth0enp1s0 等名称;虚拟环境中还可能出现:

  • lo:回环设备;
  • veth*:网络命名空间之间的虚拟以太网对;
  • br*:网桥;
  • bond*:链路聚合设备;
  • tap*tun*:用户态或虚拟网络设备;
  • docker0virbr0:容器或虚拟化环境创建的网桥。

网卡名称是设备标识,不是 IP 地址,也不是端口。一个设备可以拥有多个 IPv4 或 IPv6 地址;一个地址也可能因为策略路由、命名空间等原因出现在不同的逻辑网络环境中。

使用 ip 查看设备和地址:

ip -br link
ip -br addr

可能看到:

lo               UNKNOWN        127.0.0.1/8 ::1/128
enp1s0           UP             192.0.2.10/24 fe80::5054:ff:fe12:3456/64

这里:

  • UP 表示设备被管理员启用;
  • UNKNOWN 是回环设备常见状态,不代表回环不可用;
  • 192.0.2.10/24 是 IPv4 地址及前缀长度;
  • fe80::.../64 是链路本地 IPv6 地址;
  • 地址存在不代表已经配置了正确路由,也不代表远端服务可达。

ip link setip addr add 等命令修改的是当前网络命名空间中的内核状态,通常需要 root 权限。例如:

sudo ip link set dev enp1s0 up

这只启用设备,不会自动配置地址或默认路由。在生产机器上直接删除地址、修改链路状态,可能立即中断远程连接,必须确认控制台或带外恢复手段。

2.2 接收路径和发送路径

现代 Linux 网卡通常使用 DMA 把数据包放入内存,再由驱动和内核处理。接收方向的抽象过程是:

  1. 网卡收到二层帧;
  2. 网卡通过 DMA 放入接收队列;
  3. 驱动通过 NAPI 等机制批量取包;
  4. 内核把数据表示为 sk_buff,通常简称 skb
  5. 链路层检查帧并交给 IPv4、IPv6、ARP 等协议处理;
  6. IP 层根据协议号交给 TCP、UDP 或其他传输层;
  7. 传输层根据地址和端口查找目标 Socket;
  8. 数据进入 Socket 接收队列,等待进程读取。

发送方向大致相反:

  1. 应用把数据写入 Socket;
  2. TCP 或 UDP 加上传输层头部;
  3. IP 层根据路由选择源地址和下一跳;
  4. 邻居子系统把下一跳 IP 映射为 MAC 地址;
  5. 链路层构造帧;
  6. qdisc(排队规则)调度发送;
  7. 驱动和网卡通过发送队列发出。

这是内核常见实现路径。具体驱动、硬件卸载、虚拟设备和内核版本会改变细节,但不会改变“应用通过 Socket、内核协议栈和网络设备通信”的基本边界。

查看接口统计信息:

ip -s link show dev enp1s0

其中 RXTX 的丢包、错误、队列溢出等计数,能帮助区分“应用没有监听”和“网卡或链路已经丢包”。但统计计数的语义依赖驱动,有些硬件错误只在 ethtool -S 中可见:

sudo ethtool -S enp1s0

该命令的字段不是跨网卡统一标准,不能机械地把所有计数都当成同一种故障。


三、协议栈:从二层帧到传输层数据

3.1 二层、网络层和传输层解决不同问题

以太网帧主要负责同一链路上的传输,使用 MAC 地址。IP 负责跨网络转发,使用 IP 地址。TCP 和 UDP 则负责主机上的进程间通信。

一个简化的封装关系是:

以太网帧
└── IP 数据报
    └── TCP 报文段 / UDP 数据报
        └── 应用数据

每一层都可能添加自己的头部和校验信息。接收时按相反顺序解析。

IP 地址不能直接标识进程。 一台服务器可能只有一个 IP,却同时运行 HTTP、SSH、数据库等多个服务。传输层端口正是把 IP 主机上的数据进一步交给具体 Socket 的机制。

3.2 路由决定“从哪张网卡出去”

查看路由:

ip route

常见输出:

default via 192.0.2.1 dev enp1s0
192.0.2.0/24 dev enp1s0 proto kernel scope link src 192.0.2.10

对目标地址执行一次路由查询:

ip route get 198.51.100.20

可能输出:

198.51.100.20 via 192.0.2.1 dev enp1s0 src 192.0.2.10

它说明:

  • 目标是 198.51.100.20
  • 下一跳是 192.0.2.1
  • 出口设备是 enp1s0
  • 默认选择的源地址是 192.0.2.10

这一步只说明内核如何选择路径,不说明下一跳一定在线,也不说明远端端口有服务。

如果目标与本机在同一子网,内核通常直接把目标 IP 当作邻居地址;如果目标在其他网络,则把默认网关或更具体路由的下一跳作为邻居地址。查看邻居缓存:

ip neigh

IPv4 使用 ARP,IPv6 使用 NDP。邻居解析失败时,路由可能是正确的,但实际仍无法发出有效的链路层帧。

3.3 回环设备和网络命名空间

访问 127.0.0.1::1 时,数据通常经 lo 回环,不经过物理网卡,也不需要交换机或网关。这解释了一个常见现象:

curl http://127.0.0.1:8080

能成功,并不能证明其他机器可以访问该服务。

Linux 网络命名空间为进程提供独立的网络设备、地址、路由表、邻居表和部分 Socket 视图。容器中的 127.0.0.1 指向容器自己的网络命名空间,而不是宿主机。宿主机上监听 127.0.0.1:8080,通常不会自动让容器通过宿主机回环地址访问到它。


四、Socket:应用看到的内核通信对象

4.1 Socket 是什么

Socket 是应用程序通过系统调用使用的通信端点。典型生命周期如下:

socket()
  ↓
bind()(可选,客户端通常由内核自动完成)
  ↓
listen()(TCP 服务器)
  ↓
accept()(为一个已建立连接创建新 Socket)
  ↓
send()/recv() 或 write()/read()
  ↓
shutdown()/close()

客户端常见生命周期是:

socket()
  ↓
bind()(可选)
  ↓
connect()
  ↓
send()/recv()
  ↓
close()

socket() 返回一个文件描述符(file descriptor,FD)。FD 是进程打开文件表中的整数索引,但它指向的对象不一定是磁盘文件,也可以是 Socket、管道、终端等。

例如:

int fd = socket(AF_INET, SOCK_STREAM, 0);

这行代码创建 IPv4、面向连接的字节流 Socket。它还没有端口,也没有连接到远端。

4.2 TCP 监听 Socket 与连接 Socket 不是同一个对象

服务器执行:

bind(listen_fd, ...);
listen(listen_fd, backlog);
accept(listen_fd, ...);

之后:

  • listen_fd 是监听 Socket,负责接收新的连接请求;
  • 每次 accept() 返回一个新的连接 Socket;
  • 连接 Socket 才对应一个具体客户端连接和数据流;
  • 监听 Socket 通常不承载某个客户端的业务数据。

因此,一个服务进程可以只监听一个端口,却同时拥有成千上万个已连接 Socket。ss 中常见的两类记录正是这一点的体现:

ss -ltnp
ss -tnp

示例:

LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("server",pid=1234,fd=3))
ESTAB  0 0   192.0.2.10:8080 192.0.2.20:53144 users:(("server",pid=1234,fd=7))

第一行是监听 Socket,第二行是某个已建立连接的 Socket。-p 显示进程信息通常需要权限;普通用户可能只能看到属于自己的进程,或者看不到进程名。

4.3 字节流没有消息边界

TCP 提供的是可靠、有序的字节流,不是消息队列。假设发送方连续执行:

send("ABC")
send("DEF")

接收方可能一次读到:

"ABCDEF"

也可能读到:

"AB"
"CD"
"EF"

甚至第一次读到 "ABCDE",第二次读到 "F"。TCP 只保证字节顺序和可靠交付,不保证发送调用与接收调用一一对应。应用协议必须自行设计长度前缀、分隔符或固定长度结构。

UDP 则以数据报为单位,通常保留一次发送对应一次接收的边界,但它不提供 TCP 那样的连接可靠性、顺序保证和重传机制。UDP Socket 可以调用 connect(),但这里通常只是设置默认对端并筛选接收来源,不等于完成 TCP 三次握手。


五、端口:传输层如何找到进程

5.1 端口是地址的一部分,但不是完整地址

对 TCP,常用的连接标识是四元组:

(源 IP,源端口,目的 IP,目的端口)(\text{源 IP}, \text{源端口}, \text{目的 IP}, \text{目的端口})

例如:

(192.0.2.20, 53144, 192.0.2.10, 8080)

这表示客户端临时端口 53144 访问服务器端口 8080

仅写 192.0.2.10:8080 通常表示一个监听端点,而不是一条完整连接。两条连接可以共享服务端端口,只要四元组不同:

(192.0.2.20, 53144, 192.0.2.10, 8080)
(192.0.2.21, 53144, 192.0.2.10, 8080)

客户端端口相同并不冲突,因为客户端 IP 不同。即使客户端 IP 也相同,只要目的地址或目的端口不同,通常仍可形成不同四元组;但本地端口分配和绑定规则还受 Socket 选项、地址族和内核策略约束。

5.2 监听地址决定可达范围

服务端可以绑定:

127.0.0.1:8080
192.0.2.10:8080
0.0.0.0:8080
[::]:8080

含义不同:

  • 127.0.0.1:只接受本机回环连接;
  • 192.0.2.10:通常只接受发往该本地地址的连接;
  • 0.0.0.0:IPv4 通配地址,通常表示所有本地 IPv4 地址;
  • [::]:IPv6 通配地址,具体是否同时接收 IPv4 映射连接受 IPV6_V6ONLY 和发行版配置影响,不能假设所有系统行为相同。

检查监听地址:

ss -lntup

例如:

LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22    0.0.0.0:*

前者只能从本机访问,后者可能从所有配置了 IPv4 地址的接口访问,但最终仍会受到路由、防火墙、网络命名空间和云安全组等限制。

5.3 端口范围和临时端口

服务器通常显式绑定固定端口;客户端如果不调用 bind(),内核会在连接或首次发送时选择临时源端口。查看本机临时端口范围:

cat /proc/sys/net/ipv4/ip_local_port_range

输出格式通常是两个整数,例如:

32768 60999

这表示常见实现使用的临时端口区间,但实际范围可能被系统配置修改。大量短连接时,临时端口可能耗尽。此时新连接可能失败,即使服务端仍然健康。

端口号本身没有“服务类型”的强制含义。80 常用于 HTTP、443 常用于 HTTPS,是约定而非内核保证。Linux 内核只根据 Socket 的地址族、协议、地址和端口进行分发。


六、TCP 连接状态:一个明确的状态机

6.1 建立连接:三次握手

TCP 连接建立时,客户端和服务端通过序列号确认双方都能发送和接收:

sequenceDiagram
    participant C as 客户端
    participant S as 服务端

    C->>S: SYN, seq=x
    Note right of C: SYN-SENT
    S->>C: SYN+ACK, seq=y, ack=x+1
    Note right of S: SYN-RECEIVED
    C->>S: ACK, ack=y+1
    Note over C,S: 双方进入 ESTABLISHED

步骤含义:

  1. 客户端发送 SYN,声明初始序列号 x,进入 SYN-SENT
  2. 服务端收到后回复 SYN+ACK,声明自己的初始序列号 y,确认客户端的 SYN,进入 SYN-RECEIVED
  3. 客户端发送 ACK,确认 y,双方进入 ESTABLISHED

这里的 ack=x+1 不是因为 SYN 携带了一个应用字节,而是 TCP 规定 SYN 消耗一个序列号。

如果服务端没有监听该端口,常见结果是返回 TCP RST,客户端得到 Connection refused。如果中间防火墙静默丢弃 SYN,客户端通常重传后超时。两者在诊断上有明显区别。

6.2 终止连接:四次挥手和半关闭

TCP 两个方向独立关闭。某一方调用 close()shutdown(SHUT_WR) 后,会发送 FIN

sequenceDiagram
    participant A
    participant B

    A->>B: FIN
    Note right of A: FIN-WAIT-1
    B->>A: ACK
    Note right of A: FIN-WAIT-2
    B->>A: FIN
    Note left of B: LAST-ACK
    A->>B: ACK
    Note right of A: TIME-WAIT

发送 FIN 表示“我不会再发送数据”,不等于立即断开整个双向通信。另一方向仍可能继续发送数据,这叫 TCP 半关闭。

常见状态包括:

  • LISTEN:监听新连接;
  • SYN-SENT:主动连接方已发送 SYN;
  • SYN-RECV:被动连接方收到 SYN 并发送 SYN+ACK,等待最终 ACK;
  • ESTABLISHED:连接双方可以传输数据;
  • FIN-WAIT-1:本端发送 FIN,等待确认;
  • FIN-WAIT-2:本端 FIN 已确认,等待对端 FIN;
  • CLOSE-WAIT:收到对端 FIN,本端应用尚未关闭;
  • LAST-ACK:本端已发送 FIN,等待最终 ACK;
  • CLOSING:双方几乎同时关闭;
  • TIME-WAIT:主动关闭方等待足够时间,避免旧报文干扰新连接;
  • CLOSED:连接不存在。

CLOSE-WAIT 通常不是内核“卡住”,而是应用已经收到对端关闭通知,却没有及时调用 close()。大量 CLOSE-WAIT 应优先检查应用连接生命周期和异常路径。

TIME-WAIT 则通常说明本机主动关闭了连接。它用于:

  1. 确保旧连接中延迟到达的报文不会污染后续同四元组连接;
  2. 允许本端重传最后的 ACK。

因此,TIME-WAIT 本身不是故障。短连接、高并发客户端或代理服务器可能产生很多 TIME-WAIT。只有当它与临时端口耗尽、内存压力或连接建立失败同时出现时,才应继续分析。

6.3 状态与队列

使用:

ss -tan

示例:

State      Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN     0      128    0.0.0.0:8080      0.0.0.0:*
ESTAB      0      0      192.0.2.10:8080  192.0.2.20:53144
CLOSE-WAIT 1      0      192.0.2.10:8080  192.0.2.20:53145
TIME-WAIT  0      0      192.0.2.10:8080  192.0.2.21:53146

对 TCP:

  • Recv-Q 在已建立连接中通常表示已到达内核、但尚未被应用读取的数据;
  • Send-Q 通常表示尚未被对端确认或尚未发送完成的数据;
  • 对监听 Socket,队列字段的具体含义与内核版本、Socket 队列阶段有关,不能简单等同于“当前连接数”。

监听 Socket 实际涉及至少两个重要阶段:

  1. 半连接队列:收到 SYN、发送 SYN+ACK 后,等待最终 ACK;
  2. 已完成连接队列:握手完成,等待应用调用 accept()

应用处理慢、accept() 不及时或队列过小,可能导致已完成连接排队甚至丢弃。这里的 listen(backlog) 是请求值,内核可能根据系统上限调整实际队列长度,不应把它当作无限容量。


七、一个可运行的端到端实验

下面使用 Python 标准库,避免依赖特定发行版是否安装 nc。准备两个终端。

7.1 服务端

保存为 server.py

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("127.0.0.1", 18080))
    s.listen(16)
    print("listening on 127.0.0.1:18080")

    conn, addr = s.accept()
    with conn:
        print("accepted:", addr)
        data = conn.recv(1024)
        print("received:", data)
        conn.sendall(b"reply:" + data)

运行:

python3 server.py

每一步的作用是:

  • socket() 创建 IPv4 TCP Socket;
  • SO_REUSEADDR 允许某些关闭后的地址更快重新绑定,但它不等于允许多个普通 Socket 同时监听同一地址;
  • bind() 把本地地址和端口绑定到 Socket;
  • listen(16) 把 Socket 转为监听状态;
  • accept() 阻塞等待一个已完成连接;
  • recv() 从连接 Socket 的接收缓冲区读取字节;
  • sendall() 确保在未发生错误时持续发送,不能把一次 send() 当作必然发送完全部数据。

7.2 客户端

保存为 client.py

import socket

with socket.create_connection(("127.0.0.1", 18080), timeout=5) as s:
    s.sendall(b"hello")
    print(s.recv(1024))

运行:

python3 client.py

预期输出:

b'reply:hello'

客户端未显式 bind(),所以内核会选择临时源端口。服务端 accept() 返回的新 Socket,其本地端口仍是 18080,远端端口则是客户端临时端口。

在客户端运行期间观察:

ss -tanp | grep 18080

连接极短时可能看不到 ESTAB,因为客户端发送和关闭都很快。这是观察窗口问题,不代表没有建立连接。可以在服务端 accept() 前后加入暂停,或者使用抓包工具观察握手。

7.3 绑定失败的反例

如果同时启动两个服务端实例,它们都尝试绑定 127.0.0.1:18080,第二个通常会收到:

OSError: [Errno 98] Address already in use

原因是同一网络命名空间、同一地址族、同一协议下,普通监听 Socket 不能随意占用相同的本地地址和端口。SO_REUSEADDR 主要影响地址重用和部分复用场景,不应误认为它能让任意多个服务共享端口。SO_REUSEPORT 是另一种机制,涉及多个 Socket 分担连接,使用时还要考虑内核版本、程序行为和安全边界。


八、用 ssip 和抓包把抽象状态落到证据

8.1 检查“服务是否监听”

ss -lntp 'sport = :18080'

如果没有输出,可能是:

  • 进程未启动;
  • 进程绑定了其他端口;
  • 绑定了其他网络命名空间;
  • 只监听 IPv4 或只监听 IPv6;
  • 权限导致进程信息不可见,但端口记录仍可能存在。

检查进程打开的 Socket:

sudo lsof -nP -iTCP:18080 -sTCP:LISTEN

lsof 并非所有最小化系统默认安装;ss 通常来自 iproute2。

8.2 检查路由和端口连通性

ip route get 192.0.2.10
timeout 3 bash -c '</dev/tcp/192.0.2.10/8080' && echo open

第二条依赖 Bash 的 /dev/tcp 特性,不是 POSIX 标准,也不适合所有 shell。它只能说明 TCP 建连是否成功,不能验证应用协议是否正确。

对于 HTTP,应进一步使用:

curl -v --connect-timeout 3 http://192.0.2.10:8080/

curl -v 能展示 DNS、连接地址、TLS 或 HTTP 交换过程;如果端口可连但协议响应错误,问题已从 TCP 层进入应用层。

8.3 用 tcpdump 区分拒绝、丢弃和应用问题

sudo tcpdump -ni any 'tcp port 18080'

常见观察:

客户端 -> 服务端: Flags [S]
服务端 -> 客户端: Flags [S.]
客户端 -> 服务端: Flags [.]

这对应三次握手。

如果看到:

服务端 -> 客户端: Flags [R.]

通常表示连接被主动复位,常见原因是没有监听或某个组件主动拒绝。

如果只看到客户端反复发送 SYN,而看不到 SYN+ACK,可能是:

  • 服务端没有收到;
  • 中间设备丢弃;
  • 服务端防火墙丢弃;
  • 返回路径不通;
  • 抓包点不在实际流量路径上。

在多网卡、容器、虚拟交换机、NAT 环境中,-i any 有助于初步观察,但它不是所有设备上的精确线速镜像;需要确认方向、接口和抓包位置。抓包可能包含敏感数据,生产环境应限制权限、过滤表达式和保存文件访问权限。


九、连接状态之外的常见边界

9.1 TCP 状态不等于应用健康

ESTABLISHED 只表示 TCP 层认为连接处于已建立状态。它不保证:

  • 应用线程正在处理请求;
  • 请求格式正确;
  • 服务端有足够工作线程;
  • 数据库依赖可用;
  • 应用会及时响应。

例如,服务端进程可能建立了连接,但长期不读取数据,客户端仍能看到 ESTAB。此时应结合 Recv-Q、应用日志、请求延迟和抓包判断。

反过来,连接可能在网络层已断开,但应用尚未立刻感知。TCP 只有在收到 FIN、RST、超时或下一次读写触发错误时,才会把状态变化反馈给进程。

9.2 防火墙与 NAT 改变观察结果

Netfilter/nftables 可以在不同路径节点过滤或修改数据包。NAT 还会改变地址和端口:

内网客户端: 10.0.0.5:53144
经过 SNAT 后: 203.0.113.8:40001

服务端看到的是 NAT 后的源地址和源端口,而不是客户端原始四元组。连接跟踪表会维护这种转换关系,因此:

  • 客户端本地 ss 看到原始本地端口;
  • 服务端 ss 看到转换后的源端口;
  • 网关抓包的内侧和外侧可能看到不同地址;
  • 只在一侧抓包,可能误判“对端没有发送”。

防火墙规则还可能选择 DROPREJECT

  • DROP 通常不回包,客户端表现为重试或超时;
  • REJECT 通常主动返回错误,客户端更快失败。

实际行为还受规则链、状态匹配、路由和具体拒绝类型影响,不能仅凭“端口不通”断定是哪一种。

9.3 DNS 不属于 TCP 端口监听本身

用户访问域名时,通常先经过 DNS 将名称解析为 IP。可以使用:

dig +short example.com
dig example.com A
dig example.com AAAA

DNS 解析成功只证明名称系统返回了记录,不证明目标端口可达。反之,直接访问 IP 成功,也不证明域名解析、证书名称和应用路由正确。

IPv4 与 IPv6 可能得到不同结果。例如:

  • IPv6 地址存在但没有可用默认路由;
  • 服务只监听 IPv4;
  • DNS 返回 AAAA,但 IPv6 防火墙配置不完整。

因此排查时应分别测试:

curl -4 -v https://example.com/
curl -6 -v https://example.com/

十、如何从现象反推故障位置

可以按数据流顺序建立证据链,而不是只执行一条“端口检测”命令。

情况一:本机没有监听记录

ss -lntp 'sport = :8080'

无输出时,先检查进程启动日志、配置文件、网络命名空间和实际绑定端口。此时继续修改防火墙通常没有意义,因为内核没有可交付连接的监听 Socket。

情况二:本机监听,但远端立即收到拒绝

可能是:

  • 远端访问了错误 IP 或错误端口;
  • 服务只绑定 127.0.0.1
  • 目标网络命名空间不同;
  • 中间设备主动拒绝;
  • 监听进程接受连接后主动关闭。

ssip addrtcpdump 分别确认监听地址、目标路由和 TCP 包方向。

情况三:SYN 重传,始终没有响应

重点检查:

  1. 客户端路由:ip route get <目标>
  2. 服务端是否在正确接口和地址监听;
  3. 服务端入口防火墙;
  4. 云安全组或外部 ACL;
  5. 返回路径和反向路由;
  6. 抓包点是否位于实际路径上。

仅在客户端抓包无法证明服务端是否收到 SYN;最好在客户端、服务端和中间 NAT 设备的不同侧分别取证。

情况四:大量 CLOSE-WAIT

通常意味着对端已经发送 FIN,而本地应用没有关闭连接。应检查:

  • recv() 返回空字节后是否执行关闭;
  • 异常处理是否跳过清理;
  • 连接池是否把失效连接重新借出;
  • 是否存在文件描述符泄漏。

情况五:大量 TIME-WAIT

先确认本机是否是主动关闭方,再观察连接速率和临时端口使用。不要为了减少数量而盲目启用过时或风险较高的内核参数。正确方向通常是评估连接复用、HTTP keep-alive、连接池、超时策略和代理拓扑;是否调整参数必须结合内核版本和业务协议验证。


十一、必须区分的几个概念

11.1 “端口开放”有至少三种含义

  1. 有监听 Socketss 能看到 LISTEN
  2. 从某个网络位置能完成 TCP 握手:客户端看到连接成功;
  3. 应用协议可用:例如 HTTP 返回正确状态码。

三者不是同义词。监听在 127.0.0.1 的服务满足第一条,却不满足从远端访问的第二条。

11.2 “连接数”不是一个单一指标

ss -s 可以提供汇总:

ss -s

但不同状态代表不同资源和风险:

  • ESTAB 主要反映当前业务连接;
  • SYN-RECV 可能与握手压力、半连接攻击或后端处理有关;
  • TIME-WAIT 主要与主动关闭和连接复用有关;
  • CLOSE-WAIT 更常指向应用清理问题;
  • 监听队列溢出可能发生在连接尚未进入应用可见的阶段。

因此不能看到一个总数就直接判断“网络连接过多”。

11.3 Socket、端口和连接的关系

可以用下面的关系避免混淆:

一个监听 Socket
    → 可以通过 accept() 产生多个连接 Socket
一个连接 Socket
    → 对应一个传输层通信端点关系
一个服务端端口
    → 可以承载大量不同四元组的 TCP 连接
一个进程
    → 可以持有多个 Socket 和多个文件描述符

UDP 则不一定有独立的“每个对端一个连接 Socket”;一个 UDP Socket 可以收发来自多个对端的数据报。


十二、总结:从应用调用到状态证据

Linux 网络通信不是“进程直接占用一个端口发送数据”,而是一个逐层映射过程:

  1. 应用通过 Socket 系统调用请求通信;
  2. 内核 Socket 记录地址族、协议、绑定关系和缓冲区;
  3. TCP 或 UDP 使用端口把数据交给具体传输端点;
  4. IP 层通过路由决定源地址、下一跳和出口设备;
  5. 邻居解析把下一跳 IP 转换为链路层可用地址;
  6. 网卡驱动和硬件完成帧的接收与发送;
  7. TCP 状态机维护握手、确认、重传、关闭和时间等待;
  8. ipsstcpdump 分别从设备/地址/路由、Socket/状态、报文交换三个角度提供证据。

真正可靠的诊断应把这些证据连起来:先确认 Socket 是否存在,再确认监听地址和端口,然后检查路由与邻居,最后通过抓包判断报文在哪一跳消失,并结合 TCP 状态和应用行为解释结果。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。