继续学习 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
本地地址已被使用,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;
}
代码解释:
- 创建套接字:
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
代码解释:
重要提示与注意事项:
总结: