好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 accept4
函数,它是 accept
函数的扩展版本,用于在网络编程中接受传入的连接请求。
1. 函数介绍 链接到标题
accept4
是一个 Linux 系统调用,是标准 accept
函数的扩展版本。它用于接受一个传入的连接请求,并创建一个新的套接字来处理该连接。
在网络服务器编程中,服务器套接字(listening socket)监听特定的端口等待客户端连接。当客户端发起连接请求时,服务器调用 accept4
来接受这个连接,并获得一个新的已连接套接字(connected socket),专门用于与该客户端进行数据通信。
accept4
相比 accept
的主要优势是可以在接受连接的同时直接设置新套接字的标志,最常用的是 SOCK_CLOEXEC
和 SOCK_NONBLOCK
标志,这避免了接受连接后再调用 fcntl
来设置这些属性的额外系统调用。
2. 函数原型 链接到标题
#define _GNU_SOURCE
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
3. 功能 链接到标题
- 接受连接: 从监听套接字
sockfd
的完成连接队列中取出第一个连接请求。 - 创建新套接字: 为这个连接创建一个新的套接字文件描述符。
- 返回客户端信息: 可选地返回连接客户端的地址信息。
- 设置套接字标志: 在创建新套接字时直接应用指定的标志。
4. 参数 链接到标题
int sockfd
: 监听套接字的文件描述符。这个套接字必须已经通过bind()
绑定了本地地址,并通过listen()
开始监听连接。struct sockaddr *addr
: 指向sockaddr
结构的指针,用于存储连接客户端的地址信息。- 如果不需要客户端地址信息,可以传入
NULL
。 - 如果传入非
NULL
指针,addrlen
参数必须指向一个有效的socklen_t
变量。
- 如果不需要客户端地址信息,可以传入
socklen_t *addrlen
: 指向socklen_t
变量的指针,该变量:- 输入时: 指定
addr
指向的缓冲区大小。 - 输出时: 返回实际存储在
addr
中的地址结构大小。 - 如果
addr
是NULL
,此参数也必须是NULL
。
- 输入时: 指定
int flags
: 控制标志,可以是以下值的按位或组合:SOCK_CLOEXEC
: 为新套接字设置执行时关闭标志(FD_CLOEXEC),使得在执行exec
系列函数时自动关闭该套接字。SOCK_NONBLOCK
: 为新套接字设置非阻塞 I/O 模式。0
: 不设置任何特殊标志(等同于标准accept
的行为)。
5. 返回值 链接到标题
- 成功时: 返回一个新的已连接套接字的文件描述符(非负整数)。
- 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因:EAGAIN
或EWOULDBLOCK
: 套接字被标记为非阻塞,但没有连接请求等待。EBADF
:sockfd
不是有效的文件描述符。ECONNABORTED
: 连接已被客户端中止。EFAULT
:addr
参数指向进程地址空间之外。EINTR
: 系统调用被信号中断。EINVAL
: 套接字没有监听,或flags
参数无效。EMFILE
: 进程打开的文件描述符数量达到上限 (RLIMIT_NOFILE
)。ENFILE
: 系统范围内打开的文件数量达到上限。ENOMEM
: 内存不足。ENOTSOCK
:sockfd
不是一个套接字文件描述符。EOPNOTSUPP
: 引用的套接字不是支持accept
操作的类型(如 TCP 套接字)。EPERM
: 防火墙规则禁止连接。
6. 相似函数,或关联函数 链接到标题
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
: 标准的接受连接函数,不支持直接设置标志。socket(int domain, int type, int protocol)
: 创建套接字。bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
: 绑定套接字到地址。listen(int sockfd, int backlog)
: 将套接字置于监听状态。connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
: 客户端发起连接。fcntl(int fd, int cmd, ...)
: 可用于设置套接字标志,但不是原子操作。
7. 示例代码 链接到标题
示例 1:基本的 TCP 服务器使用 accept4 链接到标题
#define _GNU_SOURCE
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define PORT 8080
#define BACKLOG 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
printf("=== Accept4 基本 TCP 服务器演示 ===\n");
printf("服务器启动在端口 %d\n", PORT);
// 1. 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("创建套接字失败");
exit(EXIT_FAILURE);
}
printf("1. 创建服务器套接字: %d\n", server_fd);
// 2. 设置套接字选项(允许地址重用)
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("设置套接字选项失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("2. 设置套接字选项 SO_REUSEADDR\n");
// 3. 绑定套接字到地址
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);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("绑定套接字失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("3. 绑定套接字到地址: %s:%d\n",
inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
// 4. 开始监听
if (listen(server_fd, BACKLOG) == -1) {
perror("监听失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("4. 开始监听,最大等待队列: %d\n", BACKLOG);
printf("\n等待客户端连接...\n");
// 5. 使用 accept4 接受连接(设置 CLOEXEC 和 NONBLOCK 标志)
client_fd = accept4(server_fd, (struct sockaddr *)&client_addr,
&client_addr_len, SOCK_CLOEXEC | SOCK_NONBLOCK);
if (client_fd == -1) {
perror("accept4 失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("5. 客户端连接已接受\n");
printf(" 客户端套接字: %d\n", client_fd);
printf(" 客户端地址: %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 6. 验证标志是否设置
int flags = fcntl(client_fd, F_GETFD);
if (flags != -1 && (flags & FD_CLOEXEC)) {
printf(" FD_CLOEXEC 标志已设置\n");
}
flags = fcntl(client_fd, F_GETFL);
if (flags != -1 && (flags & O_NONBLOCK)) {
printf(" O_NONBLOCK 标志已设置\n");
}
// 7. 尝试与客户端通信
const char *welcome_msg = "Hello from server (using accept4)!\n";
ssize_t bytes_sent = send(client_fd, welcome_msg, strlen(welcome_msg), 0);
if (bytes_sent == -1) {
perror("发送欢迎消息失败");
} else {
printf("6. 发送欢迎消息 (%ld 字节)\n", bytes_sent);
}
// 8. 尝试接收客户端消息(非阻塞模式)
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("7. 非阻塞接收: 暂时没有数据可读\n");
} else {
perror("接收数据失败");
}
} else if (bytes_received == 0) {
printf("7. 客户端已关闭连接\n");
} else {
buffer[bytes_received] = '\0';
printf("7. 接收到来自客户端的数据: %s", buffer);
}
// 9. 清理资源
printf("\n8. 关闭连接\n");
close(client_fd);
close(server_fd);
return 0;
}
示例 2:accept4 与 accept 的对比 链接到标题
#define _GNU_SOURCE
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
void demonstrate_accept_vs_accept4() {
printf("=== Accept vs Accept4 对比演示 ===\n");
// 创建监听套接字(简化代码,实际需要完整的服务器设置)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
printf("创建套接字失败\n");
return;
}
// 设置地址重用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定和监听
struct sockaddr_in server_addr;
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(8081);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1 ||
listen(server_fd, 5) == -1) {
printf("服务器设置失败\n");
close(server_fd);
return;
}
printf("服务器监听在端口 8081\n");
// 方式1: 使用 accept + fcntl
printf("\n方式1: 使用 accept + fcntl 设置标志\n");
int client_fd1 = accept(server_fd, NULL, NULL);
if (client_fd1 != -1) {
printf(" accept 返回套接字: %d\n", client_fd1);
// 需要两次系统调用来设置标志
if (fcntl(client_fd1, F_SETFD, FD_CLOEXEC) == -1) {
perror(" 设置 FD_CLOEXEC 失败");
} else {
printf(" 通过 fcntl 设置 FD_CLOEXEC\n");
}
if (fcntl(client_fd1, F_SETFL, fcntl(client_fd1, F_GETFL) | O_NONBLOCK) == -1) {
perror(" 设置 O_NONBLOCK 失败");
} else {
printf(" 通过 fcntl 设置 O_NONBLOCK\n");
}
close(client_fd1);
}
// 方式2: 使用 accept4(原子操作)
printf("\n方式2: 使用 accept4 原子设置标志\n");
int client_fd2 = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC | SOCK_NONBLOCK);
if (client_fd2 != -1) {
printf(" accept4 返回套接字: %d\n", client_fd2);
printf(" 标志已原子性设置\n");
// 验证标志
int flags = fcntl(client_fd2, F_GETFD);
if (flags != -1 && (flags & FD_CLOEXEC)) {
printf(" FD_CLOEXEC 确认已设置\n");
}
flags = fcntl(client_fd2, F_GETFL);
if (flags != -1 && (flags & O_NONBLOCK)) {
printf(" O_NONBLOCK 确认已设置\n");
}
close(client_fd2);
}
close(server_fd);
}
void demonstrate_accept4_flags() {
printf("\n=== Accept4 标志详解 ===\n");
printf("SOCK_CLOEXEC 标志:\n");
printf(" - 作用: 设置 FD_CLOEXEC 标志\n");
printf(" - 效果: 执行 exec 时自动关闭套接字\n");
printf(" - 用途: 防止文件描述符泄漏到新程序\n\n");
printf("SOCK_NONBLOCK 标志:\n");
printf(" - 作用: 设置 O_NONBLOCK 标志\n");
printf(" - 效果: 套接字 I/O 操作变为非阻塞\n");
printf(" - 用途: 避免 I/O 操作阻塞进程\n\n");
printf("标志组合:\n");
printf(" - SOCK_CLOEXEC | SOCK_NONBLOCK: 同时设置两个标志\n");
printf(" - 0: 不设置任何特殊标志(等同于 accept)\n");
}
int main() {
demonstrate_accept4_flags();
demonstrate_accept_vs_accept4();
printf("\n=== Accept4 优势总结 ===\n");
printf("1. 原子性操作: 避免了 accept + fcntl 的竞态条件\n");
printf("2. 性能更好: 减少系统调用次数\n");
printf("3. 安全性更高: 防止文件描述符泄漏\n");
printf("4. 便利性更强: 一次性完成连接接受和标志设置\n\n");
printf("使用建议:\n");
printf("- 优先使用 accept4 而不是 accept\n");
printf("- 在服务器程序中使用 SOCK_CLOEXEC\n");
printf("- 在需要异步 I/O 的场景中使用 SOCK_NONBLOCK\n");
printf("- 注意检查返回值和错误处理\n");
return 0;
}
示例 3:错误处理和高级用法 链接到标题
#define _GNU_SOURCE
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
void demonstrate_accept4_errors() {
printf("=== Accept4 错误处理演示 ===\n");
// 1. 无效的套接字描述符
printf("1. 使用无效的套接字描述符:\n");
int result = accept4(999, NULL, NULL, 0);
if (result == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EBADF) {
printf(" 说明: 文件描述符 999 无效\n");
}
}
// 2. 无效的标志
printf("\n2. 使用无效的标志:\n");
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd != -1) {
result = accept4(server_fd, NULL, NULL, 0x1000); // 无效标志
if (result == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EINVAL) {
printf(" 说明: 标志参数无效\n");
}
}
close(server_fd);
}
// 3. 非监听套接字
printf("\n3. 在非监听套接字上使用 accept4:\n");
int non_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (non_listen_fd != -1) {
result = accept4(non_listen_fd, NULL, NULL, 0);
if (result == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EINVAL) {
printf(" 说明: 套接字未处于监听状态\n");
}
}
close(non_listen_fd);
}
}
// 信号处理函数(用于演示 EINTR 错误)
void signal_handler(int sig) {
printf("接收到信号 %d\n", sig);
}
void demonstrate_signal_interruption() {
printf("\n=== 信号中断演示 ===\n");
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) return;
// 设置地址重用和绑定
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
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(8082);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
printf("服务器监听在端口 8082\n");
printf("在另一终端运行: kill -USR1 %d\n", getpid());
// 设置信号处理
signal(SIGUSR1, signal_handler);
// 这会阻塞等待连接,可以用信号中断
printf("等待连接(可能被信号中断)...\n");
int client_fd = accept4(server_fd, NULL, NULL, 0);
if (client_fd == -1) {
if (errno == EINTR) {
printf("accept4 被信号中断\n");
} else {
perror("accept4 失败");
}
} else {
close(client_fd);
}
close(server_fd);
}
void demonstrate_nonblocking_usage() {
printf("\n=== 非阻塞模式使用演示 ===\n");
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) return;
// 设置服务器
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
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(8083);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
printf("非阻塞服务器监听在端口 8083\n");
// 使用 accept4 创建非阻塞套接字
int client_fd = accept4(server_fd, NULL, NULL, SOCK_NONBLOCK);
if (client_fd != -1) {
printf("创建了非阻塞的客户端套接字: %d\n", client_fd);
// 立即尝试接收数据(会返回 EAGAIN)
char buffer[1024];
ssize_t bytes = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞接收: 暂无数据 (EAGAIN)\n");
}
}
close(client_fd);
}
close(server_fd);
}
int main() {
printf("Accept4 函数错误处理和高级用法演示\n\n");
demonstrate_accept4_errors();
// demonstrate_signal_interruption(); // 需要交互式测试
demonstrate_nonblocking_usage();
printf("\n=== Accept4 与其他函数的关系 ===\n");
printf("accept4(sockfd, addr, addrlen, 0) 等价于 accept(sockfd, addr, addrlen)\n");
printf("accept4 比 accept 多了原子性设置标志的功能\n");
printf("在支持的系统上应优先使用 accept4\n");
return 0;
}
编译和运行说明 链接到标题
# 编译示例
gcc -o accept4_basic accept4_basic.c
gcc -o accept4_compare accept4_compare.c
gcc -o accept4_errors accept4_errors.c
# 运行示例
./accept4_basic
./accept4_compare
./accept4_errors
# 测试服务器(需要客户端连接)
# 在一个终端运行服务器
./accept4_basic
# 在另一个终端使用 telnet 或 nc 连接
telnet localhost 8080
# 或者
nc localhost 8080
重要注意事项 链接到标题
- Linux 特定:
accept4
是 Linux 特定的系统调用,在其他 Unix 系统上可能不可用 - 原子操作:
accept4
是原子操作,避免了accept
+fcntl
的竞态条件 - 性能优势: 减少系统调用次数,提高性能
- 安全性:
SOCK_CLOEXEC
标志防止文件描述符泄漏 - 兼容性: 如果需要跨平台兼容性,应该使用
accept
+fcntl
- 错误处理: 注意处理各种可能的错误情况,特别是
EAGAIN
、EINTR
等