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 完成的内核状态变化:

bind() + listen() 内核状态流转 socket() 状态: CLOSED bind() 绑定 IP + 端口 listen() 状态: LISTEN 未完成连接队列 (SYN_RCVD) 收到 SYN,三次握手进行中 长度: /proc/sys/net/ipv4/tcp_max_syn_backlog 已完成连接队列 (ESTABLISHED) 三次握手完成,等待 accept() 长度: min(backlog, somaxconn) 客户端 connect() 服务端 accept()

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 看似简单,但背后涉及内核协议栈的队列管理、状态机切换、资源限制。理解透了,你写出来的服务才稳。