好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 accept4 函数,它是 accept 函数的扩展版本,用于在网络编程中接受传入的连接请求。


1. 函数介绍 見出しへのリンク

accept4 是一个 Linux 系统调用,是标准 accept 函数的扩展版本。它用于接受一个传入的连接请求,并创建一个新的套接字来处理该连接。

在网络服务器编程中,服务器套接字(listening socket)监听特定的端口等待客户端连接。当客户端发起连接请求时,服务器调用 accept4 来接受这个连接,并获得一个新的已连接套接字(connected socket),专门用于与该客户端进行数据通信。

accept4 相比 accept 的主要优势是可以在接受连接的同时直接设置新套接字的标志,最常用的是 SOCK_CLOEXECSOCK_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 中的地址结构大小。
    • 如果 addrNULL,此参数也必须是 NULL
  • int flags: 控制标志,可以是以下值的按位或组合:
    • SOCK_CLOEXEC: 为新套接字设置执行时关闭标志(FD_CLOEXEC),使得在执行 exec 系列函数时自动关闭该套接字。
    • SOCK_NONBLOCK: 为新套接字设置非阻塞 I/O 模式。
    • 0: 不设置任何特殊标志(等同于标准 accept 的行为)。

5. 返回值 見出しへのリンク

  • 成功时: 返回一个新的已连接套接字的文件描述符(非负整数)。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
    • EAGAINEWOULDBLOCK: 套接字被标记为非阻塞,但没有连接请求等待。
    • 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

重要注意事项 見出しへのリンク

  1. Linux 特定: accept4 是 Linux 特定的系统调用,在其他 Unix 系统上可能不可用
  2. 原子操作: accept4 是原子操作,避免了 accept + fcntl 的竞态条件
  3. 性能优势: 减少系统调用次数,提高性能
  4. 安全性: SOCK_CLOEXEC 标志防止文件描述符泄漏
  5. 兼容性: 如果需要跨平台兼容性,应该使用 accept + fcntl
  6. 错误处理: 注意处理各种可能的错误情况,特别是 EAGAINEINTR