3. 绑定与监听:bind()、listen() 与 backlog 深度解析
Socket 创建出来只是一个「空壳子」,它还没跟任何网络地址挂钩。你得给它分配一个 IP 和端口,再告诉系统:「我准备好了,可以接客了」。这两步就是 bind() 和 listen() 干的事。
很多人觉得这俩函数就是走个过场,填个参数就完事。但我在项目里踩过不少坑——bind 失败、端口被占、listen 之后客户端连不上……说白了,都是对底层机制理解不够。
核心流程: socket() → bind() → listen() → accept()。bind 是「挂牌」,listen 是「开门」。
3.1 bind():给 Socket 一个身份
bind() 的作用很简单:把 socket 文件描述符和一个本地地址结构体绑定在一起。地址结构体里包含协议族、IP 地址、端口号。
函数原型长这样:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
三个参数:
- sockfd:你之前
socket()返回的那个 fd。 - addr:指向
struct sockaddr的指针。实际用的时候,我们一般填充struct sockaddr_in(IPv4)或struct sockaddr_in6(IPv6),然后强转。 - addrlen:地址结构体的大小。
我习惯这么写:
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地接口
server_addr.sin_port = htons(8080); // 端口号
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
exit(EXIT_FAILURE);
}
小技巧:
INADDR_ANY 表示绑定到本机所有 IP 地址。如果你只想监听某个特定网卡,可以用 inet_addr("192.168.1.100") 赋值给 sin_addr.s_addr。
3.2 bind() 常见失败原因
bind 失败,我见过最多的就这几种:
| 错误码 | 原因 | 解决办法 |
|---|---|---|
EADDRINUSE |
端口已被占用 | 换端口,或设置 SO_REUSEADDR |
EACCES |
权限不足(比如绑定 1024 以下端口) | 用 root 运行,或换高位端口 |
EINVAL |
socket 已经 bind 过了,或地址结构体有问题 | 检查是否重复 bind,检查 sin_family |
ENOTSOCK |
sockfd 不是有效的 socket 描述符 | 检查 socket() 返回值 |
我曾经踩过的坑: 开发一个 HTTP 服务,每次 Ctrl+C 重启后 bind 都报
EADDRINUSE。后来发现是 TIME_WAIT 状态搞的鬼。加上 setsockopt() 设置 SO_REUSEADDR 就解决了。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3.3 listen():告诉内核我准备好了
bind 之后,socket 还处于「静默」状态。你得调用 listen() 告诉内核:这个 socket 是用于监听的,可以接受连接请求了。
int listen(int sockfd, int backlog);
两个参数:
- sockfd:已经 bind 过的 socket 描述符。
- backlog:连接请求队列的最大长度。嗯,这个参数是重点。
调用 listen() 之后,socket 从 CLOSED 状态进入 LISTEN 状态。这时候内核会为它维护两个队列:
- 未完成连接队列(SYN_RCVD):收到 SYN 但还没完成三次握手的连接。
- 已完成连接队列(ESTABLISHED):三次握手完成,等待
accept()取走的连接。
注意: backlog 参数在 Linux 2.2 之后,指的是「已完成连接队列」的最大长度,而不是两个队列的总和。不同系统实现有差异,但 Linux 现在就是这么干的。
3.4 backlog 参数深度解析
backlog 到底设多大?很多人直接写 5、10、128,但不知道为什么。
我直接说结论:
- 太小(比如 1~5):高并发下客户端容易收到 ECONNREFUSED,因为队列满了。
- 太大(比如 65535):浪费内核内存,而且可能掩盖应用层处理能力不足的问题。
- 合理范围:一般 128~1024 之间。Linux 内核还有个上限
net.core.somaxconn,默认 128。你设得再大,内核也只认这个值。
你可以用 cat /proc/sys/net/core/somaxconn 查看当前上限。想改的话:
echo 1024 > /proc/sys/net/core/somaxconn
但这是临时修改。永久修改要写 /etc/sysctl.conf。
我个人的习惯: 开发环境设 128,生产环境根据压测结果调。一般 512 够用,除非你预期有上万并发连接同时涌入。
3.5 完整示例:bind + listen
把上面讲的串起来,写一个完整的 TCP 服务端骨架:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
struct sockaddr_in addr;
int backlog = 128;
int opt = 1;
// 1. 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 2. 设置 SO_REUSEADDR,避免 TIME_WAIT 导致 bind 失败
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定地址
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
// 4. 开始监听
if (listen(sockfd, backlog) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port 8080, backlog=%d\n", backlog);
// 5. 等待 accept(后续章节讲)
// ...
close(sockfd);
return 0;
}
3.6 核心流程图
下面这张图展示了从 socket 创建到 listen 完成的内核状态变化:
3.7 避坑指南
最后总结几个我实际项目中遇到的坑:
- bind 前忘记 setsockopt SO_REUSEADDR:服务重启时端口被 TIME_WAIT 占着,bind 直接挂。尤其是开发环境频繁重启,必踩。
- backlog 设得太大:你以为能扛住高并发,结果内核内存爆了,或者 accept 来不及处理,客户端超时重试反而加剧负载。
- 忽略 somaxconn 上限:设了 1024,内核只认 128,你还在那纳闷为什么队列老满。
- bind 到 0.0.0.0 还是 127.0.0.1:如果只希望本地访问,bind 到 127.0.0.1;否则用 INADDR_ANY。搞反了要么连不上,要么暴露到外网。
我曾经犯过的错: 写一个内网监控服务,bind 用了 INADDR_ANY,结果外网扫描器扫到端口,被人试了几次弱口令。从那以后,非必要不绑定 0.0.0.0。
bind 和 listen 看似简单,但背后涉及内核协议栈的队列管理、状态机切换、资源限制。理解透了,你写出来的服务才稳。