poll 函數用於檢測一組文件描述符(File Descriptor, fd)上的可讀可寫和出錯事件,其函數簽名如下:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
參數解釋:
typedef unsigned long int nfds_t;
struct pollfd 結構體定義如下:
struct pollfd { int fd; /* 待檢測事件的 fd */ short events; /* 關心的事件組合 */ short revents; /* 檢測後的得到的事件類型 */ };
struct pollfd的 events 欄位是由開發者來設置,告訴內核我們關注什麼事件,而 revents 欄位是 poll 函數返回時內核設置的,用以說明該 fd 發生了什麼事件。events 和 revents 一般有如下取值:
poll 檢測一組 fd 上的可讀可寫和出錯事件的概念與前文介紹 select 的事件含義一樣,這裡就不再贅述。poll 與 select 相比具有如下優點:
我們來看一個具體的例子:
/** * 演示 poll 函數的用法,poll_server.cpp * zhangyl 2019.03.16 */ #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <poll.h> #include <iostream> #include <string.h> #include <vector> #include <errno.h>
//無效fd標記 #define INVALID_FD -1
int main(int argc, char* argv[]) { //創建一個偵聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << "create listen socket error." << std::endl; return -1; }
//將偵聽socket設置為非阻塞的 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) { close(listenfd); std::cout << "set listenfd to nonblock error." << std::endl; return -1; }
//復用地址和埠號 int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));
//初始化伺服器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; close(listenfd); return -1; }
//啟動偵聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << "listen error." << std::endl; close(listenfd); return -1; }
std::vector<pollfd> fds; pollfd listen_fd_info; listen_fd_info.fd = listenfd; listen_fd_info.events = POLLIN; listen_fd_info.revents = 0; fds.push_back(listen_fd_info);
//是否存在無效的fd標誌 bool exist_invalid_fd; int n; while (true) { exist_invalid_fd = false; n = poll(&fds[0], fds.size(), 1000); if (n < 0) { //被信號中斷 if (errno == EINTR) continue;
//出錯,退出 break; } else if (n == 0) { //超時,繼續 continue; }
for (size_t i = 0; i < fds.size(); ++i) { // 事件可讀 if (fds[i].revents & POLLIN) { if (fds[i].fd == listenfd) { //偵聽socket,接受新連接 struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); //接受客戶端連接, 並加入到fds集合中 int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); if (clientfd != -1) { //將客戶端socket設置為非阻塞的 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) { close(clientfd); std::cout << "set clientfd to nonblock error." << std::endl; } else { struct pollfd client_fd_info; client_fd_info.fd = clientfd; client_fd_info.events = POLLIN; client_fd_info.revents = 0; fds.push_back(client_fd_info); std::cout << "new client accepted, clientfd: " << clientfd << std::endl; } } } else { //普通clientfd,收取數據 char buf[64] = { 0 }; int m = recv(fds[i].fd, buf, 64, 0); if (m <= 0) { if (errno != EINTR && errno != EWOULDBLOCK) { //出錯或對端關閉了連接,關閉對應的clientfd,並設置無效標誌位 for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++ iter) { if (iter->fd == fds[i].fd) { std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl; close(fds[i].fd); iter->fd = INVALID_FD; exist_invalid_fd = true; break; } } } } else { std::cout << "recv from client: " << buf << ", clientfd: " << fds[i].fd << std::endl; } } } else if (fds[i].revents & POLLERR) { //TODO: 暫且不處理 }
}// end outer-for-loop
if (exist_invalid_fd) { //統一清理無效的fd for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ) { if (iter->fd == INVALID_FD) iter = fds.erase(iter); else ++iter; } } }// end while-loop
//關閉所有socket for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++ iter) close(iter->fd);
return 0; }
編譯上述程序生成 poll_server 並運行,然後使用 nc 命令模擬三個客戶端並給 poll_server 發送消息,效果如下:
由於 nc 命令是以 作為結束標誌的,所以 poll_server 收到客戶端消息時顯示時分兩行的。
通過上面的示例代碼,我們也能看出 poll 函數存在的一些缺點:
與 select 函數實現非阻塞的 connect 原理一樣,我們可以使用 poll 去實現,即通過 poll 檢測 clientfd 在一定時間內是否可寫,示例代碼如下:
/** * Linux 下使用poll實現非同步的connect,linux_nonblocking_connect_poll.cpp * zhangyl 2019.03.16 */ #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <poll.h> #include <iostream> #include <string.h> #include <stdio.h> #include <fcntl.h> #include <errno.h>
#define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 #define SEND_DATA "helloworld"
int main(int argc, char* argv[]) { //1.創建一個socket int clientfd = socket(AF_INET, SOCK_STREAM, 0); if (clientfd == -1) { std::cout << "create client socket error." << std::endl; return -1; }
//連接成功以後,我們再將 clientfd 設置成非阻塞模式, //不能在創建時就設置,這樣會影響到 connect 函數的行為 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) { close(clientfd); std::cout << "set socket to nonblock error." << std::endl; return -1; }
//2.連接伺服器 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); serveraddr.sin_port = htons(SERVER_PORT); for (;;) { int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if (ret == 0) { std::cout << "connect to server successfully." << std::endl; close(clientfd); return 0; } else if (ret == -1) { if (errno == EINTR) { //connect 動作被信號中斷,重試connect std::cout << "connecting interruptted by signal, try again." << std::endl; continue; } else if (errno == EINPROGRESS) { //連接正在嘗試中 break; } else { //真的出錯了, close(clientfd); return -1; } } }
pollfd event; event.fd = clientfd; event.events = POLLOUT; int timeout = 3000; if (poll(&event, 1, timeout) != 1) { close(clientfd); std::cout << "[poll] connect to server error." << std::endl; return -1; }
if (!(event.revents & POLLOUT)) { close(clientfd); std::cout << "[POLLOUT] connect to server error." << std::endl; return -1; }
int err; socklen_t len = static_cast<socklen_t>(sizeof err); if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) return -1;
if (err == 0) std::cout << "connect to server successfully." << std::endl; else std::cout << "connect to server error." << std::endl;
//5. 關閉socket close(clientfd);
運行效果與前面的 select 實現這個非阻塞的 connect 一樣,這裡就不再給出運行效果圖了。
本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。
歡迎加入高性能伺服器開發 QQ 群一起交流: 578019391 。