listen系统调用及示例

继续学习 Linux 系统编程中的重要函数。这次我们介绍 listen 函数,它是 TCP 服务器模型中不可或缺的一环,用于将一个已绑定的套接字置于监听状态,准备接收来自客户端的连接请求。


1. 函数介绍

listen 是一个 Linux 系统调用,专门用于 TCP 服务器。它的核心作用是将一个已经绑定到本地地址(通过 bind)的套接字的状态从默认的主动打开(active open)转变为被动打开(passive open)。

简单来说,listen 告诉操作系统内核:

“嘿,内核,我这个套接字(sockfd)已经绑定了一个地址(IP 和端口),现在我想开始监听这个地址,等待客户端的连接请求。请帮我管理这些连接请求,把它们排好队,等我用 accept 来处理。”

你可以把 listen 想象成商店开门营业

  • 商店(套接字)已经选好了地址(通过 bind)。
  • listen 就像是老板在门口挂上“营业中”的牌子,并告诉店员(内核):“有人来敲门(连接请求),先让他们在门外等一会儿(排队),别让他们直接冲进来。”
  • accept 则像是店员去开门,把排队的顾客(客户端)迎进来,开始一对一的服务。

2. 函数原型

#include <sys/socket.h> // 必需

int listen(int sockfd, int backlog);

3. 功能

  • 启用监听: 将套接字 sockfd 的状态设置为监听模式。
  • 建立队列: 告诉内核为此套接字创建两个队列(具体实现可能有所不同,但概念如此):
    1. 未完成连接队列 (incomplete connection queue):存放那些正在执行 TCP 三次握手但尚未完成的连接请求。
    2. 已完成连接队列 (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 本地地址已被使用,EBADF sockfd 无效,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;
}

代码解释:

  1. 创建套接字socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。
  2. 设置选项setsockopt(... SO_REUSEADDR ...) 设置地址重用选项。
  3. 绑定地址bind(...) 将套接字绑定到所有接口 (INADDR_ANY) 的 PORT 端口。
  4. **监听连接 **(关键步骤) 调用 listen(server_fd, BACKLOG)
    • server_fd: 要监听的套接字。
    • BACKLOG: 连接队列的最大长度。这里使用 SOMAXCONN,让系统决定。
  5. 调用成功后,服务器套接字进入监听状态。内核开始为该套接字维护连接请求队列。
  6. 程序挂起,等待客户端连接。实际的连接处理需要在后续调用 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;
}

代码解释:

1. 测试 1: 创建一个 TCP 套接字后不调用 bind,直接调用 listen。这会失败,通常 errno 为 EINVAL(Invalid argument)。
2. 测试 2: 创建一个 UDP (SOCK_DGRAM) 套接字,然后调用 listen。这会失败,通常 errno 为 EOPNOTSUPP(Operation not supported)。
3. 测试 3: 创建一个套接字,调用 close 关闭它,然后再调用 listen。这会失败,通常 errno 为 EBADF(Bad file descriptor)。

示例 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;
}

如何测试:

  1. 编译并运行服务器:gcc -o listen_accept_demo listen_accept_demo.c ./listen_accept_demo
  2. 在另一个或多个终端中,快速运行客户端命令:telnet localhost 8089 # 或者 nc localhost 8089

代码解释:

1. 执行标准的 socket -> setsockopt -> bind -> listen 流程。
2. 进入一个循环,持续调用 accept(server_fd, ...)
3. accept 是一个阻塞调用。如果没有待处理的连接,程序会在此处挂起等待。
4. 当有客户端连接请求到达并完成三次握手后,accept 会从已完成连接队列中取出该连接,返回一个新的文件描述符 client_fd,专门用于与该客户端通信。
5. 调用 handle_client 函数(这里只是简单地打印信息并关闭连接)。
6. 主循环继续调用 accept,处理下一个连接。


重要提示与注意事项:

1. 顺序至关重要: 必须严格按照 socket() -> bind() -> listen() -> accept() 的顺序进行。
2. 仅用于面向连接的套接字listen 只能用于 SOCK_STREAM (TCP) 类型的套接字。对 SOCK_DGRAM (UDP) 调用会失败。
3. backlog 的含义: 理解 backlog 是队列长度的提示,而不是严格保证。内核可能会调整它。对于高并发服务器,设置一个较大的 backlog 是明智的。
4. accept 是关键listen 只是设置了监听状态和队列,真正接受连接的操作是由 accept 完成的。
5. 错误处理: 始终检查 listen 的返回值。最常见的错误是 EINVAL(套接字未绑定)和 EOPNOTSUPP(套接字类型不支持)。
6. 队列溢出: 如果连接请求的速度超过了服务器 accept 的速度,且队列已满,新的连接请求可能会被内核丢弃,客户端会收到连接被拒(ECONNREFUSED)的错误。

总结:

listen 是 TCP 服务器编程模型的核心组件之一。它将一个绑定好的套接字转变为可以接收连接请求的状态,并由内核管理一个连接队列。理解其作用和参数(特别是 backlog)对于构建能够处理并发连接请求的服务器至关重要。它是连接 bind 和 accept 的桥梁。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注