这次我们介绍 accept
函数,它是 TCP 服务器用来接受客户端连接请求的核心系统调用。
1. 函数介绍
accept
是一个 Linux 系统调用,专门用于TCP 服务器(使用 SOCK_STREAM
套接字)。它的主要功能是从监听套接字(通过 listen
设置的套接字)的未决连接队列(pending connection queue)中取出第一个连接请求,并为这个新连接创建一个全新的、独立的套接字文件描述符。
你可以把 accept
想象成总机接线员:
- 有很多电话(客户端连接请求)打进来,响铃并排队在总机(监听套接字)那里。
- 接线员(
accept
调用)拿起一个响铃的电话。 - 接线员把这条线路接到一个新的、专用的电话线(新的套接字文件描述符)上。
- 接线员可以继续去接下一个电话(下一次
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
: 这是监听套接字的文件描述符。它必须是:- 通过
socket()
成功创建的。 - 通过
bind()
绑定了本地地址(IP 和端口)的。 - 通过
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 10void 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;
}
代码解释:
- 创建、绑定、监听服务器套接字,这部分与之前
socket
,bind
,listen
的例子相同。 - 进入一个无限的
while(1)
循环。 - 在循环内部,调用
accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len)
。server_fd
: 监听套接字。&client_address
: 指向sockaddr_in
结构的指针,用于接收客户端地址。&client_addr_len
: 指向socklen_t
变量的指针,该变量在调用前被初始化为sizeof(client_address)
。
accept
是一个阻塞调用。如果没有客户端连接,程序会在此处挂起等待。- 当有客户端连接到达时,
accept
返回一个新的文件描述符client_fd
。 - 调用
handle_client
函数处理与该客户端的通信。这个函数会读取客户端数据并回显回去。 handle_client
函数结束时(客户端断开或出错),会调用close(client_fd)
关闭这个连接。- 主循环继续,再次调用
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 10void 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 clientpid = 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 sockethandle_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 socketprintf("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 quicklypid_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;
}
代码解释:
- 服务器设置部分与顺序服务器相同。
- 在
accept
成功返回后,立即调用fork()
。 fork
返回后:- 在子进程 (
pid == 0
):- 关闭不需要的监听套接字
server_fd
。 - 调用
handle_client(client_fd, ...)
处理客户端。 handle_client
处理完毕后会关闭client_fd
并调用_exit()
退出。
- 关闭不需要的监听套接字
- 在父进程 (
pid > 0
):- 关闭不需要的客户端套接字
client_fd
(因为子进程在处理它)。 - 打印信息,表明已派生子进程处理客户端。
- 可选地调用
waitpid(-1, &status, WNOHANG)
来非阻塞地清理已经结束的子进程(回收僵尸进程)。如果省略这一步,结束的子进程会变成僵尸进程,直到父进程退出。
- 关闭不需要的客户端套接字
- 在子进程 (
- 父进程继续循环,调用
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 10int 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;
}
代码解释:
- 服务器设置部分与之前相同。
- 在调用
accept4
时,传入了SOCK_NONBLOCK
标志作为第四个参数。 - 如果
accept4
成功,返回的client_fd
就已经被设置为非阻塞模式。 - 代码通过
fcntl(client_fd, F_GETFL, 0)
获取套接字标志,并检查O_NONBLOCK
位是否被设置,以验证accept4
的效果。 - 在实际应用中,得到非阻塞的
client_fd
后,通常会将其加入到select
、poll
或epoll
的监视集合中,以便高效地管理多个并发连接。
重要提示与注意事项:
- 返回新的文件描述符:
accept
返回的文件描述符与原始监听套接字sockfd
完全不同。原始套接字继续用于监听,新套接字用于与特定客户端通信。 - 必须关闭: 服务器在与客户端通信结束后,必须调用
close()
关闭accept
返回的那个文件描述符,以释放资源。 - 获取客户端地址: 利用
addr
和addrlen
参数获取客户端的 IP 和端口对于日志记录、访问控制、调试等非常有用。 - 并发处理: 对于需要同时处理多个客户端的服务器,必须使用
fork
、多线程或 I/O 多路复用(select
/poll
/epoll
)等技术。简单的顺序处理无法满足实际需求。 - 错误处理: 始终检查
accept
的返回值。在繁忙的服务器上,非阻塞accept
可能会因为没有连接而返回EAGAIN
。 accept4
的优势:accept4
可以在原子操作中设置新套接字的属性,避免了先accept
再fcntl
的两步操作,理论上更高效且没有竞态条件。
总结:
accept
是 TCP 服务器模型的核心。它使得服务器能够从监听状态进入与客户端的实际数据交换状态。理解其阻塞/非阻塞行为、返回值含义以及如何与并发处理技术(如 fork
)结合使用,是构建健壮网络服务器的基础。accept4
则为需要精细控制新连接套接字属性的场景提供了便利。