Linux网络 | 多路转接Poll
前言:本节内容主要讲述多路转接的Poll, 这是我们讲解的多路转接中的第二个解决方案。 Poll主要由select修改而来, 没有select和后面的Epoll重要, 但是友友们还是要认真学习哦!现在废话不多说,开始我们的学习吧!
ps:本节内容之前最好看一下select的知识点哦!
目录
POLL
Poll相较于Select的优点
Pollfd
POLL代码实现
准备文件
Poll_server.hpp
初始化
Start
POLL
Poll相较于Select的优点
其实, select是有一些缺点的。 这些缺点导致当select等待的就绪的文件描述符越来越多时, 其实效率不会一直提高下去, 是一定会有一个临界点的。 下面就是select的一些缺点:
- 1.等待的fd是有上限的。(因为它是使用位图的方式将要关心的文件描述符交给内核。并且位图对应的类型也是固定的。所以select就势必等待的fd是有上限的)
- 2.输入输出型参数比较多,数据拷贝的频率比较高.
- 3. 输入输出型参数比较多,每一次都要对关心的fd事件进行重置.
- 4、使用第三方数组管理用户的fd,用户层需要很多次遍历。内核中检测fd事件就绪,也要遍历.
为了修正select的问题,就有了下一种解决方案: poll。
poll只负责等待,在select的基础上,解决select的两个硬伤:等待fd有上限的问题和输入输出型参数比较多,每次都要对关心的fd进行事件重置的问题。首先返回值,大于零代表有几个文件描述符就绪,等于零代表超时了,小于零代表失败。
第三个参数使用的timeout整形,单位是毫秒。设为1000,就是1秒timeout一次。
重要的是第一个参数,是结构体。第二个参数是nfds_t。pollfd我们可以理解为一个数组,第二个参数可以理解为这个数组中一共有多少个元素。
Pollfd
这个结构体的意思就是,当我们用户向内核交付的时候,就是关心fd的events这个事件。 返回的时候,内核就告诉用户,fd的revents这个事件就绪了。
所以, pollfd将输入和输出事件进行了分离。所以,未来要关心多个文件描述符,就传过去多个包含文件描述符的pollfd对象的数组就可以了。以后只需要利用nfds遍历pollfd,检测哪些就绪。
这里的short类型如何表示事件,其实就是位图的原理,利用比特位进行标记。对应比特位代表不同的事件,下面是对应事件的宏:
事件 | 描述 | 可作为输入? | 可作为输出? |
POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
POLLRDNORN | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读 | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭, 或者对方关闭了写操作。它由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起。比如管道的写端被关闭后, 读端描述符上将收到POLLHUP事件 | 否 | 是 |
POLLNYAL | 文件描述符没有打开 | 否 | 是 |
POLL代码实现
准备文件
Poll的文件类似于Select:
由于上一节已经介绍了相关文件, 所以本节不再赘述。
Poll_server.hpp
先看整体类定义:
#pragma once
#include <iostream>
using namespace std;
#include "Log.hpp"
#include "Socket.hpp"
#include<poll.h>
#include <sys/time.h>
//以上,需要用到的头文件
int defaultport = 8080; //设置默认端口号
static const int fd_num_max = 64; // 设置最大fd的个数默认值
int defaultfd = -1 // 辅助数组默认初始值
int non_event = 0; //一开始没有事件的时候默认设置成零
class PollServer
{
public:
PollServer
{
}
~PollServer()
{
}
bool Init()
{
}
//
void Accepter()
{
}
void Recver(int fd, int i)
{
}
//
void Dispatcher()
{
}
//
void Start()
{
}
void PrintFd()
{
}
private:
Socket listensock_;
uint16_t port_;
pollfd _event_fds[fd_num_max]; //这里可以使用vector
// int fd_array[fd_num_max];
};
一共有三个成员变量, 一个listensock_用来监听新连接。 一个port_设置端口号。 一个_event_fds就是我们关心的fd。是pollfd类型的。
初始化
初始化, 先将pollfd数组里面都设置成默认值, 利用定义的全局变量defaultfd和non_event设置。然后启动服务器开始监听。
PollServer(uint16_t port = defaultport)
: port_(port)
{
// 初始化pollfd数组
for (int i = 0; i < fd_num_max; i++)
{
_event_fds[i].fd = defaultfd;
_event_fds[i].events = non_event;
_event_fds[i].revents = non_event;
// cout << "fd_array[" << i << "]" << " : " << fd_array[i] << endl;
}
}
~PollServer()
{
listensock_.Close();
}
bool Init()
{
listensock_.InitSocket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
Start
服务器运行的时候, 就是循环式的检查_event_fds数组里面有没有需要关心的fd事件就绪了。 如果有, 那么就进入Dispatcher进行事件派发。如下为Start:
void Start()
{
_event_fds[0].fd = listensock_.Fd();
_event_fds[0].events = POLLIN;//新连接到来,事件类型等于读事件就绪。 所以这里设置成POLLIN, 表示读事件就绪。
int timeout = 3000;
for (;;)
{
int n = poll(_event_fds, fd_num_max, timeout); //第一个数组元素, 第二个数组元素个数
switch (n)
{
case 0:
cout << "time out, timeout: " << endl;
break;
case -1:
cerr << "poll error" << endl;
break;
default:
// 有时间就绪了,TOOD
cout << "get a link!!!" << endl;
Dispatcher(); //事件派发
break;
}
}
}
事件派发就是因为有事件就绪, 但是我们并不能直接定位数组中哪个元素的fd就绪了, 所以就要循环检测。 如果检测的时候检测到fd的revents是POLLIN, 就是就绪了。 那么就可以根据不同情况分发不同事件了。 有两种情况:一种是fd和监听fd相同,说明来了新连接。 就要进入连接管理器。 另一种就是来信息了, 读取就行了。
void Dispatcher()
{
for (int i = 0; i < fd_num_max; i++)
{
int fd = _event_fds[i].fd; //得到合法文件描述符
if (fd == defaultfd) continue; //这个文件描述符不关心
//然后根据rfds判断是否fd就绪
if (_event_fds[i].revents & POLLIN) //判断就绪
{
if (fd == listensock_.Fd()) //如果fd就是当前listensock_的fd, 说明有新连接岛链, 就链接。
{
Accepter(); //链接管理器
}
else
{
Recver(fd, i); //读取管理器
}
}
}
}
连接管理器进行的操作分两步。 第一步:连接。第二步:将fd封装成pollfd类型对象加入到_event_fds数组中。 下面就是这个流程:
void Accepter()
{
string clientip;
uint16_t clientport = 0;
int sock = listensock_.Accept(&clientip, &clientport); //此时不会阻塞在这里, 因为新连接到来了我们才accept, 而不是先accept等待新连接到来。
if (sock < 0) return;
lg(Info, "accept success, %s: %d, sock fd : %d", clientip.c_str(), clientport, sock); //链接成功
//链接成功之后, 就是添加新需要关心的fd进入数组中。
int pos = 1;
for (;pos < fd_num_max; pos++)
{
if (_event_fds[pos].fd != defaultfd) continue;
else break;
}
if (pos == fd_num_max) //如果这个条件成立, 就说明pos加到了fd_num_max,而不是遇到了defaultfd。 就说明满了, 就把监听sock关掉。
{
lg(Waring, "server is full, close %d now!", sock);
close(sock);
//这里可以扩容
}
else //遇到了defaultfd, 没满
{
_event_fds[pos].fd = sock; //将defaultfd的位置设置为新连接的fd。
_event_fds[pos].events = POLLIN;
_event_fds[pos].revents = non_event;
PrintFd();
}
}
读取管理器就是读取接收缓冲区里面的内容即可。
void Recver(int fd, int i)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
cout << "get a message: " << buffer << endl;
}
else if (n == 0) //等待超时,
{
lg(Info, "client quit, me too, close fd is : %d", fd);
close(fd);
_event_fds[i].fd = defaultfd;
}
else
{
lg(Waring, "recv error: fd is : %d", fd);
close(fd);
_event_fds[i].fd = defaultfd;
}
}
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!