5、监听连接:listen()函数详解、backlog参数含义、被动套接字与主动套接字

好,咱们继续往下走。上一节我们把 socket 创建好了,bind 也绑上地址了。但这时候的套接字,说白了还是个「哑巴」——它不会主动等人来找它。今天要聊的 listen(),就是让这个套接字「支棱起来」的关键一步。

5.1 主动套接字 vs 被动套接字

先搞清楚一个基本概念。你想想看,我们平时写网络程序,套接字其实分两种性格:

  • 主动套接字:它自己主动去连接别人。比如客户端程序里,我们调用 connect() 去连服务器。这种套接字天生就是「进攻型」的。
  • 被动套接字:它不主动出击,而是「守株待兔」,等着别人来连它。服务器端的套接字,在调用 listen() 之后,就变成了被动套接字。

我记得刚入行那会儿,有个同事把客户端和服务器的套接字搞混了,客户端调了 listen(),服务器调了 connect(),结果折腾了一下午没连上。嗯,这种低级错误其实挺常见的。

简单来说:socket() 创建出来的套接字默认是主动的。只有调了 listen(),它才变成被动套接字,开始监听端口。

5.2 listen() 函数原型

直接看代码,不废话:

#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数就两个:

  • sockfd:就是之前创建并 bind 好的那个套接字描述符。
  • backlog:这个参数有点意思,咱们重点聊。

返回值:成功返回 0,失败返回 -1 并设置 errno。

5.3 backlog 参数到底是什么意思?

很多初学者以为 backlog 是「最大连接数」,其实不是。它表示的是「已完成连接队列」+「未完成连接队列」的总和上限。说白了,就是内核为这个监听套接字维护的两个队列的大小。

我画个图帮你理解一下:

listen() 内核队列模型 客户端连接请求 未完成连接队列 SYN_RCVD 状态 已完成连接队列 ESTABLISHED 状态 accept() backlog = 两个队列的总和上限 三次握手流程: 客户端 SYN → 服务器 SYN+ACK → 客户端 ACK 握手完成前 → 未完成队列;握手完成后 → 已完成队列 ⚠️ 如果已完成队列满了,新的连接会被直接丢弃 ⚠️ 如果未完成队列满了,SYN 包会被忽略

看到这张图了吗?客户端发来连接请求,先进入「未完成连接队列」(三次握手还没完),等三次握手完成了,再挪到「已完成连接队列」。你的程序调 accept() 就是从已完成队列里取一个连接出来。

那 backlog 到底设多少?

  • 在 Linux 2.2 之前,backlog 指的是两个队列的总和。
  • 在 Linux 2.2 之后,backlog 只影响「已完成连接队列」的大小,未完成队列的大小由内核参数 net.ipv4.tcp_max_syn_backlog 控制。

我个人习惯,一般服务器设 128 或 256。高并发场景可以设到 1024 甚至更大。但别设太大,否则内存占用会上去。

重要:backlog 不是最大连接数!最大连接数由 accept() 决定,backlog 只是排队等待 accept() 处理的连接数上限。

5.4 实际代码示例

来,看一段完整的服务器初始化代码:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>

int main() {
    int listen_fd;
    struct sockaddr_in server_addr;

    // 1. 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        return -1;
    }

    // 2. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8080);

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(listen_fd);
        return -1;
    }

    // 3. 监听 —— 关键一步
    if (listen(listen_fd, 128) == -1) {
        perror("listen");
        close(listen_fd);
        return -1;
    }

    printf("服务器已启动,监听端口 8080...\n");

    // 4. 接受连接(后续章节详解)
    // accept(listen_fd, ...);

    return 0;
}

这段代码里,listen(listen_fd, 128) 就是让套接字进入监听模式。128 是 backlog 的值。我个人习惯用 128,大部分场景够用了。

5.5 常见问题与避坑指南

我曾经踩过一个坑:有一次写一个高并发服务器,backlog 设了 5,结果压测时大量连接失败。查了半天才发现是 backlog 太小,客户端连接请求被内核直接丢弃了。后来改成 1024,问题解决。

几个要点你记住:

  • listen() 必须在 bind() 之后调用,顺序不能乱。
  • listen() 不会阻塞,它只是告诉内核:「嘿,我开始监听了,有连接进来先排队。」
  • backlog 的值受内核参数限制。Linux 下,/proc/sys/net/core/somaxconn 定义了 backlog 的上限,默认一般是 128 或 4096。你设得再大,内核也不买账。
  • 如果 backlog 设成 0,理论上还能监听,但连接可能被拒绝。不建议这么干。

小技巧:可以用 ss -lnt 命令查看当前系统的监听套接字及其 backlog 值。比如 ss -lnt | grep 8080,能看到 Send-Q 那一列就是 backlog。

5.6 总结一下

listen() 这个函数,说白了就是让套接字「转正」——从主动变成被动,从默默无闻到开始监听端口。backlog 参数控制的是排队等待 accept 的连接数量,不是最大连接数。

嗯,到这里你应该对 listen() 有了比较清晰的认识。下一节我们讲 accept(),真正把连接「接」进来。


公众号:蓝海资料掘金营,微信deep3321