accept4系统调用及示例

1. 函数介绍

在网络编程中,服务器程序通常需要监听某个端口,等待客户端的连接请求。当一个客户端尝试连接到服务器时,内核会将这个连接请求放入一个等待队列中。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

服务器程序需要一种方法从这个队列中取出(“接受”)一个连接请求,并为这个连接创建一个新的套接字(socket),通过这个新套接字与客户端进行数据通信。

accept 系统调用就是用来完成这个“接受连接”的任务的。它会阻塞(等待)直到队列中有新的连接请求,然后返回一个新的、已连接的套接字文件描述符。

accept4 是 accept 的一个扩展版本。它在功能上与 accept 几乎相同,但增加了一个非常实用的特性:允许你在接受连接的同时,为新创建的套接字文件描述符设置一些标志(flags)。

最常见的用途是设置 SOCK_CLOEXEC 标志,这可以自动防止新套接字在执行 exec() 系列函数时被意外地传递给新程序,从而提高了程序的安全性和健壮性。

简单来说,accept4 就是 accept 的“增强版”,它让你在接到电话(连接)的同时,可以立刻给电话线(套接字)加上一些安全或便利的设置。

2. 函数原型

1
2
3
4
5
6
#define _GNU_SOURCE // 必须定义这个宏才能使用 accept4
#include <sys/socket.h> // 包含 accept4 函数声明

// accept4 是 Linux 特有的系统调用
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

注意:accept4 是 Linux 特有的。在可移植的 POSIX 代码中,通常使用标准的 accept,然后手动调用 fcntl 来设置标志。

3. 功能

从监听套接字 sockfd 的已完成连接队列(completed connection queue)中取出第一个连接请求,为这个连接创建一个新的、已连接的套接字,并根据 flags 参数设置该新套接字的属性。

4. 参数

sockfd:

  • int 类型。

  • 一个监听套接字的文件描述符。这个套接字必须已经通过 bind() 绑定了本地地址和端口,并通过 listen() 开始监听连接请求。

addr:

  • struct sockaddr * 类型。

  • 一个指向 sockaddr 结构体(或其特定协议的变体,如 sockaddr_in for IPv4)的指针。当 accept4 成功返回时,这个结构体将被填充为连接到服务器的客户端的地址信息(IP 地址和端口号)。

  • 如果你不关心客户端的地址信息,可以传 NULL。

addrlen:

  • socklen_t * 类型。

  • 这是一个输入/输出参数。

  • 输入时:它应该指向一个 socklen_t 变量,该变量的值是 addr 指向的缓冲区的大小。

  • 输出时:accept4 成功返回后,这个 socklen_t 变量的值将被修改为实际存储在 addr 中的地址结构的大小。

  • 如果 addr 是 NULL,addrlen 也必须是 NULL。

flags:

  • int 类型。

一个位掩码,用于设置新创建的已连接套接字的属性。可以是以下值的按位或 (|) 组合:

  • SOCK_NONBLOCK: 为新套接字设置非阻塞模式。这样,后续在这个新套接字上的 I/O 操作(如 read, write)如果无法立即完成,不会阻塞,而是返回错误 EAGAIN 或 EWOULDBLOCK。

  • SOCK_CLOEXEC: 为新套接字设置执行时关闭(Close-on-Exec)标志 (FD_CLOEXEC)。这确保了当程序调用 exec() 系列函数执行新程序时,这个新套接字会被自动关闭,防止它被新程序意外继承。这是一个重要的安全和资源管理特性。

5. 返回值

  • 成功: 返回一个新的、非负的文件描述符,它代表了与客户端通信的已连接套接字。服务器应该使用这个新的文件描述符与客户端进行 read/write 等操作。

  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

accept4 可能返回的错误码与 accept 基本相同:

  • EAGAIN 或 EWOULDBLOCK: (对于非阻塞套接字) 监听队列中当前没有已完成的连接。

  • EBADF: sockfd 不是有效的文件描述符。

  • ECONNABORTED: 连接已被客户端中止。

  • EFAULT: addr 参数指向了进程无法访问的内存地址。

  • EINTR: 系统调用被信号中断。

  • EINVAL: 套接字没有处于监听状态,或者 flags 参数包含无效标志。

  • EMFILE: 进程已打开的文件描述符数量达到上限 (RLIMIT_NOFILE)。

  • ENFILE: 系统已打开的文件描述符数量达到上限。

  • ENOMEM: 内核内存不足。

  • ENOBUFS: 网络子系统内存不足。

  • ENOTSOCK: sockfd 不是一个套接字。

  • EOPNOTSUPP: 套接字类型不支持 accept 操作(例如,不是 SOCK_STREAM)。

  • EPERM: 防火墙规则禁止连接。

7. 相似函数或关联函数

  • accept: 标准的接受连接函数。功能与 accept4 相同,但不支持 flags 参数。通常在 accept 返回后,需要再调用 fcntl 来设置 O_NONBLOCK 或 FD_CLOEXEC。// 使用 accept + fcntl 的等效操作 new_fd = accept(sockfd, addr, addrlen); if (new_fd != -1) { // 设置非阻塞和 close-on-exec int flags = fcntl(new_fd, F_GETFL, 0); fcntl(new_fd, F_SETFL, flags | O_NONBLOCK); flags = fcntl(new_fd, F_GETFD, 0); fcntl(new_fd, F_SETFD, flags | FD_CLOEXEC); }

  • listen: 将套接字置于监听状态,使其能够接收连接请求。

  • bind: 将套接字与本地地址和端口绑定。

  • socket: 创建一个套接字。

  • read / write: 通过已连接的套接字与客户端通信。

  • close: 关闭套接字。

  • fcntl: 用于获取和设置文件描述符标志,包括 O_NONBLOCK 和 FD_CLOEXEC。

8. 示例代码

下面的示例演示了一个简单的 TCP 服务器,它使用 accept4 来接受客户端连接,并利用 SOCK_CLOEXEC 和 SOCK_NONBLOCK 标志。

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#define _GNU_SOURCE // 必须定义以使用 accept4
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h> // 包含 O_NONBLOCK 等

#define PORT 8080
#define BACKLOG 10 // 监听队列的最大长度

void handle_client(int client_fd, const struct sockaddr_in *client_addr) {
char buffer&#91;1024];
ssize_t bytes_read;

printf("Handling client %s:%d on fd %d\n",
inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);

// 读取客户端发送的数据
while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer&#91;bytes_read] = '\0';
printf("Received from client: %s", buffer);

// 将数据回显给客户端
if (write(client_fd, buffer, bytes_read) != bytes_read) {
perror("write");
break;
}
}

if (bytes_read == 0) {
printf("Client disconnected.\n");
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("No data available to read (non-blocking).\n");
} else {
perror("read");
}
}

close(client_fd); // 关闭与该客户端的连接
printf("Closed connection to client.\n");
}

int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);

printf("--- Simple TCP Server using accept4 ---\n");

// 1. 创建 socket
// AF_INET: IPv4
// SOCK_STREAM: TCP
// 0: 使用默认协议 (TCP)
server_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (server_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
printf("Created server socket: %d\n", server_fd);

// 2. 准备服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地接口
server_addr.sin_port = htons(PORT); // 绑定到指定端口 (网络字节序)

// 3. 绑定 socket 到地址和端口
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Bound server socket to port %d\n", PORT);

// 4. 开始监听连接
if (listen(server_fd, BACKLOG) == -1) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Listening for connections...\n");

printf("Server is running. Connect to it using e.g., 'telnet 127.0.0.1 %d' or 'nc 127.0.0.1 %d'\n", PORT, PORT);
printf("Press Ctrl+C to stop the server.\n");

// 5. 主循环:接受连接
while (1) {
// 6. 使用 accept4 接受连接
// SOCK_CLOEXEC: 自动设置 close-on-exec 标志
// SOCK_NONBLOCK: 自动设置非阻塞模式
client_fd = accept4(server_fd, (struct sockaddr *)&client_addr, &client_len, SOCK_CLOEXEC | SOCK_NONBLOCK);

if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 对于阻塞的监听套接字,这不太可能发生
// 但对于非阻塞的监听套接字,队列可能为空
printf("No pending connections (EAGAIN/EWOULDBLOCK).\n");
usleep(100000); // 等待 0.1 秒再试
continue;
} else if (errno == EINTR) {
// 被信号中断,通常继续循环
printf("accept4 interrupted by signal, continuing...\n");
continue;
} else {
perror("accept4");
// 对于其他严重错误,可以选择关闭服务器
// close(server_fd);
// exit(EXIT_FAILURE);
continue; // 或者简单地继续尝试
}
}

printf("\nAccepted new connection. Client fd: %d\n", client_fd);
printf("Client address: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

// 7. 处理客户端 (在这个简单示例中,我们直接处理)
// 注意:在实际的高性能服务器中,这里通常会 fork() 或使用线程/事件循环
handle_client(client_fd, &client_addr);
}

// 8. 关闭服务器套接字 (实际上不会执行到这里)
close(server_fd);
printf("Server socket closed.\n");
return 0;
}

9. 编译和运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 假设代码保存在 tcp_server_accept4.c 中
# 必须定义 _GNU_SOURCE
gcc -D_GNU_SOURCE -o tcp_server_accept4 tcp_server_accept4.c

# 在一个终端运行服务器
./tcp_server_accept4

# 在另一个终端使用 telnet 或 nc 连接服务器
telnet 127.0.0.1 8080
# 或者
nc 127.0.0.1 8080

# 在 telnet/nc 窗口中输入一些文字,按回车,会看到服务器回显
# 输入 Ctrl+] 然后 quit (telnet) 或 Ctrl+C (nc) 来断开连接

10. 预期输出

服务器终端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- Simple TCP Server using accept4 ---
Created server socket: 3
Bound server socket to port 8080
Listening for connections...
Server is running. Connect to it using e.g., 'telnet 127.0.0.1 8080' or 'nc 127.0.0.1 8080'
Press Ctrl+C to stop the server.

Accepted new connection. Client fd: 4
Client address: 127.0.0.1:54321
Handling client 127.0.0.1:54321 on fd 4
Received from client: Hello, Server!

Client disconnected.
Closed connection to client.

客户端终端 (telnet 或 nc):

1
2
3
4
5
6
7
8
9
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello, Server!
Hello, Server! # 服务器回显
^]
telnet> quit
Connection closed.

11. 总结

accept4 是一个在 Linux 上非常有用的系统调用,特别适合于需要高性能和安全性的网络服务器程序。

  • 核心优势:它将“接受连接”和“设置套接字属性”这两个操作原子化地结合在一起,避免了使用 accept + fcntl 时可能存在的竞态条件(即在 accept 和 fcntl 之间,新套接字可能被意外使用)。

  • SOCK_CLOEXEC:自动设置 close-on-exec 标志,防止套接字被 exec() 继承,提高安全性。

  • SOCK_NONBLOCK:自动设置非阻塞模式,使得在新套接字上的 I/O 操作不会阻塞。

  • 与 accept 的关系:accept4(sockfd, addr, addrlen, 0) 在功能上等同于 accept(sockfd, addr, addrlen)。

  • 可移植性:accept4 是 Linux 特有的。如果需要编写可移植的代码,应使用 accept 并手动调用 fcntl。

对于 Linux 系统编程新手来说,掌握 accept4 及其标志的使用,是编写健壮、高效网络服务的重要一步。

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