accept系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 accept 函数,它是 TCP 服务器用来接受客户端连接请求的核心系统调用。


1. 函数介绍

accept 是一个 Linux 系统调用,专门用于TCP 服务器(使用 SOCK_STREAM 套接字)。它的主要功能是从监听套接字(通过 listen 设置的套接字)的未决连接队列(pending connection queue)中取出第一个连接请求,并为这个新连接创建一个全新的、独立的套接字文件描述符

你可以把 accept 想象成总机接线员

  1. 有很多电话(客户端连接请求)打进来,响铃并排队在总机(监听套接字)那里。
  2. 接线员(accept 调用)拿起一个响铃的电话。
  3. 接线员把这条线路接到一个新的、专用的电话线(新的套接字文件描述符)上。
  4. 接线员可以继续去接下一个电话(下一次 accept 调用),而第一个通话(与第一个客户端的通信)则通过那条专用线路进行,互不干扰。

这个新创建的套接字文件描述符专门用于与那一个特定的客户端进行双向数据通信。原始的监听套接字则继续保持监听状态,等待并接受更多的连接请求。


2. 函数原型

#include <sys/socket.h> // 必需

// 标准形式
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 带有标志的变体 (Linux 2.6.28+)
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

3. 功能

  • 从队列中取出连接: 从监听套接字 sockfd 维护的未决连接队列中提取第一个已完成或正在完成的连接请求。
  • 创建新套接字: 为这个新连接创建一个新的、非监听状态的套接字文件描述符。
  • 返回通信端点: 返回这个新的套接字文件描述符,服务器程序可以使用它来与特定的客户端进行数据交换(read/write)。
  • 获取客户端信息: 如果 addr 和 addrlen 参数不为 NULL,则将连接到服务器的客户端的地址信息(IP 地址和端口号)填充到 addr 指向的缓冲区中。

4. 参数

  • int sockfd: 这是监听套接字的文件描述符。它必须是:
    1. 通过 socket() 成功创建的。
    2. 通过 bind() 绑定了本地地址(IP 和端口)的。
    3. 通过 listen() 进入监听状态的。
  • struct sockaddr *addr: 这是一个指向套接字地址结构的指针,用于接收客户端的地址信息。
    • 如果你不关心客户端是谁,可以传入 NULL
    • 如果传入非 NULL 值,则它通常指向一个 struct sockaddr_in (IPv4) 或 struct sockaddr_in6 (IPv6) 类型的变量。
    • 该结构体在 accept 返回后会被填入客户端的地址信息。
  • socklen_t *addrlen: 这是一个指向 socklen_t 类型变量的指针。
    • 输入: 在调用 accept 时,这个变量必须被初始化为 addr 指向的缓冲区的大小(以字节为单位)。例如,如果 addr 指向 struct sockaddr_in,则 *addrlen 应初始化为 sizeof(struct sockaddr_in)
    • 输出accept 返回时,这个变量会被更新为实际存储在 addr 中的地址结构的大小。这对于处理不同大小的地址结构(如 IPv4 和 IPv6)很有用。
  • int flags (accept4 特有): 这个参数允许在创建新套接字时设置一些属性,类似于 socket() 的 type 参数可以使用的修饰符。
    • SOCK_NONBLOCK: 将新创建的套接字设置为非阻塞模式。
    • SOCK_CLOEXEC: 在调用 exec() 时自动关闭该套接字。

5. 返回值

  • 成功时: 返回一个新的、非负的整数,即为新连接创建的套接字文件描述符。服务器应使用这个返回的文件描述符与客户端进行后续的数据通信。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EAGAIN 或 EWOULDBLOCK 套接字被标记为非阻塞且没有未决连接,EBADF sockfd 无效,EINVAL 套接字未监听,EMFILE 进程打开的文件描述符已达上限等)。

阻塞与非阻塞:

  • 阻塞套接字(默认):如果监听队列中没有待处理的连接,accept 调用会阻塞(挂起)当前进程,直到有新的连接到达。
  • 非阻塞套接字(如果监听套接字被设置为非阻塞):如果监听队列中没有待处理的连接,accept 会立即返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK

6. 相似函数,或关联函数

  • socket: 用于创建原始的监听套接字。
  • bind: 将监听套接字绑定到本地地址。
  • listen: 使套接字进入监听状态,开始接收连接请求。
  • connect: 客户端使用此函数向服务器发起连接。
  • close: 服务器在与客户端通信结束后,需要关闭 accept 返回的那个套接字文件描述符。通常也需要关闭原始的监听套接字(在服务器退出时)。
  • fork / 多线程: 服务器通常在 accept 之后调用 fork 或创建新线程来处理与客户端的通信,以便主服务器进程可以继续调用 accept 接受新的连接。

7. 示例代码

示例 1:基本的 TCP 服务器 accept 循环

这个例子演示了一个典型的、顺序处理的 TCP 服务器如何使用 accept 循环来接受和处理客户端连接。

// sequential_tcp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h> // inet_ntoa (注意:不是线程安全的)

#define PORT 8080
#define BACKLOG 10

void handle_client(int client_fd, struct sockaddr_in *client_addr) {
    char buffer[1024];
    ssize_t bytes_read;

    printf("Handling client %s:%d (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); // buffer 可能已包含 \n

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

    if (bytes_read < 0) {
        perror("read from client failed");
    } else {
        printf("Client %s:%d disconnected (fd: %d)\n",
               inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);
    }

    close(client_fd); // 关闭与该客户端的连接
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    int opt = 1;

    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 配置服务器地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 4. 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 5. 监听连接
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // 6. 主循环:接受并处理连接
    while (1) {
        printf("Waiting for a connection...\n");

        // 7. 接受连接 (阻塞调用)
        client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
        if (client_fd < 0) {
            perror("accept failed");
            continue; // 或 exit(EXIT_FAILURE);
        }

        printf("New connection accepted.\n");

        // 8. 处理客户端 (顺序处理,同一时间只能处理一个)
        handle_client(client_fd, &client_address);

        // 处理完一个客户端后,循环继续 accept 下一个
    }

    // 注意:在实际程序中,需要有退出机制和清理代码
    // close(server_fd); // 不会执行到这里
    return 0;
}

代码解释:

  1. 创建、绑定、监听服务器套接字,这部分与之前 socketbindlisten 的例子相同。
  2. 进入一个无限的 while(1) 循环。
  3. 在循环内部,调用 accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len)
    • server_fd: 监听套接字。
    • &client_address: 指向 sockaddr_in 结构的指针,用于接收客户端地址。
    • &client_addr_len: 指向 socklen_t 变量的指针,该变量在调用前被初始化为 sizeof(client_address)
  4. accept 是一个阻塞调用。如果没有客户端连接,程序会在此处挂起等待。
  5. 当有客户端连接到达时,accept 返回一个新的文件描述符 client_fd
  6. 调用 handle_client 函数处理与该客户端的通信。这个函数会读取客户端数据并回显回去。
  7. handle_client 函数结束时(客户端断开或出错),会调用 close(client_fd) 关闭这个连接。
  8. 主循环继续,再次调用 accept 等待下一个客户端。

缺点: 这种顺序处理的方式效率很低。服务器在处理一个客户端时,无法接受其他客户端的连接,直到当前客户端处理完毕。

示例 2:并发 TCP 服务器 (使用 fork)

这个例子演示了如何使用 fork 创建子进程来并发处理多个客户端连接。

// concurrent_tcp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/wait.h> // waitpid

#define PORT 8080
#define BACKLOG 10

void handle_client(int client_fd, struct sockaddr_in *client_addr) {
    char buffer[1024];
    ssize_t bytes_read;

    printf("Child %d: Handling client %s:%d (fd: %d)\n",
           getpid(), 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("Child %d: Received from client: %s", getpid(), buffer);
        if (write(client_fd, buffer, bytes_read) != bytes_read) {
            perror("Child: write to client failed");
            break;
        }
    }

    if (bytes_read < 0) {
        perror("Child: read from client failed");
    } else {
        printf("Child %d: Client %s:%d disconnected.\n",
               getpid(), inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port));
    }

    close(client_fd);
    printf("Child %d: Connection closed. Exiting.\n", getpid());
    _exit(EXIT_SUCCESS); // 子进程使用 _exit 退出
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    int opt = 1;
    pid_t pid;

    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Concurrent Server (PID: %d) listening on port %d\n", getpid(), PORT);

    while (1) {
        client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
        if (client_fd < 0) {
            perror("accept failed");
            continue;
        }

        printf("Main process (PID: %d): New connection accepted.\n", getpid());

        // Fork a new process to handle the client
        pid = fork();
        if (pid < 0) {
            perror("fork failed");
            close(client_fd); // Important: close the client fd on fork failure
        } else if (pid == 0) {
            // --- Child process ---
            close(server_fd); // Child doesn't need the listening socket
            handle_client(client_fd, &client_address);
            // handle_client calls close(client_fd) and _exit()
            // so nothing more needed here
        } else {
            // --- Parent process ---
            close(client_fd); // Parent doesn't need the client-specific socket
            printf("Main process (PID: %d): Forked child process (PID: %d) to handle client.\n", getpid(), pid);

            // Optional: Clean up any finished child processes (non-blocking)
            // This prevents zombie processes if children finish quickly
            pid_t wpid;
            int status;
            while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) {
                printf("Main process (PID: %d): Reaped child process (PID: %d)\n", getpid(), wpid);
            }
        }
    }

    close(server_fd);
    return 0;
}

代码解释:

  1. 服务器设置部分与顺序服务器相同。
  2. 在 accept 成功返回后,立即调用 fork()
  3. fork 返回后
    • 子进程 (pid == 0):
      • 关闭不需要的监听套接字 server_fd
      • 调用 handle_client(client_fd, ...) 处理客户端。
      • handle_client 处理完毕后会关闭 client_fd 并调用 _exit() 退出。
    • 父进程 (pid > 0):
      • 关闭不需要的客户端套接字 client_fd(因为子进程在处理它)。
      • 打印信息,表明已派生子进程处理客户端。
      • 可选地调用 waitpid(-1, &status, WNOHANG) 来非阻塞地清理已经结束的子进程(回收僵尸进程)。如果省略这一步,结束的子进程会变成僵尸进程,直到父进程退出。
  4. 父进程继续循环,调用 accept 等待下一个客户端连接。

示例 3:使用 accept4 设置非阻塞客户端套接字

这个例子演示了如何使用 accept4 函数在创建新连接套接字的同时就将其设置为非阻塞模式。

// accept4_example.c
#define _GNU_SOURCE // 必须定义以使用 accept4
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // F_GETFL, F_SETFL, O_NONBLOCK

#define PORT 8080
#define BACKLOG 10

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    int opt = 1;
    int flags;

    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d. Accepting connections...\n", PORT);

    while (1) {
        printf("Waiting for a connection...\n");

        // 使用 accept4 直接创建非阻塞的客户端套接字
        client_fd = accept4(server_fd, (struct sockaddr *)&client_address, &client_addr_len, SOCK_NONBLOCK);

        if (client_fd < 0) {
            perror("accept4 failed");
            continue;
        }

        printf("New connection accepted (fd: %d). Checking if it's non-blocking...\n", client_fd);

        // 验证套接字是否确实是非阻塞的
        flags = fcntl(client_fd, F_GETFL, 0);
        if (flags == -1) {
            perror("fcntl F_GETFL failed");
            close(client_fd);
            continue;
        }

        if (flags & O_NONBLOCK) {
            printf("Confirmed: Client socket (fd: %d) is non-blocking.\n", client_fd);
        } else {
            printf("Warning: Client socket (fd: %d) is NOT non-blocking.\n", client_fd);
        }

        // --- 在这里,你可以对非阻塞的 client_fd 进行 read/write/select/poll 操作 ---
        // 例如,将其添加到 epoll 或 select 的监视集合中

        // 为了演示,我们简单地关闭它
        printf("Closing client socket (fd: %d).\n", client_fd);
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

代码解释:

  1. 服务器设置部分与之前相同。
  2. 在调用 accept4 时,传入了 SOCK_NONBLOCK 标志作为第四个参数。
  3. 如果 accept4 成功,返回的 client_fd 就已经被设置为非阻塞模式。
  4. 代码通过 fcntl(client_fd, F_GETFL, 0) 获取套接字标志,并检查 O_NONBLOCK 位是否被设置,以验证 accept4 的效果。
  5. 在实际应用中,得到非阻塞的 client_fd 后,通常会将其加入到 selectpoll 或 epoll 的监视集合中,以便高效地管理多个并发连接。

重要提示与注意事项:

  1. 返回新的文件描述符accept 返回的文件描述符与原始监听套接字 sockfd 完全不同。原始套接字继续用于监听,新套接字用于与特定客户端通信。
  2. 必须关闭: 服务器在与客户端通信结束后,必须调用 close() 关闭 accept 返回的那个文件描述符,以释放资源。
  3. 获取客户端地址: 利用 addr 和 addrlen 参数获取客户端的 IP 和端口对于日志记录、访问控制、调试等非常有用。
  4. 并发处理: 对于需要同时处理多个客户端的服务器,必须使用 fork、多线程或 I/O 多路复用(select/poll/epoll)等技术。简单的顺序处理无法满足实际需求。
  5. 错误处理: 始终检查 accept 的返回值。在繁忙的服务器上,非阻塞 accept 可能会因为没有连接而返回 EAGAIN
  6. accept4 的优势accept4 可以在原子操作中设置新套接字的属性,避免了先 accept 再 fcntl 的两步操作,理论上更高效且没有竞态条件。

总结:

accept 是 TCP 服务器模型的核心。它使得服务器能够从监听状态进入与客户端的实际数据交换状态。理解其阻塞/非阻塞行为、返回值含义以及如何与并发处理技术(如 fork)结合使用,是构建健壮网络服务器的基础。accept4 则为需要精细控制新连接套接字属性的场景提供了便利。

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

发表回复

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