1.什么是poll
poll
是一种用于监控多个文件描述符状态的系统调用,它可以等待多个文件描述符上的事件发生。它与 select
和 epoll
类似,但在某些场景下使用更为方便。
poll的机制与select类似,与select在本质上没有多大差别,使用方法也类似,下面的是对于二者的对比:
- 内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
- poll和select检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
- select检测的文件描述符个数上限是1024,poll没有最大文件描述符数量的限制
- select可以跨平台使用,poll只能在Linux平台使用
2.poll函数
2.1struct pollfd
struct pollfd
是 poll
函数中用来描述每个文件描述符及其关注事件的结构体。
struct pollfd {
int fd; // 文件描述符
short events; // 要监视的事件掩码
short revents; // 实际发生的事件掩码
};
fd:需要监视的文件描述符。
events:关注的事件掩码,可以是以下宏的组合:
POLLIN:可读事件。
POLLOUT:可写事件。
POLLERR:发生错误事件。
POLLHUP:对端关闭连接事件。
POLLNVAL:非法请求事件。
revents:实际发生的事件掩码,由系统填充并返回给调用者。
2.2 poll
函数
功能:
poll
函数用于等待多个文件描述符上的事件发生。- 它会阻塞进程,直到指定的文件描述符中至少有一个文件描述符上有事件发生,或者超过了指定的超时时间。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:指向 struct pollfd 数组的指针,数组中每个元素描述一个文件描述符及其关注的事件。
nfds:fds 数组中元素的数量。这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数1数组的元素总个数)
timeout:超时时间,以毫秒为单位。传递 -1 表示无限阻塞,传递 0 表示立即返回,传递正整数表示超时时间。
返回值:
返回值为发生事件的文件描述符的数量,或者特定的返回码(如 -1 表示出错)。
如果超时时间到期而没有任何文件描述符发生事件,则返回 0。
注意事项和细节:
poll
函数在等待期间会修改传入的fds
数组中每个pollfd
结构体的revents
字段,用于指示实际发生的事件。- 如果
poll
返回值大于0
,可以遍历fds
数组来确定具体哪些文件描述符发生了事件,以及发生了哪些事件。 - 需要注意处理返回值为
-1
的情况,表示poll
调用发生了错误。 - 使用
poll
时应注意合理设置超时时间,避免无限阻塞造成程序无响应。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
#define SERVER_PORT 8888
int main() {
int server_fd, client_fds[MAX_CLIENTS], max_clients = MAX_CLIENTS;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
struct pollfd fds[MAX_CLIENTS + 1]; // +1 用于服务器端套接字
char buffer[BUFFER_SIZE];
int num_clients = 0;
// 创建服务器端套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("无法创建套接字");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
// 将服务器套接字绑定到指定地址
if (bind(server_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) {
perror("绑定失败");
exit(EXIT_FAILURE);
}
// 监听传入连接请求
if (listen(server_fd, 5) < 0) {
perror("监听失败");
exit(EXIT_FAILURE);
}
printf("回显服务器正在端口 %d 上监听...\n", SERVER_PORT);
// 初始化 pollfd 结构体数组,用于服务器套接字和客户端套接字
fds[0].fd = server_fd;
fds[0].events = POLLIN; // 监听可读事件
// 初始化客户端文件描述符数组
for (int i = 1; i < MAX_CLIENTS + 1; ++i) {
fds[i].fd = -1; // -1 表示未使用的槽位
}
while (1) {
// 调用 poll 并阻塞等待事件发生
int ready = poll(fds, num_clients + 1, -1);
if (ready < 0) {
perror("Poll 调用失败");
exit(EXIT_FAILURE);
}
// 检查服务器套接字是否有事件发生
if (fds[0].revents & POLLIN) {
// 接受传入连接
client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *) &client_addr, &client_len);
if (client_fd < 0) {
perror("接受连接失败");
continue;
}
// 将新客户端添加到数组中
if (num_clients < max_clients) {
for (int i = 1; i < max_clients + 1; ++i) {
if (fds[i].fd == -1) {
fds[i].fd = client_fd;
fds[i].events = POLLIN;
client_fds[num_clients++] = client_fd;
printf("客户端已连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
}
} else {
printf("客户端过多,连接被拒绝。\n");
close(client_fd);
}
}
// 检查客户端是否有数据
for (int i = 1; i < num_clients + 1; ++i) {
if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {
// 从客户端读取数据
int bytes_received = recv(fds[i].fd, buffer, BUFFER_SIZE, 0);
if (bytes_received <= 0) {
// 客户端关闭连接或发生错误
if (bytes_received == 0) {
printf("客户端 %d 断开连接。\n", fds[i].fd);
} else {
perror("接收数据失败");
}
close(fds[i].fd);
fds[i].fd = -1;
memmove(&fds[i], &fds[i + 1], (num_clients - i) * sizeof(struct pollfd));
num_clients--;
} else {
// 将数据原样回送给客户端
send(fds[i].fd, buffer, bytes_received, 0);
}
}
}
}
return 0;
}
从上面的测试代码可以得知,使用poll和select进行IO多路转接的处理思路是完全相同的,但是使用poll编写的代码看起来会更直观一些,select使用的位图的方式来标记要委托内核检测的文件描述符(每个比特位对应一个唯一的文件描述符),并且对这个fd_set类型的位图变量进行读写还需要借助一系列的宏函数,操作比较麻烦。而poll直接将要检测的文件描述符的相关信息封装到了一个结构体struct pollfd中,我们可以直接读写这个结构体变量。
另外poll的第二个参数有两种赋值方式,但是都和第一个参数的数组有关系:
- 使用参数1数组的元素个数
- 使用参数1数组中存储的最后一个有效元素对应的下标值 + 1
内核会根据第二个参数传递的值对参数1数组中的文件描述符进行线性遍历,这一点和select也是类似的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024
int main() {
int client_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建客户端套接字
if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("无法创建套接字");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
// 将IP地址从文本转换为网络地址
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("inet_pton 失败");
exit(EXIT_FAILURE);
}
// 连接到服务器
if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("连接服务器失败");
exit(EXIT_FAILURE);
}
printf("连接到服务器 %s:%d 成功\n", SERVER_IP, SERVER_PORT);
// 发送和接收数据
while (1) {
printf("请输入要发送的消息 (输入 \"quit\" 退出):");
fgets(buffer, BUFFER_SIZE, stdin);
// 发送数据
if (send(client_fd, buffer, strlen(buffer), 0) < 0) {
perror("发送数据失败");
break;
}
// 如果输入 quit 则退出
if (strncmp(buffer, "quit", 4) == 0)
break;
// 接收服务器的回复
int bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (bytes_received < 0) {
perror("接收数据失败");
break;
} else if (bytes_received == 0) {
printf("服务器断开连接\n");
break;
} else {
buffer[bytes_received] = '\0';
printf("从服务器接收到的消息:%s\n", buffer);
}
}
// 关闭套接字
close(client_fd);
return 0;
}