accept4系统调用及示例
1. 函数介绍
在网络编程中,服务器程序通常需要监听某个端口,等待客户端的连接请求。当一个客户端尝试连接到服务器时,内核会将这个连接请求放入一个等待队列中。
服务器程序需要一种方法从这个队列中取出(“接受”)一个连接请求,并为这个连接创建一个新的套接字(socket),通过这个新套接字与客户端进行数据通信。
accept
系统调用就是用来完成这个“接受连接”的任务的。它会阻塞(等待)直到队列中有新的连接请求,然后返回一个新的、已连接的套接字文件描述符。
accept4
是 accept
的一个扩展版本。它在功能上与 accept
几乎相同,但增加了一个非常实用的特性:允许你在接受连接的同时,为新创建的套接字文件描述符设置一些标志(flags)。
最常见的用途是设置 SOCK_CLOEXEC
标志,这可以自动防止新套接字在执行 exec()
系列函数时被意外地传递给新程序,从而提高了程序的安全性和健壮性。
简单来说,accept4
就是 accept
的“增强版”,它让你在接到电话(连接)的同时,可以立刻给电话线(套接字)加上一些安全或便利的设置。
2. 函数原型
#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-execint 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
标志。
#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[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[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. 编译和运行
# 假设代码保存在 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. 预期输出
服务器终端:
--- 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
):
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
及其标志的使用,是编写健壮、高效网络服务的重要一步。