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 是 NULLaddrlen 也必须是 NULL
  • flags:
    • int 类型。
    • 一个位掩码,用于设置新创建的已连接套接字的属性。可以是以下值的按位或 (|) 组合:
      • SOCK_NONBLOCK: 为新套接字设置非阻塞模式。这样,后续在这个新套接字上的 I/O 操作(如 readwrite)如果无法立即完成,不会阻塞,而是返回错误 EAGAIN 或 EWOULDBLOCK
      • SOCK_CLOEXEC: 为新套接字设置执行时关闭(Close-on-Exec)标志 (FD_CLOEXEC)。这确保了当程序调用 exec() 系列函数执行新程序时,这个新套接字会被自动关闭,防止它被新程序意外继承。这是一个重要的安全和资源管理特性。

5. 返回值

  • 成功: 返回一个新的、非负的文件描述符,它代表了与客户端通信的已连接套接字。服务器应该使用这个新的文件描述符与客户端进行 read/write 等操作。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

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

  • EAGAIN 或 EWOULDBLOCK: (对于非阻塞套接字) 监听队列中当前没有已完成的连接。
  • EBADFsockfd 不是有效的文件描述符。
  • ECONNABORTED: 连接已被客户端中止。
  • EFAULTaddr 参数指向了进程无法访问的内存地址。
  • EINTR: 系统调用被信号中断。
  • EINVAL: 套接字没有处于监听状态,或者 flags 参数包含无效标志。
  • EMFILE: 进程已打开的文件描述符数量达到上限 (RLIMIT_NOFILE)。
  • ENFILE: 系统已打开的文件描述符数量达到上限。
  • ENOMEM: 内核内存不足。
  • ENOBUFS: 网络子系统内存不足。
  • ENOTSOCKsockfd 不是一个套接字。
  • 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 标志。

#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 及其标志的使用,是编写健壮、高效网络服务的重要一步。

此条目发表在未分类分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注