Appearance
第9章 源端口和目标端口
IP 地址负责将数据报送到主机,而端口则接力将 TCP 报文段(Segment)准确送达主机内的通信端点。在我们抓取的样本 TCP 首部中,前四个字节就已经标明了报文的传输方向:
text
c0 00 23 28
└───┘ └───┘
源端口 目标端口本章我们就从这四个字节入手,看看操作系统是如何在单个“监听 Socket”和众多“已连接 Socket”之间,精准完成报文匹配与分发的。
两个 16 位整数
源端口(Source Port)和目标端口(Destination Port)各占 16 位,取值范围是 0~65535。在网络传输中,它们采用网络字节序(大端序),以我们的抓包样本为例:
结合 IPv4 首部中的源地址和目标地址,我们就能完整写出这条报文的传输方向:
text
192.0.2.10:49152 -> 198.51.100.20:9000当服务端返回响应时,源和目标的角色就会互换:
text
198.51.100.20:9000 -> 192.0.2.10:49152这一来一回的报文共同属于同一条 TCP 连接。在 Wireshark 等分析工具中,它们会被自动归入同一个 tcp.stream(TCP 流)进行追踪。
为什么连接需要四元组
假设服务端正在 198.51.100.20:9000 这个端点上,同时为两个客户端提供服务:
| 连接 | 客户端端点 | 服务端端点 | 四元组 |
|---|---|---|---|
| A | 192.0.2.10:49152 | 198.51.100.20:9000 | 192.0.2.10, 49152, 198.51.100.20, 9000 |
| B | 192.0.2.11:53000 | 198.51.100.20:9000 | 192.0.2.11, 53000, 198.51.100.20, 9000 |
尽管这两条连接共享了服务端的 9000 端口,但由于客户端的 IP 和端口不同,它们的“四元组”依然是唯一的。操作系统正是依靠这种唯一性,为这两条连接分别维护独立的状态,包括各自的序列号(Sequence Number)、确认号(Acknowledgment Number)、接收窗口(Window)、重传计时器以及缓冲区。
在工程实践中,操作系统的查找上下文还会包含 IP 版本、传输层协议甚至网络命名空间(Network Namespace)。这样一来,IPv4/TCP、IPv6/TCP 以及彼此隔离的容器网络,都能互不干扰地维护各自的端点集合。对开发者而言,“四元组”依然是我们阅读和排查单条 TCP 连接时最核心的标识,而那些系统上下文信息,只是决定了去哪一张具体的“连接表”里进行查找。
如果同一个客户端进程再次向该服务端发起连接,操作系统通常会为其分配一个新的临时端口:
text
127.0.0.1:49152 -> 127.0.0.1:9000
127.0.0.1:49153 -> 127.0.0.1:9000这两条连接可能出自同一个客户端进程,也可能属于不同的进程。我们需要明确一个概念:端口属于传输层的命名空间,而进程、线程与 Socket 之间的对应关系,则完全由操作系统和应用程序自身的架构来管理。一个进程可以同时持有多个 Socket;在开启端口复用(Port Reuse)机制后,多个工作进程甚至也能共同监听同一个端口来承接流量。
监听端点与已连接端点
服务端在处理连接时,通常会分为两个层次:
- 监听 Socket:负责绑定本地地址和端口(例如
0.0.0.0:9000),被动等待客户端到来。 - 已连接 Socket:三次握手完成后,内核会为每条连接建立包含远端端点信息的“已连接状态”。应用程序通过调用
accept(),就能拿到这个专属的 Socket,用来与特定客户端收发数据。
这里的通配地址 0.0.0.0(在 IPv6 中对应 ::)表示监听本机所有网卡上的 IPv4 流量。如果服务端只绑定了 127.0.0.1:9000,那就只有本地的回环流量(Loopback)才能匹配到这个监听端点;而外部发往该机器局域网 IP 的流量,则会被直接拒绝或走其他的地址匹配逻辑。
当一个 TCP 报文段(Segment)到达网卡时,我们可以用下面这个简单的概念模型,来理解内核是如何进行匹配查找的:
text
报文到达
↓
读取协议、目标地址、目标端口、源地址、源端口
↓
按完整连接标识查找已连接状态
↓
若报文用于建立新连接,再按本地地址与端口查找监听端点
↓
进入该连接或监听路径的队列与状态机在底层实现中,为了提高查找效率,内核通常会引入哈希表、网络命名空间、网卡接口约束以及端口复用规则等复杂机制。但上面的心智模型依然抓住了排查网络问题时最重要的两点:已建立的连接严格依赖完整的四元组,而监听入口则主要依靠本地的 IP 和端口进行匹配。
在日常排查中我们经常会看到:运行 Get-NetTCPConnection 或 ss 命令时,输出结果中会有一行 LISTEN 状态和多行 ESTABLISHED 状态的记录。它们都共享了本地的 9000 端口,但由于远端端点不同,它们各自维持着独立的状态。
端口号的常用分区
根据 IANA(互联网名称与数字地址分配机构)的规范,16 位的端口空间通常被划分为以下三个区间:
| 范围 | 常见名称 | 阅读抓包时的用途 |
|---|---|---|
| 0~1023 | System / Well-Known Ports | 常见基础服务注册范围 |
| 1024~49151 | User / Registered Ports | 注册应用服务范围 |
| 49152~65535 | Dynamic / Private Ports | 常见客户端动态选择范围 |
在我们前面的抓包样本中,客户端使用的 49152 属于动态端口(Dynamic Ports),而服务端的 9000 则属于注册端口(Registered Ports)。 值得注意的是,操作系统分配临时端口的具体范围是可以配置的,Windows、Linux 和 macOS 各有不同的默认策略。因此在看抓包时,如果我们看到一个高位端口(比如 50000+),第一直觉可以假设它是客户端分配的临时端口,随后再通过观察 SYN 握手方向、本地 Socket 状态和应用日志来交叉验证。
此外,端口 0 在网络编程中有一个特殊用途:当应用程序在绑定时指定端口为 0,其实是在告诉操作系统“请帮我随机挑选一个可用的端口”。等连接真正建立后,程序就可以通过 getsockname() 系统调用或者查看网络连接表,来获知最终被分配的实际端口号。
两个方向如何共同标识一条流
在客户端发出的请求报文中:
text
src = 192.0.2.10:49152
dst = 198.51.100.20:9000而在服务端返回的响应报文中:
text
src = 198.51.100.20:9000
dst = 192.0.2.10:49152当我们站在连接的视角来分析问题时,通常会把这个端点对看作一个无方向的集合;但当我们具体到单条报文时,源(Source)和目标(Destination)则永远携带着明确的传输方向。Wireshark 的显示过滤器(Display Filters)也精准地体现了这两种视角的差别:
text
tcp.port == 9000这个条件会匹配源端口或目标端口为 9000 的所有报文,也就是能同时抓出双向的数据;
text
tcp.dstport == 9000这就加上了方向限制,只匹配当前目标端口为 9000 的报文;
text
ip.src == 192.0.2.10 && tcp.srcport == 49152 &&
ip.dst == 198.51.100.20 && tcp.dstport == 9000这是一种极为严苛的单向过滤,仅匹配从客户端发往服务端的报文。 在实际排查中,一旦我们通过上述条件锁定了一条连接,最推荐的做法是右键追踪流(Follow TCP Stream)或者直接使用 tcp.stream == N 过滤器(N 为 Wireshark 自动分配的流序号)。这样既能剔除其他干扰,又能完整地观察双向的通信交互。
NAT 会改写抓包中的四元组
当客户端身处私有网络(局域网)时,NAT(网络地址转换)设备会在报文出站时,将其内部端点替换为公网端点。例如:
text
NAT 内侧抓包:192.168.1.20:49152 -> 198.51.100.20:9000
NAT 外侧抓包:203.0.113.9:62001 -> 198.51.100.20:9000同时,NAT 设备内部会维护一张映射表。当服务端发来响应报文时:
text
198.51.100.20:9000 -> 203.0.113.9:62001NAT 收到报文后,会查表将其重新送回内网的 192.168.1.20:49152。 这就导致了一个常见现象:服务端访问日志里记录的客户端端点是 203.0.113.9:62001,而客户端本机 Socket 显示的却是 192.168.1.20:49152。此时,两侧的抓包文件都没有骗人,它们只是忠实地记录了报文在各自抓包点“当下”的面貌。
正因如此,在进行网络故障分析时,首要步骤就是明确“当前报文是在哪个节点抓取的”。如果遇到 IP 乃至端口对不上的情况,请务必去查阅 NAT 地址转换表、负载均衡器(Load Balancer)日志或者代理服务器日志。本书在第 36 章还会把 NAT、防火墙与负载均衡器串联起来,带你建立更复杂的多段连接分析模型。
实验:观察两个客户端共享服务端端口
准备与步骤
- 打开我们在第二篇编写的客户端与服务端代码,将两端的
PORT端口号从50007同步修改为9000。启动服务端程序,让其监听127.0.0.1:9000,并在accept接受连接后保持一段时间。 - 打开两个 PowerShell 窗口,分别运行客户端程序,向服务端发起连接。
- 在第三个 PowerShell 窗口中运行以下命令,查看当前的网络连接状态:
powershell
Get-NetTCPConnection -State Established |
Where-Object { $_.LocalPort -eq 9000 -or $_.RemotePort -eq 9000 } |
Sort-Object LocalAddress, LocalPort, RemoteAddress, RemotePort |
Format-Table LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess- 切到 Wireshark,在显示过滤器中输入
tcp.port == 9000,找到这两条连接,并分别记录下它们的tcp.stream编号。 - 依次使用
tcp.stream == N过滤这两条流,观察它们各自的三次握手、数据传输与四次挥手过程。 - 最终让客户端正常退出,对比系统连接表的销毁情况,以及抓包中 FIN 报文的关闭方向是否一致。
预期现象
- 在服务端的视角中,这两条处于
ESTABLISHED状态的连接,其本地端口都是 9000。 - 两个客户端则被操作系统分配了截然不同的本地临时端口。
- Wireshark 准确地识别出了这是两条独立的连接,并为它们分配了不同的 Stream 编号。
- 每个 Stream 都独立维护着自己的序列号(Seq)、确认号(Ack)以及完整的生命周期状态机。
- 服务端的响应报文中,源端口和目标端口发生了互换。
提示:如果程序执行太快导致来不及查看连接状态,你可以在客户端发送数据后加个键盘输入等待(如
Console.ReadLine()),或者让服务端在受控实验中暂停数秒。在排查问题时,抓包记录和通过命令查询系统状态的操作,最好放在同一个时间窗口内进行比对。
理解检查
- 现有两条连接,分别是
10.0.0.8:51000 -> 10.0.0.2:443和10.0.0.8:51001 -> 10.0.0.2:443。操作系统是依据哪个字段来区分它们的? - 当收到一条
10.0.0.2:443 -> 10.0.0.8:51000的报文时,它属于上述哪条连接?是请求方向还是响应方向? - 服务端的访问日志中,记录的客户端 IP 是
203.0.113.9:62001;而客户端的本地日志中,却显示自己绑定的端点是192.168.1.20:49152。网络链路上存在哪类设备,会导致这种现象发生? - Wireshark 显示过滤器
tcp.port == 9000和tcp.dstport == 9000在报文方向的筛选上有什么区别?
参考答案
- 它们依赖客户端的源端口(51000 和 51001)来区分。由于源端口不同,这两条记录构成了两个完全独立的“四元组”。
- 它属于第一条连接(51000),且这是服务端向客户端发送的响应报文。
- 链路上存在具备 SNAT(源地址与端口转换)能力的 NAT 设备。部署在 NAT 设备内外两侧的探针或抓包点,会看到完全不同的 IP 和端口对。
tcp.port只要源端口或目标端口任意一方满足即可,所以它能抓出双向往返的报文;而tcp.dstport只匹配目标端口,因此通常只覆盖单向发送的报文。
本章小结
- 源端口和目标端口各占 16 位,在传输时均使用网络字节序(大端序)。
- IP 地址负责将报文送达目标主机,而端口则负责将报文精准分发给特定的 TCP 通信端点(进程或线程)。
- 依靠“四元组”的唯一性,同一个服务端监听端口能够同时承载成千上万条独立的 TCP 连接。
- 监听 Socket 只绑定本地端点被动接收连接;而已连接 Socket 除了本地端点,还绑定了明确的远端端点信息。
- NAT 设备、代理服务器以及负载均衡器等中间件,会在转发报文时改写四元组,因此抓包位置决定了你看到的 IP 和端口样貌。
- 在 Wireshark 中,
tcp.stream过滤器非常适合用来追踪整条 TCP 连接的双向交互,而类似tcp.dstport这样的定向过滤器则更适合用来剖析单一方向的数据流。
上一章:第8章 TCP 首部总览 · 所属篇:第三篇 · 下一章:第10章 Sequence Number · 教程总览