Socket 与端口基础:编程模型、端口号与协议
聊端口复用之前,得先把地基打牢。Socket 是什么?端口号怎么分配?TCP 和 UDP 在端口层面到底差在哪?这些搞不清楚,后面讲端口复用你肯定听得云里雾里。
我刚开始学网络编程那会儿,总觉得 Socket 是个很玄乎的东西。后来调了几年线上问题,才真正理解——说白了,Socket 就是操作系统给应用层开的一扇门。
Socket 编程模型:应用与内核之间的门
Socket 本身是一个抽象层。它把传输层(TCP/UDP)的细节藏了起来,给应用层暴露一个文件描述符。你往这个描述符里写数据,内核帮你封装成报文发出去;内核收到报文,你也从这个描述符里读。
嗯,这里要注意:Socket 不是协议,是接口。
一个典型的 TCP Socket 生命周期是这样的:
- 创建:调用
socket(),指定协议族、类型、协议号。 - 绑定:调用
bind(),把 Socket 和本地 IP + 端口绑在一起。 - 监听(服务端):调用
listen(),告诉内核这个 Socket 是拿来接客的。 - 连接(客户端):调用
connect(),发起三次握手。 - 收发:
send()/recv(),或者read()/write()。 - 关闭:
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,绑定端口,内核根据协议类型把端口注册到对应的端口表里。数据包到达时,内核先看协议号(TCP 还是 UDP),再去对应的表里查端口,最后把数据交给对应的 Socket。
注意:同一个端口号,TCP 和 UDP 可以各绑各的,互不冲突。但如果你试图在同一个协议内(比如 TCP)把同一个端口绑给两个不同的 Socket,默认情况下会报错。这就是端口复用要解决的问题。
好了,Socket 和端口的基础就这些。记住三个核心点:Socket 是门,端口是门牌号,TCP/UDP 是两套独立的门牌系统。后面讲端口复用的时候,你会反复用到这些概念。