poll也是
一种多路转接的方案 ,poll
对select的缺点进行了改进 :
-
解决了select 可以检测的文件描述符有上限的问题 -
把用户告知OS要检测的文件描述符和OS告知哪些文件描述符上有事件就绪的输入输出型参数进行了分离 也就是说,保存要检测文件描述符的数据结构,不会在OS返回时被覆盖了 ,在轮询调用poll时,不再需要对检测文件描述符的数据结构再进行重新设置 了,也就意味着用户不再需要自己创建数据结构来维护要检测文件描述符了
poll接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- fds:一个poll函数检测的结构列表. 每一个元素中, 包含了三部分内容:
文件描述符 , 监听的事件集合 , 返回的事件集合 - nfds:表示fds数组的长度
- timeout:poll函数的超时时间, 单位是毫秒(ms)
返回值:
- 返回0:表示超出设置的timeout时间后还没有文件描述符就绪
- 返回-1:发生错误
- 返回大于0的值:表示poll由于监听的文件描述符就绪而返回
pollfd结构
- fd:要检测的文件描述符
- events:需要OS检测的事件的就绪类型
- revents:OS返回的事件就绪类型
events和revents的取值:
timeout决定poll的等待策略
- timeout设为0:表示非阻塞轮询
- timeout设为大于0的值:表示阻塞多少ms后返回,轮询检测
- timeout设为-1:表示阻塞等待
poll TCP服务器执行流程
- 首先
创建TCP套接字 ,绑定端口号 ,对TCP套接字进行Listen ,这时就会得到一个监听套接字listen_sock - 然后创建
pollfd 结构体数组rfds[],并对rfds进行清空 ,把对listen_sock 的读事件监听添加到数组的第一个元素 - 接着进行阻塞轮询,根据
不同的poll返回值 ,分为三种程序走向:
- 返回值为0:timeout超时,继续轮询检测
- 返回值为1:出错返回,继续轮询
- 返回值大于0的值:说明这时有事件就绪或者有新连接到来
- 如果poll返回值大于0,就把
rfds[]中元素的revents&POLLIN 来筛选读事件就绪的文件描述符
- 如果是listen_sock就绪,则说明有新连接到来,这时就可以
对连接进行accept ,此时进行accept不会被阻塞,accept之后,把新获取的文件描述符添加到rfds[]中的未使用位置,并检测读事件 - 如果是别的文件描述符就绪,就是读事件就绪了,也就是有客户端发来了数据,这时就能对数据进行read,这时的read也不会阻塞
PollServer实现
-
封装套接字,这里的封装套接字和select一样 -
按照上面的执行流程编写PollServer
PollServer.hpp
#pragma once
#include "socket.hpp"
#include <poll.h>
#define BACKLOG 5
#define DFL_FD -1
#define NUM 64
namespace ns_poll {
class PollServer {
private:
int port;
int listen_sock;
public:
PollServer(int _port) :port(_port) {}
void InitServer() {
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock, port);
ns_sock::Sock::Listen(listen_sock, BACKLOG);
}
void Run() {
std::cout << "服务器启动" << std::endl;
struct pollfd rfds[64];
for (int i = 0; i < 64; ++i) {
rfds[i].fd = DFL_FD;
rfds[i].events = 0;
rfds[i].revents = 0;
}
rfds[0].fd = listen_sock;
rfds[0].events |= POLLIN;
rfds[0].revents = 0;
while (true) {
switch (poll(rfds, 64, -1)) {
case 0:
std::cout << "timeout!!!" << std::endl;
break;
case -1:
std::cerr << "poll error!" << std::endl;
break;
default:
HanderEnent(rfds, NUM);
break;
}
}
}
~PollServer() {}
private:
void HanderEnent(struct pollfd rfds[], int num) {
for (int i = 0; i < num; ++i) {
if (rfds[i].fd == DFL_FD) {
continue;
}
if (rfds[i].revents & POLLIN) {
if (rfds[i].fd == listen_sock) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0) {
std::cerr << "获取连接错误" << std::endl;
continue;
}
uint16_t peer_port = htons(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
std::cout << "有一个新连接 " << "ip :[" << peer_ip << "] port: [" << peer_port << "]" << std::endl;
if (!AddFdToPollFd(sock, rfds, NUM)) {
std::cerr << "添加失败" << std::endl;
close(sock);
}
}
else {
char buffer[1024];
ssize_t size = recv(rfds[i].fd, buffer, sizeof(buffer) - 1, 0);
if (size > 0) {
buffer[size] = 0;
std::cout << "echo# " << buffer;
}
else if (size == 0) {
std::cout << "客户端断开连接" << std::endl;
close(rfds[i].fd);
rfds[i].fd = DFL_FD;
}
else {
std::cerr << "读取出错!!!" << std::endl;
close(rfds[i].fd);
rfds[i].fd = DFL_FD;
}
}
}
}
}
bool AddFdToPollFd(int sock, struct pollfd rfds[], int num) {
for (int i = 0; i < num; ++i) {
if (rfds[i].fd == DFL_FD) {
rfds[i].fd = sock;
rfds[i].events |= POLLIN;
rfds[i].revents = 0;
return true;
}
}
return false;
}
};
}
#include "poll_server.hpp"
#include <string>
static void Usage(std::string proc){
std::cerr << "Usage :" << "\n\t" << proc <<" port "<<std::endl;
}
int main(int argc, char* argv[]){
if(argc != 2){
Usage(argv[0]);
exit(4);
}
ns_poll::PollServer* pollsvr = new ns_poll::PollServer(atoi(argv[1]));
pollsvr->InitServer();
pollsvr->Run();
}
poll的优点
pollfd 结构包含了要监视的event和发生的revent,不再使用select“参数-值”传递的方式. 接口使用比select更方便- poll并没有检测文件描述符最大数量限制 (但是数量过大后性能也是会下降)
poll缺点
poll中监听的文件描述符数目增多时
- 和select函数一样,poll返回后,需要
轮询pollfd来获取就绪的描述符 ,遍历有开销 - 每次调用poll都
需要把大量的pollfd结构从用户态拷贝到内核中 - 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降
|