IO多路复用-poll
1. poll函数
和select
函数的比较
- 内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
- poll和select检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
- select检测的文件描述符个数上限是1024,poll没有最大文件描述符数量的限制
- select可以跨平台使用,poll只能在Linux平台使用
poll
函数的原型如下:
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 -> 传出 */
};
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
先看**struct pollfd
结构体**:
- fd:委托内核检测的文件描述符
- events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
- revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
再看poll
函数的参数
-
fds: 这是一个struct pollfd类型的数组, 里边存储了待检测的文件描述符的信息
-
nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数1数组的元素总个数)
-
timeout: 指定poll函数的阻塞时长
-
-1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
-
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
-
大于0:阻塞指定的毫秒(ms)数之后,解除阻塞
-
函数返回值:
- 失败: 返回-1
- 成功:返回一个大于0的整数,表示检测的集合中已就绪的文件描述符的总个数
2. 通信代码
服务器端
//
// Created by 47468 on 2024/1/25.
// server.cpp
#include "arpa/inet.h"
#include "poll.h"
#include "unistd.h"
#include <cstdio>
#include "cstdlib"
#include "iostream"
using namespace std;
int main(){
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int res = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1){
perror("bind");
exit(0);
}
// 3. 监听
res = listen(lfd, 128);
if(res == -1){
perror("listen");
exit(0);
}
// 4. 等待连接 -> 循环
// 检测 -> 读缓冲区, 委托内核去处理
// 数据初始化, 创建自定义的文件描述符集
pollfd fds[1024];
// 初始化
for (int i = 0; i < 1024; ++i) {
fds[i].fd = -1;
fds[i].events = POLLIN; // 检测读事件
}
// 把监听描述符放到0索引
fds[0].fd = lfd;
int maxfd = 0;
while (true){
// 委托内核检测
res = poll(fds, maxfd + 1, -1); // 一直阻塞, 直到检测到
if(res == -1){
perror("poll");
exit(0);
}
// 检测的读缓冲区有变化
// 有新连接, 检测的是监听描述符, 下表为0的那个
if(fds[0].revents & POLLIN){
// 接收连接请求
sockaddr_in cliaddr{};
int len = sizeof(cliaddr);
int cfd = accept(fds[0].fd, (sockaddr*)&cliaddr, (socklen_t*)(&len));
// 输出客户端信息
char ip[32];
cout << "有客户端连接, 客户端ip: "
<< inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip))
<< ", 端口: "
<< ntohs(cliaddr.sin_port)
<< endl;
int i;
for(i = 1; i < 1024; ++i){
if(fds[i].fd == -1){ // 这个下标可用
fds[i].fd = cfd;
break;
}
}
maxfd = max(maxfd, i);
}
// 通信, 有客户端发送数据过来
for (int i = 1; i <= maxfd; ++i) {
if(fds[i].revents & POLLIN){ // 说明读缓冲区有数据
char buf[100];
ssize_t len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}
else if(len == 0){
cout << "客户端断开了连接" << endl;
close(fds[i].fd);
fds[i].fd = -1;
}
else{
buf[len] = '\0';
cout << "客户端说: " << buf << endl;
for(int j = 0; j < len; ++j){
buf[j] = toupper(buf[j]);
}
write(fds[i].fd, buf, len);
}
}
}
}
close(lfd);
return 0;
}
客户端
//
// Created by 47468 on 2024/1/25.
//
#include "arpa/inet.h"
#include "poll.h"
#include "unistd.h"
#include <cstdio>
#include <cstring>
#include "cstdlib"
#include "iostream"
using namespace std;
int main(){
// 1. 创建用于通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(0);
}
// 2. 连接服务器
sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.110.129", &saddr.sin_addr.s_addr);
int res = connect(fd, (sockaddr *) &saddr, sizeof(saddr));
if(res == -1){
perror("connet");
exit(0);
}
// 通信
while(true){
// 读数据
char readBuf[1024];
// 写数据
cout << "请输入要发送的字符串: " << endl;
cin.getline(readBuf, sizeof(readBuf));
// 发送数据到客户端
write(fd, readBuf, strlen(readBuf));
// 接收服务器发送的数据
read(fd, readBuf, sizeof(readBuf));
cout << readBuf << endl;
}
close(fd);
return 0;
}
测试:
服务器:
两个客户端: