listen系统调用及示例

继续学习 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 想象成商店开门营业:

  • 商店(套接字)已经选好了地址(通过 bind)。

  • listen 就像是老板在门口挂上“营业中”的牌子,并告诉店员(内核):“有人来敲门(连接请求),先让他们在门外等一会儿(排队),别让他们直接冲进来。”

  • accept 则像是店员去开门,把排队的顾客(客户端)迎进来,开始一对一的服务。

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 的套接字文件描述符。

  • 必须是面向连接的套接字,如 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 服务器的标准三步流程。

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)。

  • server_fd: 要监听的套接字。

  • BACKLOG: 连接队列的最大长度。这里使用 SOMAXCONN,让系统决定。

调用成功后,服务器套接字进入监听状态。内核开始为该套接字维护连接请求队列。

程序挂起,等待客户端连接。实际的连接处理需要在后续调用 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. 测试 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

代码解释:

  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 的桥梁。

data-ad-format="auto" data-full-width-responsive="true">