✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
[1、I/O 多路转接之 poll](#1、I/O 多路转接之 poll)
[1.2、poll 函数接口](#1.2、poll 函数接口)
[1.3、poll 的优点](#1.3、poll 的优点)
[1.4、poll 的缺点](#1.4、poll 的缺点)
1、I/O 多路转接之 poll
select缺点
上一弹我们知道了select有四个缺点 ,poll能够解决select中的两个缺点!
1.1、初识poll
poll函数用于监视多个文件描述符以查看它们是否有 I/O(输入/输出)活动。
作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!定位:只负责进行等,等就绪事件派发!
1.2、poll 函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数:
fds
: 指向pollfd
结构体数组的指针,每个结构体指定一个要监视的文件描述符及其感兴趣的事件。nfds
: 数组fds
中的元素数量,即要监视的文件描述符数量。timeout
: 超时时间(毫秒)。- 如果为**-1**, 则
poll
将阻塞直到某个文件描述符准备好; - 如果为 0, 则
poll
立即返回,检查是否有文件描述符准备好,而不阻塞; - 如果为正数,则
poll
将阻塞指定的毫秒数。
- 如果为**-1**, 则
返回值:
- 成功 时,返回准备就绪的文件描述符数量(
revents
不为零的pollfd
结构体的数量)。- 返回值等于 0, 表示 poll 函数等待超时;
- 返回值大于 0, 表示 poll 由于监听的文件描述符就绪而返回.
- 失败 时,返回 -1,并设置
errno
以指示错误。
events 和 revents 的取值:
1.3、poll 的优点
不同于 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现.
- pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select"参数-值"传递的方式. 接口使用比 select 更方便.
- poll 并没有最大数量限制 (但是数量过大后性能也是会下降).
1.4、poll 的缺点
poll 中监听的文件描述符数目增多时
- 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符.
- 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中.
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
1.5、代码演示
此处对上一弹select版本的多路转接进行修改!
1.5.1、主函数
老规矩,根据主函数反向实现类和成员函数!
// ./poll_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen(); // 开启日志
std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
svr->InitServer();
svr->Loop();
return 0;
}
1.5.2、PollServer类
PollServer类的成员变量与SelectServer类的成员变量基本一致,但是此处的数组(存放fd)类型是struct pollfd,还需要端口号和套接字!
基本结构
class PollServer
{
const static int gnum = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
public:
PollServer(uint16_t port);
void InitServer();
// 处理新链接
void Accepter();
// 处理普通fd就绪
void HandlerIO(int i);
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent();
void Loop();
void PrintDebug();
~PollServer();
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
struct pollfd fd_events[gnum];
};
构造析构函数
构造函数****初始化端口号并根据端口号创建监听套接字对象, 析构函数****暂时不做处理!
PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildListenSocket(_port);
}
~PollServer()
{}
InitServer()
InitServer()函数将结构体类型的数组fd成员设置为默认fd,其他两个事件先设置为0,并将listensockfd添加到结构体数组的第一个元素的fd成员,并将events事件设置为读!
void InitServer()
{
for (int i = 0; i < gnum; i++)
{
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
fd_events[0].fd = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
fd_events[0].events = POLLIN;
}
Loop()
Loop()函数调用poll系统调用,根据返回值执行对应的操作:
1、返回值为0 :打印超时日志,并退出循环
2、返回值为-1 :打印出错日志,并退出循环
3、返回值大于0 :处理事件
void Loop()
{
while (true)
{
int timeout = 1000;
int n = ::poll(fd_events,gnum,timeout);
switch (n)
{
case 0:
LOG(DEBUG, "time out\n");
break;
case -1:
LOG(ERROR, "poll error\n");
break;
default:
// 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
HandlerEvent();
PrintDebug();
// sleep(1);
break;
}
}
}
注意:Loop函数中的timeout变量后序会用于测试!
HandlerEvent()
在执行HandlerEvent()函数之前,赋值数组中一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd ,此处主要分以下两步:
- 1、判断fd是否合法
- 2、判断fd是否就绪
- 2.1、就绪是listensockfd
- 2.1.1、调用Accepter()处理新链接函数
- 2.2、就绪是normal sockfd
- 2.2.1、调用HandlerIO()处理普通fd就绪函数
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent()
{
// 事件派发
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_events[i].fd == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (fd_events[i].revents & POLLIN)
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_events[i].fd)
{
// listensockfd
// 链接事件就绪,等价于读事件就绪
Accepter();
}
else
{
// normal sockfd,正常的读写
HandlerIO(i);
}
}
}
}
Accepter()
Accepter()函数处理新链接 ,主要分为以下三步:
- 1、获取链接
- 2、获取链接成功将新的fd 和 读事件 添加到数组中
- 3、数组满了,需关闭sockfd,此处可以扩容并再次添加新的fd和事件
// 处理新链接
void Accepter()
{
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if (sockfd > 0)
{
LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 只要将新的fd添加到fd_events中即可!
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (fd_events[pos].fd == gdefaultfd)
{
flag = true;
fd_events[pos].fd = sockfd; // 把新的fd放入数组中
fd_events[pos].events = POLLIN;
LOG(INFO, "add %d to fd_array success \n", sockfd);
break;
}
}
// 数组满了
if (!flag)
{
LOG(WARNING, "Server Is Full\n");
::close(sockfd);
// 扩容
// 添加
}
}
}
HandlerIO()
HandlerIO()函数处理普通fd情况 ,直接读取文件描述符中的数据,根据recv()函数的返回值做出不一样的决策,主要分为以下三种情况:
1、返回值大于0,读取文件描述符中的数据,并使用send()函数做出回应!
2、返回值等于0,读到文件结尾,打印客户端退出的日志,关闭文件描述符,并将该下标的文件描述符设置为默认fd,事件都设置为0
3、返回值小于0,读取文件错误,打印接受失败的日志,然后同上!
注意:此处的读取是有问题的,因为正确的读取是有协议的,此处是直接读取!
// 处理普通fd就绪
void HandlerIO(int i)
{
char buffer[1024];
ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string content = "<html><body><h1>hello linux</h1></body></html>";
std::string echo_str = "HTTP/1.0 200 OK\r\n";
echo_str += "Content-Type: text/html\r\n";
echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
echo_str += content;
::send(fd_events[i].fd, echo_str.c_str(), echo_str.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client quit...\n");
// 关闭fd
::close(fd_events[i].fd);
// select 不再关心这个fd了
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
else
{
LOG(ERROR, "recv error\n");
// 关闭fd
::close(fd_events[i].fd);
// select 不再关心这个fd了
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
}
PrintDebug()
PrintDebug()遍历数组,将合法的文件描述符打印出来!
void PrintDebug()
{
std::cout << "fd list: ";
for (int i = 0; i < gnum; i++)
{
if (fd_events[i].fd == gdefaultfd)
continue;
std::cout << fd_events[i].fd << " ";
}
std::cout << "\n";
}
1.5.3、运行结果
Loop()函数中timeout = 1000时,即1000毫秒,即1秒等待一次!
Loop()函数中timeout = 0时,即1000毫秒,即非阻塞等待!
Loop()函数中timeout = -1时,即阻塞等待!
1.6、完整代码
1.6.1、Main.cc
#include <iostream>
#include <memory>
#include "PollServer.hpp"
// ./poll_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen(); // 开启日志
std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
svr->InitServer();
svr->Loop();
return 0;
}
1.6.2、Makefile
poll_server:Main.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf poll_server
1.6.3、PollServer.hpp
#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace socket_ns;
class PollServer
{
const static int gnum = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
public:
PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildListenSocket(_port);
}
void InitServer()
{
for (int i = 0; i < gnum; i++)
{
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
fd_events[0].fd = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
fd_events[0].events = POLLIN;
}
// 处理新链接
void Accepter()
{
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if (sockfd > 0)
{
LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 只要将新的fd添加到fd_events中即可!
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (fd_events[pos].fd == gdefaultfd)
{
flag = true;
fd_events[pos].fd = sockfd; // 把新的fd放入数组中
fd_events[pos].events = POLLIN;
LOG(INFO, "add %d to fd_array success \n", sockfd);
break;
}
}
// 数组满了
if (!flag)
{
LOG(WARNING, "Server Is Full\n");
::close(sockfd);
// 扩容
// 添加
}
}
}
// 处理普通fd就绪
void HandlerIO(int i)
{
char buffer[1024];
ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string content = "<html><body><h1>hello linux</h1></body></html>";
std::string echo_str = "HTTP/1.0 200 OK\r\n";
echo_str += "Content-Type: text/html\r\n";
echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
echo_str += content;
::send(fd_events[i].fd, echo_str.c_str(), echo_str.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client quit...\n");
// 关闭fd
::close(fd_events[i].fd);
// select 不再关心这个fd了
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
else
{
LOG(ERROR, "recv error\n");
// 关闭fd
::close(fd_events[i].fd);
// select 不再关心这个fd了
fd_events[i].fd = gdefaultfd;
fd_events[i].events = 0;
fd_events[i].revents = 0;
}
}
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent()
{
// 事件派发
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_events[i].fd == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (fd_events[i].revents & POLLIN)
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_events[i].fd)
{
// listensockfd
// 链接事件就绪,等价于读事件就绪
Accepter();
}
else
{
// normal sockfd,正常的读写
HandlerIO(i);
}
}
}
}
void Loop()
{
while (true)
{
int timeout = -1;
// int timeout = 0;
// int timeout = 1000;
int n = ::poll(fd_events,gnum,timeout);
switch (n)
{
case 0:
LOG(DEBUG, "time out\n");
break;
case -1:
LOG(ERROR, "poll error\n");
break;
default:
// 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
HandlerEvent();
PrintDebug();
// sleep(1);
break;
}
}
}
void PrintDebug()
{
std::cout << "fd list: ";
for (int i = 0; i < gnum; i++)
{
if (fd_events[i].fd == gdefaultfd)
continue;
std::cout << fd_events[i].fd << " ";
}
std::cout << "\n";
}
~PollServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
struct pollfd fd_events[gnum];
};