🔥個人主頁🔥:孤寂大仙V
🌈收錄專欄🌈:計算機網絡
🌹往期回顧🌹:【計算機網絡】非阻塞IO——select實現多路轉接
🔖流水不爭,爭的是滔滔不息
一、poll實現多路轉接
在網絡編程或多路 IO 編程中,我們經常需要同時監聽多個文件描述符(fd),比如多個客戶端的 socket 連接。這時,poll 就登場了。
poll 是 Linux 提供的一種 IO 多路復用機制,用于監聽多個 fd 上的讀寫等事件,一旦有就緒,立刻通知我們處理。
poll的參數
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
- 參數
struct pollfd fds[]
是關心的文件描述符數組,每一個元素代表一個要監聽的 fd 及其感興趣的事件和返回的事件。
struct pollfd {int fd; // 要監聽的文件描述符short events; // 感興趣的事件(由你設置)short revents; // 實際返回的事件(由內核設置)
};
- 參數
nfds_t nfds
這個是 fds[] 數組里有效元素的數量,簡單說就是你監聽幾個文件描述符就寫幾。 - int timeout
單位是 毫秒(ms),表示阻塞多久。timeout > 0:等待指定毫秒數后返回、timeout == 0:立即返回(非阻塞)、timeout < 0:永遠阻塞,直到有事件發生。
函數返回值
- 0:就緒的文件描述符數量
- 0:超時,沒有任何事件發生
- <0:出錯(比如信號中斷)
常用事件(events/revents)取值
POLLIN // 有數據可讀
POLLOUT // 可以寫數據而不會阻塞
POLLERR // 錯誤(由 revents 設置)
POLLHUP // 對端關閉連接
POLLNVAL // 描述符非法
poll的缺點
- 每次調用都要傳入整個 fd 列表
poll 的 pollfd 是個數組,內核不會保存狀態,每次都得重新傳。
如果你有上千個連接,那每次 poll() 都會把這上千個 fd 重新復制到內核 → 代價大。 - 線性掃描效率低
poll 返回的是“多少個 fd 就緒”,但你要線性掃描整個數組找出來。
比如你監聽 1000 個連接,但只有 1 個可讀,你得從 0 掃到 999 才找到它。 - fd 數量仍有限制
雖然 poll 相比 select 不再限制 1024 個,但:
它還是受限于內核最大文件描述符數量(ulimit -n,比如 65535)
每個 pollfd 占用空間較大,更不適合極大并發 - 無事件通知機制
poll 只能“等和掃”,沒有像 epoll 的 EPOLLONESHOT、EPOLLET(邊緣觸發)那種高級機制。
沒法在事件處理完后說“我暫時不關注這個 fd 了”。
二、poll實現非阻塞服務器
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "Socket.hpp"
#include <sys/poll.h>using namespace std;
using namespace LogModule;
using namespace SocketModule;class PollServer
{const static int size = 4096;const static int defaultfd = -1;public:PollServer(int port): _isrunning(false), _listensockfd(make_unique<TcpSocket>()){_listensockfd->BuildTcpSocketServer(port); // 構造TCP服務器for (int i = 0; i < size; i++){_fds[i].fd = defaultfd;_fds[i].events = 0;_fds[i].revents = 0;}_fds[0].fd = _listensockfd->FD();_fds[0].events = POLLIN;}void Start() // 服務器啟動{int timeout = 1000; // 1000毫秒_isrunning = true;while (true){int n = poll(_fds, size, timeout); // 多路轉接只關系讀事件switch (n){case -1:LOG(LogLevel::ERROR) << "poll error"; // 異常break;case 0:LOG(LogLevel::WARNING) << "poll timeout"; // 超時default:LOG(LogLevel::INFO) << "事件就緒"; // 讀事件就緒Dispatcher(); // 派發break;}}}void Dispatcher() // 事件派發{for (int i = 0; i < size; i++){if (_fds[i].fd == defaultfd) // 跳過continue;if (_fds[i].revents & POLLIN) // 是讀就緒{if (_fds[i].fd == _listensockfd->FD()){// listen 套接字Accept();}else{// 普通 套接字Recv(i);}}}}void Accept(){InetAddr client;int sockfd = _listensockfd->AcceptOrDie(&client);LOG(LogLevel::DEBUG) << "accept a new client" << client.StringAddr();int pos = 0;for (; pos < size; pos++){if (_fds[pos].fd == defaultfd)break; // 數組中找到空位}if (pos == size){LOG(LogLevel::WARNING) << "poll server full";close(sockfd);}else{_fds[pos].fd = sockfd;_fds[pos].events = POLLIN;_fds[pos].revents = 0;}}void Recv(int pos) // 讀數據{char buffer[1024];ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 收信息if (n > 0){buffer[n] = 0;cout << "client say@ " << buffer << endl;}else if (n == 0) // 客戶端退出{LOG(LogLevel::INFO) << "client quit";_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;close(_fds[pos].fd);}else // 出現錯誤 異常{LOG(LogLevel::FATAL) << "recv error";_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;close(_fds[pos].fd);}}~PollServer(){}private:unique_ptr<Socket> _listensockfd;bool _isrunning;struct pollfd _fds[size];
};
構造
//私有成員變量
private:unique_ptr<Socket> _listensockfd;bool _isrunning;struct pollfd _fds[size];
//構造PollServer(int port): _isrunning(false), _listensockfd(make_unique<TcpSocket>()){_listensockfd->BuildTcpSocketServer(port); // 構造TCP服務器for (int i = 0; i < size; i++){_fds[i].fd = defaultfd;_fds[i].events = 0;_fds[i].revents = 0;}_fds[0].fd = _listensockfd->FD();_fds[0].events = POLLIN;}
私有成員變量中,把關心文件描述符的數組開好,構造服務器的時候遍歷這個數組,把要監聽的文件描述符設置進去,把要關系的狀態設置為關系讀事件。
服務器啟動
void Start() // 服務器啟動{int timeout = 1000; // 1000毫秒_isrunning = true;while (true){int n = poll(_fds, size, timeout); // 多路轉接只關系讀事件switch (n){case -1:LOG(LogLevel::ERROR) << "poll error"; // 異常break;case 0:LOG(LogLevel::WARNING) << "poll timeout"; // 超時default:LOG(LogLevel::INFO) << "事件就緒"; // 讀事件就緒Dispatcher(); // 派發break;}}}
用poll函數,進行多路轉接,事件就緒就派發任務。
事件派發
void Dispatcher() // 事件派發{for (int i = 0; i < size; i++){if (_fds[i].fd == defaultfd) // 跳過continue;if (_fds[i].revents & POLLIN) // 是讀就緒{if (_fds[i].fd == _listensockfd->FD()){// listen 套接字Accept();}else{// 普通 套接字Recv(i);}}}}
對文件描述符數組,進行遍歷。如果不是合法位置(沒放文件描述符)就跳過這個位置。如果是讀就緒然后判斷是監聽套接字合適普通套接字。
收到客戶端的連接accept
void Accept(){InetAddr client;int sockfd = _listensockfd->AcceptOrDie(&client);LOG(LogLevel::DEBUG) << "accept a new client" << client.StringAddr();int pos = 0;for (; pos < size; pos++){if (_fds[pos].fd == defaultfd)break; // 數組中找到空位}if (pos == size){LOG(LogLevel::WARNING) << "poll server full";close(sockfd);}else{_fds[pos].fd = sockfd;_fds[pos].events = POLLIN;_fds[pos].revents = 0;}}
主要是收到客戶端的連接,創建了accept的套接字,要把這個套接字放到數組中。在文件描述符數組中找到空位,把這個acceptfd設置進去。
讀數據
void Recv(int pos) // 讀數據{char buffer[1024];ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 收信息if (n > 0){buffer[n] = 0;cout << "client say@ " << buffer << endl;}else if (n == 0) // 客戶端退出{LOG(LogLevel::INFO) << "client quit";_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;close(_fds[pos].fd);}else // 出現錯誤 異常{LOG(LogLevel::FATAL) << "recv error";_fds[pos].fd = defaultfd;_fds[pos].events = 0;_fds[pos].revents = 0;close(_fds[pos].fd);}
這時候,讀數據已經是非阻塞的了,客戶端退出和異常要把文件描述符數組的內容清空,然后關閉文件描述符。
源碼:poll多路轉接