继续学习 Linux 系统编程中的重要函数。这次我们介绍 listen 函数,它是 TCP 服务器模型中不可或缺的一环,用于将一个已绑定的套接字置于监听状态,准备接收来自客户端的连接请求。
1. 函数介绍 listen 是一个 Linux 系统调用,专门用于 TCP 服务器。它的核心作用是将一个已经绑定到本地地址(通过 bind)的套接字的状态从默认的主动打开(active open)转变为被动打开(passive open)。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
简单来说,listen 告诉操作系统内核:
“嘿,内核,我这个套接字(sockfd)已经绑定了一个地址(IP 和端口),现在我想开始监听这个地址,等待客户端的连接请求。请帮我管理这些连接请求,把它们排好队,等我用 accept 来处理。”
你可以把 listen 想象成商店开门营业:
2. 函数原型 1 2 3 4 #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 的套接字文件描述符。
int backlog: 这个参数用于指定连接请求队列的最大长度。
选择合适的值:
过小: 可能导致客户端连接被拒绝(ECONNREFUSED),特别是在高并发场景下。
过大: 可能消耗过多内核资源。
常见做法: 传统上使用 5 (#define LISTENQ 5)。现代高性能服务器可能会设置一个更大的值,如 128 或 1024。#define LISTENQ 1024 是一个常用的较大值。
现代建议: 可以直接使用 SOMAXCONN 常量,让系统决定最大值。
5. 返回值
6. 相似函数,或关联函数
socket: 创建套接字。
bind: 将套接字绑定到本地地址,是 listen 的前置步骤。
accept: 从 listen 创建的已完成连接队列中取出一个连接,是 listen 的后续步骤。
connect: 客户端使用此函数向监听的服务器发起连接请求。
7. 示例代码 示例 1:标准的 TCP 服务器 socket -> bind -> listen 流程 这个例子演示了设置一个 TCP 服务器的标准三步流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 // 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)。
调用成功后,服务器套接字进入监听状态。内核开始为该套接字维护连接请求队列。
程序挂起,等待客户端连接。实际的连接处理需要在后续调用 accept()。
示例 2:演示 listen 失败的情况 这个例子演示了在错误的情况下调用 listen 会发生什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // 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: 创建一个 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 结合起来,展示一个完整的、但简化的服务器循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 // 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
代码解释:
执行标准的 socket -> setsockopt -> bind -> listen 流程。2. 进入一个循环,持续调用 accept(server_fd, …)。3. accept 是一个阻塞调用。如果没有待处理的连接,程序会在此处挂起等待。4. 当有客户端连接请求到达并完成三次握手后,accept 会从已完成连接队列中取出该连接,返回一个新的文件描述符 client_fd,专门用于与该客户端通信。5. 调用 handle_client 函数(这里只是简单地打印信息并关闭连接)。6. 主循环继续调用 accept,处理下一个连接。
重要提示与注意事项:
顺序至关重要: 必须严格按照 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 的桥梁。