
accept函数如何接收客户端连接:一个简单示例
在 TCP 服务器中,accept 的作用是从监听套接字的待处理连接队列中取出一个连接,并为这个客户端创建一个新的套接字文件描述符。服务器随后通过这个新描述符与客户端收发数据。
容易混淆的一点是:accept 本身并不负责创建监听套接字,也不负责建立客户端到服务器的网络连接。服务器通常先调用 socket、bind 和 listen,再循环调用 accept。理解这几个阶段的分工,是正确使用它的基础。
一、accept 适用于什么场景
典型场景是服务器通过 TCP 监听某个地址和端口,等待客户端发起连接。例如 HTTP 服务、数据库服务和自定义 TCP 协议服务,都可以使用类似的流程:
- 用
socket创建套接字。 - 用
bind将套接字绑定到本地 IP 地址和端口。 - 用
listen将套接字设为监听状态。 - 用
accept取出已经完成连接握手、等待应用程序处理的连接。 - 在返回的新套接字上读写数据,处理完后关闭它。
accept 主要用于面向连接的套接字,最常见的是 TCP。它不是 UDP 收包函数:UDP 通常通过 recvfrom 接收数据报,不通过 accept 为每个对端创建连接套接字。
二、函数原型与参数含义
在 POSIX 系统中,常见函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:处于监听状态的套接字文件描述符。addr:可选输出参数,用来接收对端地址。如果不需要客户端地址,可以传入NULL。addrlen:addr缓冲区长度的指针。调用前写入缓冲区容量;调用返回后,内核将其更新为实际地址长度。
调用成功时,返回一个新的、非负的文件描述符。这个描述符代表与某个客户端之间的连接;之后的 recv、send 或 read、write 操作都应使用它。原来的 sockfd 仍然是监听套接字,通常会继续留在接收新连接的循环中。
调用失败时返回 -1,并设置 errno。监听套接字不会因为一次成功的 accept 而变成客户端连接套接字,也不应把它关闭后再尝试用同一个描述符处理已接受连接。
三、一个可运行的 TCP 回显服务器
下面的示例使用 IPv4,在本机地址 127.0.0.1 的 9090 端口监听。服务器一次处理一个客户端:收到数据后原样发回;客户端关闭连接后,服务器继续接收下一个客户端。
代码针对常见的 POSIX/Linux 环境。它处理了系统调用被信号中断、发送数据只写出一部分等情况。示例忽略 SIGPIPE,这样客户端提前断开时,写入已关闭连接不会直接令进程因该信号退出;发送失败仍会通过返回值报告。
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERVER_PORT 9090
#define BUFFER_SIZE 4096
static int send_all(int fd, const char *buf, size_t len)
{
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, buf + sent, len - sent, 0);
if (n > 0) {
sent += (size_t)n;
continue;
}
if (n < 0 && errno == EINTR) {
continue;
}
return -1;
}
return 0;
}
int main(void)
{
int listen_fd = -1;
struct sockaddr_in server_addr;
/* 避免向已断开的连接写入时由 SIGPIPE 终止进程。 */
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("signal");
return EXIT_FAILURE;
}
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) != 1) {
fprintf(stderr, "invalid server address\n");
close(listen_fd);
return EXIT_FAILURE;
}
if (bind(listen_fd, (struct sockaddr *)&server_addr,
sizeof(server_addr)) < 0) {
perror("bind");
close(listen_fd);
return EXIT_FAILURE;
}
if (listen(listen_fd, 16) < 0) {
perror("listen");
close(listen_fd);
return EXIT_FAILURE;
}
printf("listening on 127.0.0.1:%d\n", SERVER_PORT);
for (;;) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd;
client_fd = accept(listen_fd,
(struct sockaddr *)&client_addr,
&client_len);
if (client_fd < 0) {
if (errno == EINTR) {
/* 信号中断了调用,监听套接字仍可继续使用。 */
continue;
}
perror("accept");
continue;
}
char client_ip[INET_ADDRSTRLEN] = "unknown";
if (inet_ntop(AF_INET, &client_addr.sin_addr,
client_ip, sizeof(client_ip)) != NULL) {
printf("accepted client %s:%u\n",
client_ip, (unsigned)ntohs(client_addr.sin_port));
}
for (;;) {
char buffer[BUFFER_SIZE];
ssize_t n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n > 0) {
if (send_all(client_fd, buffer, (size_t)n) < 0) {
perror("send");
break;
}
continue;
}
if (n == 0) {
/* 对端有序关闭了连接。 */
printf("client disconnected\n");
break;
}
if (errno == EINTR) {
continue;
}
perror("recv");
break;
}
close(client_fd);
}
close(listen_fd);
return EXIT_SUCCESS;
}
将代码保存为 accept_server.c,在 Linux 上可以用下面的命令编译:
cc -Wall -Wextra -O2 accept_server.c -o accept_server
运行服务器:
./accept_server
另开一个终端,用常见的 nc(netcat)连接并发送一行文字:
nc 127.0.0.1 9090
在终端输入内容后,服务器会把收到的字节发回。按 Ctrl+D 关闭输入,或者在服务器端结束进程。若系统没有安装 nc,也可以用其他 TCP 客户端连接该地址和端口。
四、示例中的关键细节
1. 地址长度是输入输出参数
示例先设置 client_len 为 sizeof(client_addr),然后把它的地址传给 accept。调用后,client_len 表示实际返回的对端地址长度。忽略这一点,可能导致地址结构没有正确初始化,或者未来更换地址类型时出现问题。
如果不需要对端地址,可以这样调用:
int client_fd = accept(listen_fd, NULL, NULL);
若 addr 非空,addrlen 也应提供有效指针;不能把一个没有初始化的长度变量传进去。反过来,如果不提供地址输出参数,两个参数都传 NULL 是有效用法。
2. 监听套接字与连接套接字分工不同
服务器需要同时保留监听描述符和已接受连接的描述符。监听描述符用于再次调用 accept;连接描述符用于和某个客户端交换数据。示例关闭每个客户端对应的 client_fd,但不关闭 listen_fd,因此还能继续接受连接。
3. TCP 是字节流,不保留消息边界
一次 recv 读到的数据,不一定恰好等于客户端一次 send 写入的数据。TCP 只提供有序字节流,不提供应用层消息分隔。回显示例对每次读到的字节立即原样写回,所以不依赖消息边界。实际协议需要自行定义消息格式,例如固定长度、长度前缀或分隔符,并处理拆包与粘连。
4. send 可能只发送一部分数据
send 的成功返回值可以小于请求发送的字节数。示例用 send_all 循环发送尚未写出的部分,直到数据全部发送或发生错误。生产代码还要结合非阻塞 I/O 处理“暂时无法写入”的情况,不能把一次 send 返回成功当作整段数据都已发出。
五、阻塞、非阻塞与并发处理
默认情况下,套接字是阻塞的。对阻塞监听套接字调用 accept\ 时,如果没有待处理连接,调用通常会等待,直到有连接到来或发生错误。若监听套接字设为非阻塞,队列暂时为空时,accept 会失败并设置 errno 为 EAGAIN 或 EWOULDBLOCK;具体代码中通常需要把这两种情况都视为“当前没有连接可取”。
非阻塞模式常与 poll、select、epoll 等 I/O 多路复用机制配合。事件通知后,程序可以反复调用 accept\,直到收到 EAGAIN 或 EWOULDBLOCK,以便取出队列中当前所有可接受的连接。若使用边缘触发的 epoll,遗漏这一步可能导致之后没有新的边沿通知,从而让已排队的连接长期得不到处理。
上面的示例是单线程、顺序处理:一个客户端连接期间,服务器会一直读写它,期间不调用下一次 accept。因此,一个不发送数据也不关闭连接的客户端就可能让其他客户端等待。这种结构适合演示 API 和测试简单协议,不适合作为高并发服务器架构。实际服务可以采用线程池、进程、事件循环或其他并发模型,并为连接设置超时和资源上限。
六、错误处理与平台差异
对任何失败的系统调用,都应先检查返回值,再依据 errno 决定处理方式。EINTR 表示调用被信号中断;在不打算因该信号退出或处理其他工作时,通常可以重试。示例对 accept 和 recv 都做了这种处理。
非阻塞模式下,EAGAIN 或 EWOULDBLOCK 通常表示当前没有可取的连接,而不一定是服务器故障。除此之外,文件描述符耗尽、系统资源不足、监听套接字状态异常等情况也可能导致失败。不能把所有错误都无限重试:例如文件描述符耗尽时,持续紧密循环会造成高 CPU 占用。服务程序应根据错误类型选择等待、退避、记录并退出,或执行资源恢复策略。
在 Linux 上,监听套接字处于非阻塞状态时,若连接在进入应用程序处理之前已经失效,accept 还可能报告一些网络相关错误。健壮的服务器通常会将适合重试的瞬时网络错误按暂时性问题处理,而不是把它们等同于程序逻辑错误。具体错误集合与操作系统有关,应查阅目标平台的 accept(2) 文档。
POSIX 的 accept 不会通过参数设置新连接套接字的非阻塞状态或关闭执行时继承。Linux 上通常可以使用 accept4,在接受连接时通过 SOCK_NONBLOCK 和 SOCK_CLOEXEC 设置这些标志;它不是所有系统都提供的可移植 POSIX 接口。若使用普通 accept,需要按目标系统的规则在返回后设置相应标志,并考虑多线程程序中“接受连接后、设置标志前”被其他线程创建子进程等竞态。
七、backlog 不等于最大客户端数
listen(listen_fd, backlog) 中的 backlog 与等待被应用程序取出的连接队列有关。它不是服务器最多能同时服务多少客户端的通用配置,也不能替代线程数、连接数或文件描述符数量的管理。操作系统可能对该值进行限制或调整,队列语义和上限也会因系统实现而异。
如果应用程序处理连接速度长期低于连接到达速度,待处理队列可能被占满。此时客户端可能遇到连接延迟或连接失败。调大 backlog 不能解决服务器处理能力不足的问题;还需要检查请求处理是否阻塞、并发模型是否合适,以及系统的文件描述符和资源限制是否充足。
八、常见错误
- 把返回值当成客户端地址或状态码。 成功时返回的是新的套接字描述符;对端地址写入
addr指向的结构。 - 拿监听描述符收发客户端数据。 客户端数据应通过
accept返回的连接描述符读写。 - 没有初始化或正确传递地址长度。 用地址输出参数时,调用前将长度设为缓冲区大小,并传入其地址。
- 成功后忘记关闭连接描述符。 长期运行的服务可能因此耗尽文件描述符,表现为后续
accept失败。 - 认为一次 recv 就是一条完整消息。 TCP 是字节流,应用协议必须自行定义边界,并处理部分读写。
- 忽略 EINTR、EAGAIN 等错误。 阻塞与非阻塞模式下的重试条件不同,要结合调用模式和
errno处理。 - 误把 backlog 当作并发容量。 它不等于活动连接上限,更不能保证服务器能及时处理连接。
- 让客户端输入直接变成日志格式字符串。 应使用固定格式参数,例如
printf("%s", text),不要写成printf(text)。
九、accept 与安全边界
accept 只负责取出连接,不会验证用户身份、授权请求或保证数据安全。能够建立 TCP 连接的客户端并不因此获得可信身份。应用程序需要按具体协议执行身份认证、权限检查、输入校验和资源限制;涉及敏感数据时,还应考虑使用经过正确配置的 TLS 等加密与认证机制。
示例绑定到 127.0.0.1,意味着只监听本机 IPv4 回环地址,其他机器通常无法通过网络连接到该监听地址。若改为绑定 0.0.0.0,服务器会在本机可用的 IPv4 接口上监听,是否能从外部访问还取决于主机防火墙、网络路由和云平台安全规则。监听范围应按服务需求选择,不能把绑定回环地址或使用某个端口当作身份认证措施。
客户端地址可以用于诊断、审计或粗略限流,但它不能单独作为可靠身份凭据。经过 NAT 的多个客户端可能显示相同的公网地址;代理场景中的真实客户端信息要通过可信代理协议取得并正确验证;来源 IP 也可能受到网络环境影响。是否信任转发头、如何识别代理,应由明确的部署边界和协议配置决定。
最后,监听地址并不会自动赋予程序对系统资源的无限访问权,但恶意或过量连接仍可能消耗文件描述符、内存、线程和 CPU。面向不可信网络时,应设置连接与请求超时、限制同时处理的连接数和消息大小,并对错误及过载采取可控的退避或拒绝策略。
总结
使用 accept 时,记住三个核心点:它从监听队列中取出连接并返回新的套接字描述符;对端地址参数可选,但提供地址时必须正确初始化长度;阻塞行为、错误处理、资源回收和并发模型都需要由服务器程序明确管理。把这些边界处理好,才能从一个可运行的示例走向稳定的网络服务。



