一、accept() 函数概述

1.1 定义与作用

accept() 函数用于从监听套接字的已完成连接队列中取出一个已建立的连接,返回一个新的套接字文件描述符,用于与客户端进行通信。在 TCP 服务器编程中,这是处理客户端连接的关键步骤。

1.2 函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

二、accept() 函数的参数与返回值

2.1 参数解析

  • sockfd:监听套接字的文件描述符,由 socket() 创建,并经过 bind()listen() 函数设置为监听状态。
  • addr:可选参数,指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。
  • addrlen:指向一个 socklen_t 类型的变量,初始值为 addr 所指向的结构体的长度,返回时包含客户端地址的实际长度。

2.2 返回值

  • 成功:返回一个新的套接字文件描述符,用于与客户端通信。
  • 失败:返回 -1,并设置 errno,指示具体的错误原因。

三、accept() 函数的工作原理

3.1 连接队列

在调用 listen() 后,套接字进入被动监听状态,内核为其维护两个队列:

  1. 未完成连接队列:存放已收到客户端的 SYN 请求,但尚未完成三次握手的连接。
  2. 已完成连接队列:存放已完成三次握手的连接,等待应用程序调用 accept() 取出处理。

accept() 函数从已完成连接队列中取出一个连接,返回新的套接字描述符。

3.2 套接字的区别

  • 监听套接字 (sockfd):用于监听新的连接请求。
  • 已连接套接字:由 accept() 返回,用于与特定客户端通信。

四、accept() 的使用示例

4.1 基本的服务器接收连接示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BACKLOG 5

int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addrlen = sizeof(client_addr);

    // 创建套接字
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 绑定地址
    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(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, BACKLOG) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

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

    // 接受连接
    if ((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addrlen)) == -1) {
        perror("accept failed");
        close(listen_fd);
        return -1;
    }

    printf("Accepted a connection from %s:%d\n",
           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 与客户端通信...

    // 关闭套接字
    close(conn_fd);
    close(listen_fd);
    return 0;
}

五、使用注意事项

5.1 accept() 的阻塞行为

  • 问题:默认情况下,accept() 是阻塞的,如果已完成连接队列为空,accept() 将阻塞,直到有新的连接。
  • 解决方案
    • 非阻塞模式:将监听套接字设置为非阻塞模式,若无连接则立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK
    • I/O 多路复用:使用 select()poll()epoll() 等机制,在调用 accept() 之前检测套接字是否可读。

5.2 addraddrlen 参数

  • 可选性addraddrlen 可以为 NULL,如果不需要获取客户端地址信息。
  • 注意事项:若需要获取客户端信息,必须提供有效的 addraddrlen,并确保 addrlen 指向的变量初始化为 addr 的长度。

5.3 文件描述符的管理

  • 问题accept() 返回的新套接字描述符需要妥善管理,及时关闭,防止文件描述符耗尽。
  • 解决方案:在通信结束后,调用 close() 关闭已连接套接字。

5.4 多线程和并发处理

  • 场景:在高并发服务器中,需要同时处理多个客户端连接。
  • 解决方案:每次 accept() 后,创建新线程或进程处理连接,或使用事件驱动的方式。

六、常见问题

6.1 忘记检查返回值

  • 问题:未检查 accept() 的返回值,无法及时发现错误。
  • 解决方案:始终检查返回值,处理可能的错误。

6.2 忽略 EINTR 错误

  • 问题accept() 可能被信号中断,返回 -1,并设置 errnoEINTR
  • 解决方案:在收到 EINTR 错误时,重新调用 accept()
1
2
3
4
5
6
7
8
while ((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addrlen)) == -1) {
    if (errno == EINTR)
        continue;
    else {
        perror("accept failed");
        break;
    }
}

6.3 文件描述符耗尽

  • 问题:未及时关闭已连接套接字,导致文件描述符耗尽,无法接受新的连接。
  • 解决方案:妥善管理套接字的生命周期,及时关闭不再使用的套接字。

6.4 拒绝服务攻击的防范

  • 问题:恶意客户端大量连接服务器,导致资源耗尽。
  • 解决方案
    • 限制每个客户端的连接数。
    • 实现连接超时机制,关闭长时间未活动的连接。
    • 使用防火墙和入侵检测系统。

七、accept() 函数的高级用法

7.1 使用 accept4() 函数

  • 简介accept4() 是 Linux 特有的系统调用,功能类似于 accept(),但增加了一个 flags 参数。
  • 函数原型
1
2
3
#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
  • 优势
    • 可以在接受连接的同时设置新套接字的属性,如非阻塞模式 (O_NONBLOCK)。
    • 减少系统调用次数,提高性能。

7.2 非阻塞 accept()

  • 方法:将监听套接字设置为非阻塞模式,或使用 accept4() 设置 O_NONBLOCK 标志。
  • 应用场景:在事件驱动的服务器中,避免阻塞在 accept() 调用上。

7.3 获取客户端的更多信息

  • 方法:使用 getpeername() 获取已连接套接字对端的地址信息。
  • 示例
1
2
3
4
5
6
7
struct sockaddr_in peer_addr;
socklen_t peer_len = sizeof(peer_addr);

if (getpeername(conn_fd, (struct sockaddr *)&peer_addr, &peer_len) == 0) {
    printf("Connected to %s:%d\n",
           inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
}

八、拓展资料:关键概念解释

8.1 阻塞与非阻塞模式

  • 阻塞模式:默认情况下,accept() 会阻塞,直到有新的连接或发生错误。
  • 非阻塞模式accept() 立即返回,若无连接,则返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  • 设置方法:使用 fcntl() 函数设置套接字为非阻塞模式。
1
2
3
4
#include <fcntl.h>

int flags = fcntl(listen_fd, F_GETFL, 0);
fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);

8.2 I/O 多路复用

  • 概念:使用单个线程或进程监控多个文件描述符,提高资源利用率。
  • 常用机制select()poll()epoll()
  • 应用场景:高并发服务器,避免为每个连接创建线程或进程。

8.3 accept() 与多线程

  • 问题:在多线程服务器中,多个线程同时调用 accept() 可能导致竞争。
  • 解决方案
    • 使用同步机制,如互斥锁,确保只有一个线程调用 accept()
    • 或者将监听套接字设置为非阻塞模式,结合事件驱动模型。

8.4 SO_REUSEADDRSO_REUSEPORT

  • SO_REUSEADDR:允许绑定已在使用的地址,常用于服务器重启时快速重新绑定端口。
  • SO_REUSEPORT:允许多个套接字绑定到同一 IP 和端口,常用于多线程或多进程服务器。

九、示例代码:多线程服务器示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BACKLOG 5

void *handle_client(void *arg) {
    int conn_fd = *(int *)arg;
    free(arg);

    char buffer[1024];
    ssize_t bytes_read;

    // 通信处理
    while ((bytes_read = recv(conn_fd, buffer, sizeof(buffer), 0)) > 0) {
        send(conn_fd, buffer, bytes_read, 0);
    }

    close(conn_fd);
    return NULL;
}

int main() {
    int listen_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addrlen = sizeof(client_addr);

    // 创建套接字
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 绑定地址
    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(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, BACKLOG) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

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

    // 接受并处理连接
    while (1) {
        int *conn_fd = malloc(sizeof(int));
        if ((*conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addrlen)) == -1) {
            perror("accept failed");
            free(conn_fd);
            continue;
        }

        printf("Accepted a connection from %s:%d\n",
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        pthread_t tid;
        if (pthread_create(&tid, NULL, handle_client, conn_fd) != 0) {
            perror("pthread_create failed");
            close(*conn_fd);
            free(conn_fd);
        } else {
            pthread_detach(tid);
        }
    }

    close(listen_fd);
    return 0;
}