继续学习 Linux 系统编程中的重要函数。这次我们介绍 listen 函数,它是 TCP 服务器模型中不可或缺的一环,用于将一个已绑定的套接字置于监听状态,准备接收来自客户端的连接请求。
1. 函数介绍
“嘿,内核,我这个套接字(sockfd)已经绑定了一个地址(IP 和端口),现在我想开始监听这个地址,等待客户端的连接请求。请帮我管理这些连接请求,把它们排好队,等我用 accept 来处理。”
你可以把 listen 想象成商店开门营业:
- 商店(套接字)已经选好了地址(通过 
bind)。 listen就像是老板在门口挂上“营业中”的牌子,并告诉店员(内核):“有人来敲门(连接请求),先让他们在门外等一会儿(排队),别让他们直接冲进来。”accept则像是店员去开门,把排队的顾客(客户端)迎进来,开始一对一的服务。
2. 函数原型
#include <sys/socket.h> // 必需
int listen(int sockfd, int backlog);
3. 功能
- 启用监听: 将套接字 
sockfd的状态设置为监听模式。 - 建立队列: 告诉内核为此套接字创建两个队列(具体实现可能有所不同,但概念如此):
- 未完成连接队列 (incomplete connection queue):存放那些正在执行 TCP 三次握手但尚未完成的连接请求。
 - 已完成连接队列 (completed connection queue):存放那些已经完成 TCP 三次握手、等待服务器程序通过 
accept接受的连接。 
 - 限制队列长度: 
backlog参数用于提示内核这两个队列的最大总长度。当队列满时,新的连接请求可能会被忽略或拒绝。 
4. 参数
int sockfd: 这是一个已经成功调用bind的套接字文件描述符。- 必须是面向连接的套接字,如 
SOCK_STREAM(TCP)。 - 不能是无连接的套接字,如 
SOCK_DGRAM(UDP)。对 UDP 套接字调用listen会失败。 
- 必须是面向连接的套接字,如 
 int backlog: 这个参数用于指定连接请求队列的最大长度。- 它告诉内核,最多允许多少个已完成(或接近完成)的连接请求在此套接字上排队等待 
accept。 - 实际队列长度: 内核可能会将这个值视为一个提示,并可能根据系统资源或配置将其调整为一个不同的、通常是不超过 
SOMAXCONN的值。SOMAXCONN是系统定义的最大队列长度(在 Linux 上通常是 128 或 4096)。 - 选择合适的值:
- 过小: 可能导致客户端连接被拒绝(
ECONNREFUSED),特别是在高并发场景下。 - 过大: 可能消耗过多内核资源。
 - 常见做法: 传统上使用 5 (
#define LISTENQ 5)。现代高性能服务器可能会设置一个更大的值,如 128 或 1024。#define LISTENQ 1024是一个常用的较大值。 - 现代建议: 可以直接使用 
SOMAXCONN常量,让系统决定最大值。 
 - 过小: 可能导致客户端连接被拒绝(
 
- 它告诉内核,最多允许多少个已完成(或接近完成)的连接请求在此套接字上排队等待 
 
5. 返回值
- 成功时: 返回 0。套接字 
sockfd现在处于监听状态。 - 失败时: 返回 -1,并设置全局变量 
errno来指示具体的错误原因(例如EADDRINUSE本地地址已被使用,EBADFsockfd无效,EINVAL套接字未绑定或不支持监听,ENOMEM内存不足等)。 
6. 相似函数,或关联函数
socket: 创建套接字。bind: 将套接字绑定到本地地址,是listen的前置步骤。accept: 从listen创建的已完成连接队列中取出一个连接,是listen的后续步骤。connect: 客户端使用此函数向监听的服务器发起连接请求。
7. 示例代码
示例 1:标准的 TCP 服务器 socket -> bind -> listen 流程
这个例子演示了设置一个 TCP 服务器的标准三步流程。
// tcp_listen_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORT 8088
// 使用 SOMAXCONN 作为 backlog,让系统选择合适的最大队列长度
#define BACKLOG SOMAXCONN
// 或者使用一个自定义值,如 #define BACKLOG 128
int main() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;
    // 1. 创建套接字 (第一步)
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Step 1: Socket created successfully (fd: %d)\n", server_fd);
    // 2. 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    // 3. 配置服务器地址结构
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    // 4. 绑定套接字到地址和端口 (第二步)
    printf("Step 2: Binding socket to port %d...\n", PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Socket bound successfully to 0.0.0.0:%d\n", PORT);
    // 5. 使套接字进入监听状态 (第三步,关键)
    printf("Step 3: Putting socket into listening mode with backlog %d...\n", BACKLOG);
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server is now LISTENING on port %d with backlog %d.\n", PORT, BACKLOG);
    printf("\nServer setup complete. Waiting for connections...\n");
    printf("Run a client to connect, e.g., 'telnet localhost %d' or 'nc localhost %d'\n", PORT, PORT);
    // --- 服务器已准备好,可以调用 accept() 来接受连接 ---
    // 按 Ctrl+C 退出程序
    pause(); // 永久挂起,直到收到信号
    close(server_fd);
    printf("Server socket closed.\n");
    return 0;
}
代码解释:
- 创建套接字: 
socket(AF_INET, SOCK_STREAM, 0)创建一个 IPv4 TCP 套接字。 - 设置选项: 
setsockopt(... SO_REUSEADDR ...)设置地址重用选项。 - 绑定地址: 
bind(...)将套接字绑定到所有接口 (INADDR_ANY) 的PORT端口。 - **监听连接 **(关键步骤) 调用 
listen(server_fd, BACKLOG)。server_fd: 要监听的套接字。BACKLOG: 连接队列的最大长度。这里使用SOMAXCONN,让系统决定。
 - 调用成功后,服务器套接字进入监听状态。内核开始为该套接字维护连接请求队列。
 - 程序挂起,等待客户端连接。实际的连接处理需要在后续调用 
accept()。 
示例 2:演示 listen 失败的情况
这个例子演示了在错误的情况下调用 listen 会发生什么。
// listen_failures.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    int sock;
    // --- 情况 1: 对未绑定的套接字调用 listen ---
    printf("--- Test 1: listen() on an unbound socket ---\n");
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    if (listen(sock, 5) < 0) {
        perror("listen on unbound socket failed (expected)");
        // 这通常会失败,errno 为 EINVAL
    } else {
        printf("listen on unbound socket unexpectedly succeeded.\n");
    }
    close(sock);
    // --- 情况 2: 对 UDP 套接字调用 listen ---
    printf("\n--- Test 2: listen() on a UDP socket ---\n");
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("UDP socket failed");
        exit(EXIT_FAILURE);
    }
    if (listen(sock, 5) < 0) {
        perror("listen on UDP socket failed (expected)");
        // 这会失败,errno 通常为 EOPNOTSUPP (Operation not supported)
    } else {
        printf("listen on UDP socket unexpectedly succeeded.\n");
    }
    close(sock);
    // --- 情况 3: 对已关闭的套接字调用 listen ---
    printf("\n--- Test 3: listen() on a closed socket ---\n");
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    close(sock); // 先关闭
    if (listen(sock, 5) < 0) {
        perror("listen on closed socket failed (expected)");
        // 这会失败,errno 通常为 EBADF (Bad file descriptor)
    } else {
        printf("listen on closed socket unexpectedly succeeded.\n");
    }
    printf("\nAll failure tests completed.\n");
    return 0;
}
代码解释:
示例 3:listen 与 accept 的结合使用
这个例子将 listen 和 accept 结合起来,展示一个完整的、但简化的服务器循环。
// listen_accept_demo.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h> // inet_ntoa
#define PORT 8089
#define BACKLOG 10
void handle_client(int client_fd, struct sockaddr_in *client_addr) {
    printf("Handling client %s:%d (fd: %d)\n",
           inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);
    // 在实际应用中,这里会进行数据读写
    // 为了演示,我们立即关闭连接
    close(client_fd);
    printf("Closed connection to client %s:%d\n",
           inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port));
}
int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    int opt = 1;
    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    // 3. 配置并绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    // 4. 关键:监听连接
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d (backlog: %d)\n", PORT, BACKLOG);
    printf("Accepting connections for 10 seconds...\n");
    // 5. 循环接受连接 (只接受几个演示)
    time_t start_time = time(NULL);
    int connections_handled = 0;
    while (difftime(time(NULL), start_time) < 10.0) {
        // accept 是阻塞调用,会等待直到有连接或出错
        client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
        if (client_fd < 0) {
            perror("accept failed");
            continue;
        }
        connections_handled++;
        printf("New connection #%d accepted.\n", connections_handled);
        handle_client(client_fd, &client_address);
        // 简单限制演示连接数
        if (connections_handled >= 3) {
            break;
        }
    }
    printf("Handled %d connections in 10 seconds. Shutting down.\n", connections_handled);
    close(server_fd);
    return 0;
}
如何测试:
- 编译并运行服务器:
gcc -o listen_accept_demo listen_accept_demo.c ./listen_accept_demo - 在另一个或多个终端中,快速运行客户端命令:
telnet localhost 8089 # 或者 nc localhost 8089 
代码解释:
重要提示与注意事项:
总结: