Socket 与端口基础:编程模型、端口号与协议

聊端口复用之前,得先把地基打牢。Socket 是什么?端口号怎么分配?TCP 和 UDP 在端口层面到底差在哪?这些搞不清楚,后面讲端口复用你肯定听得云里雾里。

我刚开始学网络编程那会儿,总觉得 Socket 是个很玄乎的东西。后来调了几年线上问题,才真正理解——说白了,Socket 就是操作系统给应用层开的一扇门。

Socket 编程模型:应用与内核之间的门

Socket 本身是一个抽象层。它把传输层(TCP/UDP)的细节藏了起来,给应用层暴露一个文件描述符。你往这个描述符里写数据,内核帮你封装成报文发出去;内核收到报文,你也从这个描述符里读。

嗯,这里要注意:Socket 不是协议,是接口。

一个典型的 TCP Socket 生命周期是这样的:

  1. 创建:调用 socket(),指定协议族、类型、协议号。
  2. 绑定:调用 bind(),把 Socket 和本地 IP + 端口绑在一起。
  3. 监听(服务端):调用 listen(),告诉内核这个 Socket 是拿来接客的。
  4. 连接(客户端):调用 connect(),发起三次握手。
  5. 收发:send() / recv(),或者 read() / write()。
  6. 关闭:close(),释放资源。

UDP 更简单,没有 listen 和 connect 那套,直接 sendto() / recvfrom() 就完事了。

核心概念:每个 Socket 在内核里对应一个 struct sock,里面存着四元组(源 IP、源端口、目标 IP、目标端口)。端口复用,本质上就是让多个 struct sock 共享同一个端口号。

代码示例,一个最简单的 TCP 服务端骨架:

int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;

bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 128);

while (1) {
    int client = accept(sock, NULL, NULL);
    // 处理客户端请求...
    close(client);
}

这段代码我写过不下几百次。但有个坑——如果不设置 SO_REUSEADDR,服务端重启时会报 Address already in use。这个后面会细讲。

端口号与协议:16 位的世界

端口号是一个 16 位的无符号整数,范围 0~65535。内核用它来区分同一台机器上的不同网络连接。

你想想看,如果一台服务器只有一个 IP,那怎么同时跑 Web 服务、数据库、SSH?靠的就是端口号。

端口号分三段:

范围 分类 说明
0 - 1023 Well-Known Ports(系统端口) 需要 root 权限才能绑定。HTTP(80)、HTTPS(443)、SSH(22) 都在这里。
1024 - 49151 Registered Ports(注册端口) IANA 分配给特定服务的,比如 MySQL(3306)、Redis(6379)。普通用户也能绑。
49152 - 65535 Dynamic / Private Ports(动态端口) 客户端临时用,内核自动分配。你每次访问网站,本地端口基本都落在这个区间。

避坑指南:我曾经在生产环境遇到过一个问题——某个服务绑了 8080 端口,但另一个进程占用了 8080 的 UDP 端口,导致服务启动失败。后来才发现,TCP 和 UDP 的端口空间是独立的,但有些老旧的应用层框架会混着检查。

TCP 与 UDP 端口区别:不只是协议不同

很多人以为 TCP 和 UDP 的端口是一回事。其实不然。

内核里,TCP 和 UDP 各自维护一套独立的端口表。TCP 的端口绑定记录在 tcp_hashinfo 里,UDP 的在 udp_table 里。所以理论上,TCP 80 和 UDP 80 可以同时被不同的进程占用,互不干扰。

但实际中,很多服务会同时监听 TCP 和 UDP 的同一个端口。比如 DNS(53 端口),既用 TCP 做区域传输,也用 UDP 做普通查询。

来看一张对比表:

对比维度 TCP UDP
连接状态 有状态(ESTABLISHED、LISTEN 等) 无状态
端口复用场景 多进程/多线程监听同一端口(需 SO_REUSEADDR) 多播、广播场景常见
内核查找方式 四元组精确匹配 + 监听 Socket 兜底 五元组(含协议号)匹配
端口冲突表现 bind() 返回 EADDRINUSE 同样返回 EADDRINUSE,但 UDP 无 TIME_WAIT 问题
典型应用 HTTP、HTTPS、SSH、MySQL DNS、DHCP、视频流、游戏

我个人习惯把 TCP 端口理解成「电话线路」——你得先拨号、接通、才能说话,说完还得挂断。UDP 端口更像「信箱」——你往里面扔信,不管对方在不在,扔完就走。

这个区别在端口复用上影响很大。TCP 因为有 TIME_WAIT 状态,端口释放会有延迟,所以复用起来更麻烦。UDP 就没这个烦恼,端口一关,立马就能给别人用。

一张图看懂 Socket 与端口的关系

下面这张 SVG 图,把 Socket、端口、协议、内核四者的关系串起来了:

Socket 与端口关系框架图 应用层(用户态) 进程 A:Socket A(TCP 8080) 进程 B:Socket B(UDP 8080) 内核层(系统态) TCP 端口表:8080 → Socket A(四元组匹配) UDP 端口表:8080 → Socket B(五元组匹配) 端口复用标志:SO_REUSEADDR / SO_REUSEPORT 传输层协议 TCP:面向连接、可靠、有序、有状态 UDP:无连接、不可靠、无序、无状态 关键点:TCP 和 UDP 端口空间独立,同一端口号可被不同协议同时使用

从这张图能看出来:应用层创建 Socket,绑定端口,内核根据协议类型把端口注册到对应的端口表里。数据包到达时,内核先看协议号(TCP 还是 UDP),再去对应的表里查端口,最后把数据交给对应的 Socket。

注意:同一个端口号,TCP 和 UDP 可以各绑各的,互不冲突。但如果你试图在同一个协议内(比如 TCP)把同一个端口绑给两个不同的 Socket,默认情况下会报错。这就是端口复用要解决的问题。

好了,Socket 和端口的基础就这些。记住三个核心点:Socket 是门,端口是门牌号,TCP/UDP 是两套独立的门牌系统。后面讲端口复用的时候,你会反复用到这些概念。