accept() 是什么?
accept() 是一个系统调用(在 POSIX 系统中),属于 Berkeley Sockets (BSD Sockets) API 的一部分,它的主要作用是:

(图片来源网络,侵删)
从监听套接字(listening socket)的等待队列中取出一个已完成的连接请求,并创建一个新的套接字来与这个客户端进行通信。
accept() 就像一个“前台接待员”:
- 服务器先创建一个“总机”套接字(
listen状态),并开始监听端口,等待电话(连接请求)打进来。 - 当有客户端拨打电话时,请求被排队等待。
accept()函数被调用,它会从等待队列中取出第一个请求,并为这个特定的通话分配一个新的“分机”套接字。- 之后,服务器就可以通过这个新的“分机”套接字与该客户端进行一对一的数据收发,而“总机”套接字则继续监听新的来电。
函数原型
accept() 函数的原型通常在 <sys/socket.h> 头文件中定义:
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数详解:
-
int sockfd:
(图片来源网络,侵删)- 这是一个监听套接字(listening socket)的文件描述符。
- 这个套接字之前必须已经调用
socket()创建,并调用了bind()绑定到一个本地地址和端口,最后调用了listen()将其设置为监听状态。 - 它的作用是告诉
accept()从哪个等待队列中取出连接请求。
-
struct sockaddr *addr:- 这是一个指向
sockaddr结构体的指针。 accept()成功后,会将连接对端(也就是客户端)的地址信息(如 IP 地址和端口号)填充到这个结构体中。- 如果你对客户端的地址不感兴趣,可以将其设置为
NULL。
- 这是一个指向
-
socklen_t *addrlen:- 这是一个指向
socklen_t类型的指针,它代表了addr结构体的大小。 - 在调用
accept()之前,你需要将*addrlen设置为addr结构体的大小(sizeof(struct sockaddr_in))。 - 调用之后,
accept()会更新*addrlen为实际填充的字节数。 addr是NULL,addrlen也必须设置为NULL。
- 这是一个指向
返回值:
- 成功时:
accept()返回一个新的套接字文件描述符,这个新的套接字专门用于与发起连接请求的那个客户端进行通信(read/write),而传入的sockfd监听套接字保持不变,继续监听新的连接请求。 - 失败时:返回
-1,并设置errno来指示错误原因,常见的错误包括:EAGAIN或EWOULDBLOCK:在非阻塞模式下,没有等待的连接。EBADF:sockfd不是一个有效的文件描述符。EFAULT:addr指向的地址不可访问。EINVAL:套接字没有处于监听状态。EMFILE:进程已达到可打开文件描述符的上限。ENFILE:系统已达到打开文件的总数上限。ECONNABORTED:连接被中止。
accept() 的工作流程(与 listen 配合)
accept() 的使用离不开 listen() 和 bind(),一个典型的 TCP 服务器流程如下:
-
socket(): 创建一个套接字。
(图片来源网络,侵删)int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
-
bind(): 将套接字绑定到指定的 IP 地址和端口。struct sockaddr_in server_addr; // ... 初始化 server_addr ... bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
-
listen(): 将套接字设置为被动监听模式,并指定一个等待队列的长度。listen(listen_fd, 10); // 最多允许10个连接在等待队列中
-
accept(): 循环调用accept()来等待并接受客户端连接。struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); // 阻塞等待,直到有客户端连接 int new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len); if (new_fd < 0) { // 处理错误 perror("accept"); } else { printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 使用 new_fd 与客户端通信 // ... // 通信结束后,关闭 new_fd close(new_fd); }
阻塞 vs. 非阻塞模式
accept() 的行为受套接字模式影响:
-
阻塞模式 (默认):
- 如果没有等待的连接,
accept()调用会阻塞(程序暂停执行),直到一个客户端连接建立为止。 - 这是最常见和简单的用法。
- 如果没有等待的连接,
-
非阻塞模式:
- 如果套接字被设置为非阻塞(通过
fcntl()或ioctl()),调用accept()时:- 如果有等待的连接,它立即返回新的套接字。
- 如果没有等待的连接,它会立即返回
-1,并将errno设置为EAGAIN或EWOULDBLOCK。
- 非阻塞模式通常与 I/O 多路复用(如
select,poll,epoll)结合使用,以高效地管理多个连接。
- 如果套接字被设置为非阻塞(通过
一个简单的完整示例
这是一个简单的回显服务器,它会接受客户端连接,并将客户端发送的任何消息原样返回。
服务器端代码 (server.c)
#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>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT);
// 2. 绑定套接字到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Accepted connection from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 与客户端通信
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
printf("Received: %s", buffer);
send(new_socket, buffer, valread, 0); // 回显
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
if (valread == 0) {
printf("Client disconnected\n");
} else if (valread < 0) {
perror("read");
}
// 6. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
客户端代码 (client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *message = "Hello from client";
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IPv4 地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// 3. 发送数据
send(sock, message, strlen(message), 0);
printf("Message sent\n");
// 4. 接收回显
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server echoed: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
- 核心作用:服务器端接受客户端连接,并生成一个用于通信的新套接字。
- 关键参数:
sockfd(监听套接字),addr(客户端地址),addrlen(地址长度)。 - 返回值:成功返回新套接字的文件描述符,失败返回 -1。
- 阻塞特性:默认阻塞,直到有连接建立。
- 使用场景:所有基于 TCP 的服务器程序,如 Web 服务器、数据库服务器、聊天服务器等。
