poll技术是对select技术进行改进,所以select技术肯定具有缺点
当客户端多了的时候,也就是fd多了的时候,就会出现如下的一系列问题
其中的第四条就是不使用临时 tmp_set 的问题,read_set应该要继续检测的部分被置为0了,就因为这个时候没有数据进来,所以言下之意就是不能重用,每次都需要重置
使用时引头文件
#include
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
- timeout : 阻塞时长
0 : 不阻塞
-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0 : 阻塞的时长,单位是毫秒
- 返回值:
-1 : 失败
>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化
代码的架构和前面的几乎没有区别,只有server.cpp进行了修改,这里只放出server.cpp
select技术的链接:
https://blog.csdn.net/m0_61588837/article/details/132411683?spm=1001.2014.3001.5501
// server.cpp
#include
#include
using namespace std;
#include
#include
#include
#include "Client_Info.h"
#define MAXSIZE 1024
#define MAX_CLIENT_SIZE 1024
#define MAX_POLLFD_SIZE 1025
// 全局存放客户端连接的IP和端口
class Client_Info cli_infos[MAX_CLIENT_SIZE];
// 全局存放需要检测的文件描述符数组
struct pollfd fds[MAX_POLLFD_SIZE];
int bigger(const int& val1, const int& val2) {
return val1 > val2 ? val1 : val2;
}
void Communicate(const int& _index) {
int _connect_fd = fds[_index].fd;
char* _client_ip = cli_infos[_connect_fd].client_ip;
in_port_t& _client_port = cli_infos[_connect_fd].client_port;
char buf[MAXSIZE] = {0};
// 读
bzero(buf, sizeof(buf));
int len = read(_connect_fd, buf, sizeof(buf) - 1);
if (-1 == len) {
perror("read");
exit(-1);
}
if (len > 0)
printf("recv client (ip : %s , port : %d) : %s", _client_ip, _client_port, buf);
else if (0 == len) { // 客户端关闭
printf("client ip : %s , port : %d has closed...\n", _client_ip, _client_port);
// 关闭文件描述符
close(_connect_fd);
// 将对应的文件描述符置为-1
fds[_index].fd = -1;
return;
}
// 写
write(_connect_fd, buf, strlen(buf));
}
int main() {
// 1.创建socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listen_fd) {
perror("socket");
return -1;
}
// 设置一下端口复用
int _optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &_optval, sizeof(_optval));
// 2.绑定IP和端口
struct sockaddr_in server_addr;
// 地址族
server_addr.sin_family = AF_INET;
// IP
server_addr.sin_addr.s_addr = INADDR_ANY;
// 端口
server_addr.sin_port = htons(9999);
int ret = bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (-1 == ret) {
perror("bind");
return -1;
}
printf("server has initialized.\n");
// 3.开始监听
ret = listen(listen_fd, 8);
if (-1 == ret) {
perror("listen");
return -1;
}
// 使用NIO模型,使用poll解决问题
// 初始化检测的文件描述符数组
for (int i = 0; i < MAX_POLLFD_SIZE; ++i) {
fds[i].fd = -1;
fds[i].events = POLLIN; // 表示一会儿要去检测读事件
}
// 加入监听的文件描述符
fds[0].fd = listen_fd;
// 定义最大的文件描述符的fds[]数组的索引
int nfds = 0;
while (1) {
// 调用poll()函数,这是select()函数的改进版本
ret = poll(fds, nfds + 1, -1);
if (-1 == ret) {
perror("select");
return -1;
} else if (0 == ret)
// 为0表示超时并且没有检测到有改变的
continue; // 这里我们的设置因为是阻塞的,所以不会走到这里
else if (ret > 0) {
// 说明检测到了有文件描述符对应缓冲区的数据发生了改变
if (fds[0].revents & POLLIN == POLLIN) {
// 表示有新的客户端连接进来了
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (-1 == connect_fd) {
perror("accept");
return -1;
}
// 获取客户端的信息
char ip[MAX_IPV4_STRING] = {0};
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip));
in_port_t port = ntohs(client_addr.sin_port);
// 打印信息
printf("client ip : %s , port : %d has connected...\n", ip, port);
// 将客户端的信息保存到全局数组中
cli_infos[connect_fd] = Client_Info(ip, port);
// 将新的文件描述符加入到事件中,注意文件描述符的优先用小的机制
for (int i = 1; i < MAX_POLLFD_SIZE; ++i)
if (fds[i].fd == -1) {
fds[i].fd = connect_fd;
fds[i].events = POLLIN;
// 更新nfds
nfds = bigger(nfds, i);
break;
}
}
// 看完监听的文件描述符,看其他的文件描述符是否收到数据
for (int i = 1; i < nfds + 1; ++i) {
if (fds[i].revents & POLLIN == POLLIN)
Communicate(i);
}
}
}
// 4.关闭连接
close(listen_fd);
return 0;
}
首先我们要理解结构体 pollfd 的含义
这是用来保存委托内核检测的文件描述符;委托内核检测的文件描述符的什么事件,比如读写,类似于select中的read_set;还有检测过后实际发生的事件,比如没有读,就修改,类似于select中的 tmp_set;的一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
值得注意的是这些事件的类型和存储方法,是short类型的,我们来看它可以描述哪些事件
其实他和文件属性stat变量里面st_mode(表示文件类型和权限)是一个道理,一个bit位表示一个权限,1表示有,0表示没有,因此添加权限应该用 按位或 | , 这里的事件也是一样的道理,我们一般判断读事件就POLLIN,写事件就POLLOUT
第三个参数就是经过检测之后的状态,可以用它来判断是否有检测到读;由于我们设置的event没有变化,所以相对于select()还是好了很多
其次,我们查看poll()接口的第一个参数是: struct pollfd *fds,需要一个结构体的数组传入进来,每一个元素就封装了一个文件描述符对应的信息,我们从0开始依次记录,如果该元素的fd为-1就表示没有使用,可以存放新的元素,注意这个下标,或者我们称他为索引,索引的值和文件描述符的值是不同的,为了提高效率我们这么设计,在代码中一定要注意,其他的逻辑没什么区别
还有一点,我们看如何判断最后的 revents 检测到读信息
还是前面的思想,每一位对应一个,读对应一位为1,其他为0;当然为什么不是直接相等呢?可能我们设置了其他性质也需要检测,内核处理后还是有了其他的性质为1,我们最好不要冒险,所以这里我们用 &
if (fds[i].revents & POLLIN == POLLIN)
//下面的操作