poll
- 1.poll初始
- 2.poll函數接口
- 3.poll服務器
- 4.poll的優點缺點
點贊👍👍收藏🌟🌟關注💖💖
你的支持是對我最大的鼓勵,我們一起努力吧!😃😃
1.poll初始
poll也是一種linux中多路轉接的方案。它所對應的多路轉接方案主要是解決select兩個問題。
- select的文件描述符有上限的問題
- select每次都要重新設置關心的fd
下面通過poll接口來認識它是怎么解決select的問題的。
2.poll函數接口
struct pollfd * fds:這里可以把它想象一個動態數組、數組或者new/malloc出來的結構體數組
nfds_t nfds:代表這個數組的長度
int timeout:純輸入型,時間單位ms
- 大于0:在timeout以內 阻塞,超過timeout非阻塞返回一次
- 等于0 :非阻塞
- 小于<0:阻塞
這個和select一模一樣的意思。用起來更簡單了。
返回值:同select一模一樣
- 大于0:表示有幾個fd就緒了
- 等于0:表示超時了
- 小于0:表示poll等待失敗了
poll的作用和select一模一樣:只負責等待!
這個struct pollfd 結構體 在傳給poll表示 用戶->內核:
int fd:你要關心一下這個fd哦
short events:關心的是這個fd的什么事件。我們把對應的事件設置進events里
輸入看:fd+events
當poll返回時這個struct pollfd 結構體 表示內核->用戶:
你要關心的fd上面的events中有那些事件已經就緒啦
short revents:就緒事件由revents返回
輸出看:fd+revents
很顯然這種設計解決了這樣的問題:
- 輸入輸出分離!
現在,用戶->內核,內核->用戶,events和revents的分離!以前select就用一張位圖表示不同含義,因為輸入輸出分離了所以決定了poll不需要對參數進行重新設定
events和revents類型是整數,對應的事件如下:
其中對我們來說常用的是POLLIN、POLLOUT、POLLERR ,這些都是大寫的宏每一個占一個比特位,不同比特位表示不同事件。
所以用戶->內核,只要將events設置成要關心的宏值,那么操作系統就幫我們進行關心了。當操作系統返回時只要把revents設置成對應的宏值,不就把那些事件就緒不就告訴我們了嗎。
因為它的類型是short而沒有用操作系統自己封裝的各種各樣的結構體,所以對于事件的設計,我們自己用戶檢測事件有沒有設置或者就緒一定要由我們自己來做,按位與,按位或這樣的操作。
- select等待fd有上限的問題
struct pollfd *fds不是一個數組嗎,nfds_t nfds不就是該數組大小也就是上限嗎,你怎么說poll解決了select等待fd上限的問題?
select是一個具體的數據類型fd_set,既然是一個具體的類型那就直接決定了數據類型大小只能由你的編譯環境自己定,今天不一樣了,因為這個數組由我們自己說的算!
3.poll服務器
前面不是寫了select服務器嗎,現在我們把它改成poll服務器
錯誤碼封裝
#pragma onceenum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};
日志封裝
#pragma once#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"const char* level_to_string(int level)
{switch(level){case DEBUG: return "DEBUG";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";}
}//時間戳變成時間
char* timeChange()
{time_t now=time(nullptr);struct tm* local_time;local_time=localtime(&now);static char time_str[1024];snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \local_time->tm_min, local_time->tm_sec);return time_str;
}void logMessage(int level,const char* format,...)
{//[日志等級] [時間戳/時間] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [創建sock失敗]
#define NUM 1024//獲取時間char* nowtime=timeChange();char logprefix[NUM];snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());//char logconten[NUM];va_list arg;va_start(arg,format);vsnprintf(logconten,sizeof logconten,format,arg);std::cout<<logprefix<<logconten<<std::endl;};
套接字封裝
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "err.hpp"using namespace std;class Sock
{const static int backlog = 32;public:static int sock(){// 1. 創建socket文件套接字對象int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", sock);int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));return sock;}static void Bind(int sock,int port){// 2. bind綁定自己的網絡信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}static void Listen(int sock){// 3. 設置socket 為監聽狀態if (listen(sock, backlog) < 0) {logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}static int Accept(int listensock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr *)&peer, &len);if (sock < 0)logMessage(ERROR, "accept error, next");else{logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}
};
調用邏輯
#include "pollServer.hpp"
#include "err.hpp"
#include <memory>static void usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}string service(string request)
{return request;
}int main(int argc,char* argv[])
{if(argc != 2){usage(argv[0]);exit(USAGG_ERR);}unique_ptr<pollServer> usl(new pollServer(service,atoi(argv[1])));usl->initServer();usl->start();return 0;
今天poll服務器,也是需要一個數組。只不過以前select數組純純的保存文件描述符,poll這里必須是保存struct pollfd結構體的數組。
一般我們如果把fd設置為-1或者小于0的值,操作系統就不會關注這樣的文件描述符了。它只會關心大于等于0的fd。
因此我們要重新定義一個指針,構造析構都跟著改一下
class pollServer
{static const int defaultport = 8080;static const int defaultfd = -1;static const int defaultnum=2048;using func_t=function<string(string)>;public:pollServer(func_t f,int port = defaultport) : _cbs(f),_port(port), _listensock(-1), _rfds(nullptr){}~pollServer(){if (_listensock != defaultfd)close(_listensock);if (_rfds)delete[] _rfds;}private:int _listensock;int _port;struct pollfd* _rfds;func_t _cbs;
};
接下來初始化服務器這里創建結構體數組大小自己隨意定
void initServer()
{// 1.創建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_rfds=new struct pollfd[defaultnum];//大小這里自己隨便定for (int i = 0; i < defaultnum; ++i)//數組初始化{_rfds[i].fd = defaultfd;_rfds[i].events=0;_rfds[i].revents=0;}_rfds[0].fd = _listensock; // 這個位置后面就不變了_rfds[0].events=POLLIN; //告訴內核幫我關心_listensock讀事件
}
打印這里也改一下
void print()
{for (int i = 0; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)cout << _rfds[i].fd << " ";}cout << endl;
}
現在當我們啟動服務之后,就不需要每次調用select之前都需要重新設置fd了添加到讀文件描述符集里面了,然后才能添加到select里面。現在直接把數組給poll。所以能明顯感覺到poll比select簡單
void start()
{int timenout=1000;for (;;){int n=poll(_rfds,defaultnum,timenout);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));break;default:// 說明有事件就緒了,目前只有一個監聽事件就緒了logMessage(NORMAL, "have event ready!");HandlerEvent();//這里不用傳了,因為就緒事件就在_rfds里break;}}
}
今天這里我們只處理讀事件就緒的情況
// 1.handler event _rfds 中,不僅僅是有一個fd是就緒的,可能存在多個
// 2.我們的poll目前只處理了read事件
void HandlerEvent()
{// 你怎么知道那些fd就緒了呢? 我不知道,我只能遍歷for (int i = 0; i < defaultnum; ++i){// 不合法fdif (_rfds[i].fd == defaultfd)continue;// 合法fd,但必須曾經向內核設置過幫我關心對應fd讀事件才能往下走if (!(_rfds[i].events & POLLIN))continue;if (_rfds[i].fd == _listensock && _rfds[i].revents & POLLIN)Accepter(_listensock);else if (_rfds[i].revents & POLLIN)Recver(i);}
處理_listensock讀就緒事件
void Accepter(int listensock)
{logMessage(DEBUG, "Accepter in");// 走到這里,accept 函數,會不會被阻塞?// 走到這里就是, poll 告送我,_listensock就緒了,然后才能執行下面代碼string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport); accept = 等 + 獲取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一個sock套接字后,然后我們可以直接進行read/recv嗎? 不能,整個代碼只有poll有資格檢測事件是否就緒// 將新的sock 托管給poll!// 將新的sock,托管給poll的本質,其實就是將sock,添加到_rfds數組里!int i = 0;for (; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)continue;elsebreak;}if (i == defaultnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_rfds[i].fd = sock;_rfds[i].events = POLLIN;_rfds[i].revents = 0;}print();logMessage(DEBUG, "Accepter out");
}
處理普通sock讀就緒事件
void ResetItem(int i)
{_rfds[i].fd = defaultfd;_rfds[i].events = 0;_rfds[i].revents = 0;
}void Recver(int pos)
{logMessage(DEBUG, "in Recver");// 1. 讀取request// 這樣讀取是有問題的!char buffer[1024];ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 這里在進行讀取的時候,會不會被阻塞?if (s > 0) // 讀取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) // 對方關閉了文件描述符{close(_rfds[pos].fd);ResetItem(pos);logMessage(NORMAL, "client quit");return;}else // 讀取失敗{close(_rfds[pos].fd);ResetItem(pos);logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 處理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(_rfds[pos].fd, response.c_str(), response.size());logMessage(DEBUG, "out Recver");
}
自此poll服務器就已經寫完了,很顯然poll服務器主體代碼和select服務器一模一樣,只不過poll在進行事件監聽的時候明顯要比select簡潔,而且數組沒有上限。
poll服務器完整代碼
#pragma once#include <iostream>
#include <functional>
#include <poll.h>
#include "sock.hpp"using namespace std;class pollServer
{static const int defaultport = 8080;static const int defaultfd = -1;static const int defaultnum = 2048;using func_t = function<string(string)>;public:pollServer(func_t f, int port = defaultport) : _cbs(f), _port(port), _listensock(-1), _rfds(nullptr){}void initServer(){// 1.創建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_rfds = new struct pollfd[defaultnum]; // 大小這里自己隨便定for (int i = 0; i < defaultnum; ++i) ResetItem(i);_rfds[0].fd = _listensock; // 這個位置后面就不變了_rfds[0].events = POLLIN; // 告訴內核幫我關心_listensock讀事件}void print(){for (int i = 0; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)cout << _rfds[i].fd << " ";}cout << endl;}void Accepter(int listensock){logMessage(DEBUG, "Accepter in");// 走到這里,accept 函數,會不會被阻塞?// 走到這里就是, poll 告送我,_listensock就緒了,然后才能執行下面代碼string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport); accept = 等 + 獲取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一個sock套接字后,然后我們可以直接進行read/recv嗎? 不能,整個代碼只有poll有資格檢測事件是否就緒// 將新的sock 托管給poll!// 將新的sock,托管給select的本質,其實就是將sock,添加到fdarray數組里!int i = 0;for (; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)continue;elsebreak;}if (i == defaultnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_rfds[i].fd = sock;_rfds[i].events = POLLIN;_rfds[i].revents = 0;}print();logMessage(DEBUG, "Accepter out");}void ResetItem(int i){_rfds[i].fd = defaultfd;_rfds[i].events = 0;_rfds[i].revents = 0;}void Recver(int pos){logMessage(DEBUG, "in Recver");// 1. 讀取request// 這樣讀取是有問題的!char buffer[1024];ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 這里在進行讀取的時候,會不會被阻塞?if (s > 0) // 讀取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) // 對方關閉了文件描述符{close(_rfds[pos].fd);ResetItem(pos);logMessage(NORMAL, "client quit");return;}else // 讀取失敗{close(_rfds[pos].fd);ResetItem(pos);logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 處理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(_rfds[pos].fd, response.c_str(), response.size());logMessage(DEBUG, "out Recver");}// 1.handler event _rfds 中,不僅僅是有一個fd是就緒的,可能存在多個// 2.我們的poll目前只處理了read事件void HandlerEvent(){// 你怎么知道那些fd就緒了呢? 我不知道,我只能遍歷for (int i = 0; i < defaultnum; ++i){// 不合法fdif (_rfds[i].fd == defaultfd)continue;// 合法fd,但必須曾經向內核設置過幫我關心對應fd讀事件才能往下走if (!(_rfds[i].events & POLLIN))continue;if (_rfds[i].fd == _listensock && _rfds[i].revents & POLLIN)Accepter(_listensock);else if (_rfds[i].revents & POLLIN)Recver(i);}}void start(){int timenout = -1;for (;;){int n = poll(_rfds, defaultnum, timenout);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));break;default:// 說明有事件就緒了,目前只有一個監聽事件就緒了logMessage(NORMAL, "have event ready!");HandlerEvent(); // 這里不用傳了,因為就緒事件就在_rfds里break;}}}~pollServer(){if (_listensock != defaultfd)close(_listensock);if (_rfds)delete[] _rfds;}private:int _listensock;int _port;struct pollfd *_rfds;func_t _cbs;
};
4.poll的優點缺點
poll的優點就不用過多介紹,輸入輸出分離,而且沒有select上限的問題
poll的主要缺點依舊是遍歷問題,因為我們交給poll多個文件描述符,poll在底層去遍歷去查找。隨著等待的文件描述符變多,poll要線性遍歷的方式檢測所有文件描述符,這勢必會帶來效率的降低 。
正是因為poll有這樣的問題,所有才有了下一個多路轉接之epoll