I/O多路复用之poll
poll() 函数是一个系统调用,用于在一个文件描述符数组中等待多个文件描述符上的 I/O 事件,并返回就绪的文件描述符的数量。它与 select() 函数类似,但提供了更好的性能。
poll() 函数原型如下:
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中,各个参数的含义如下:
fds:一个指向 pollfd 结构体数组的指针,用于传递需要监听的文件描述符和对应的 I/O 操作类型(读、写和异常);
nfds:需要监听的文件描述符数目;
timeout:等待事件的超时时间,单位是毫秒。如果超过这个时间仍然没有任何事件发生,则 poll() 函数将返回 0,表示超时。
pollfd 结构体类型用于描述一个文件描述符和需要监听的事件类型。其原型定义如下:
struct pollfd {int fd; /* 文件描述符 */short events; /* 需要监听的事件类型 */short revents; /* 实际发生的事件类型 */
};
其中,fd 是需要监听的文件描述符;events 是需要监听的事件类型,包括 POLLIN(可读)、POLLOUT(可写)和 POLLERR(错误)等;revents 是实际发生的事件类型,对于需要监听的事件类型,如果事件发生了,对应的位将被设置为 1。
使用 poll() 函数的步骤如下:
构建 pollfd 结构体数组,初始化需要监听的文件描述符和对应的事件类型;
调用 poll() 函数进行监听;
根据 poll() 返回的结果和实际发生的事件类型,进行相应的处理。
以下是一个简单的示例程序,演示了如何使用 poll() 函数等待多个文件描述符上的事件:
#include <iostream>
#include <cstring>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main() {// 创建两个 TCP 套接字int socket1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);int socket2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// 监听地址和端口struct sockaddr_in addr1 = {0};addr1.sin_family = AF_INET;addr1.sin_port = htons(8888);inet_pton(AF_INET, "0.0.0.0", &addr1.sin_addr);bind(socket1, (struct sockaddr*)&addr1, sizeof(addr1));struct sockaddr_in addr2 = {0};addr2.sin_family = AF_INET;addr2.sin_port = htons(8889);inet_pton(AF_INET, "0.0.0.0", &addr2.sin_addr);bind(socket2, (struct sockaddr*)&addr2, sizeof(addr2));// 设置套接字为监听状态,等待连接listen(socket1, 10);listen(socket2, 10);// 构建 pollfd 结构体数组struct pollfd fds[2];fds[0].fd = socket1;fds[0].events = POLLIN;fds[1].fd = socket2;fds[1].events = POLLIN;// 等待事件的发生int res = poll(fds, 2, -1);if (res == -1) {std::cerr << "poll error: " << std::strerror(errno) << std::endl;return 1;}if (res == 0) {std::cout << "poll timed out." << std::endl;return 0;}// 检查哪些文件描述符上发生了事件if (fds[0].revents & POLLIN) {std::cout << "socket 1 is readable." << std::endl;// TODO: 处理文件描述符 1 上的 I/O 事件}if (fds[1].revents & POLLIN) {std::cout << "socket 2 is readable." << std::endl;// TODO: 处理文件描述符 2 上的 I/O 事件}return 0;
}
在上面的示例中,我们创建了两个 TCP 套接字,并将它们添加到一个 pollfd 数组中进行监听。我们在代码中使用 poll() 函数来阻塞等待多个文件描述符上的 I/O 事件的发生,直到有文件描述符上发生了事件,poll() 函数才会返回。在检查 revents 指示的实际发生的事件类型,根据处理需要,可以在对应的条件分支中添加事件处理的代码。
需要注意的是,相比于 select() 函数,poll() 函数可以动态地添加和删除事件,因此可以用于动态管理和扩展任务列表。因此,如果我们需要处理动态管理的套接字,应该使用 poll() 函数。
总结来说,poll() 函数是实现高效网络编程中 I/O 多路复用的一种方法,它提供了更好的性能和更灵活的动态事件管理方式。但是与 epoll() 相比,它在处理大量文件描述符时性能较低,因此在高并发的情况下,应该优先选择使用 epoll()。
服务端和客户端示例代码
server
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>// ulimit -n
#define MAXNFDS 1024// 初始化服务端的监听端口。
int initserver(int port);int main(int argc,char *argv[])
{if (argc != 2){printf("usage: ./tcppoll port\\n"); return -1;}// 初始化服务端用于监听的socket。int listensock = initserver(atoi(argv[1]));printf("listensock=%d\\n",listensock);if (listensock < 0){printf("initserver() failed.\\n"); return -1;}int maxfd; // fds数组中需要监视的socket的大小。struct pollfd fds[MAXNFDS]; // fds存放需要监视的socket。for (int ii=0;ii<MAXNFDS;ii++) fds[ii].fd=-1; // 初始化数组,把全部的fd设置为-1。// 把listensock添加到数组中。fds[listensock].fd=listensock;fds[listensock].events=POLLIN; // 有数据可读事件,包括新客户端的连接、客户端socket有数据可读和客户端socket断开三种情况。maxfd=listensock;while (1){int infds = poll(fds,maxfd+1,5000);// printf("poll infds=%d\\n",infds);// 返回失败。if (infds < 0){printf("poll() failed.\\n"); perror("poll():"); break;}// 超时。if (infds == 0){printf("poll() timeout.\\n"); continue;}// 检查有事情发生的socket,包括监听和客户端连接的socket。// 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。for (int eventfd=0; eventfd <= maxfd; eventfd++){if (fds[eventfd].fd<0) continue;if ((fds[eventfd].revents&POLLIN)==0) continue;fds[eventfd].revents=0; // 先把revents清空。if (eventfd==listensock){// 如果发生事件的是listensock,表示有新的客户端连上来。struct sockaddr_in client;socklen_t len = sizeof(client);int clientsock = accept(listensock,(struct sockaddr*)&client,&len);if (clientsock < 0){printf("accept() failed.\\n"); continue;}printf ("client(socket=%d) connected ok.\\n",clientsock);if (clientsock>MAXNFDS){ printf("clientsock(%d)>MAXNFDS(%d)\\n",clientsock,MAXNFDS); close(clientsock); continue;}fds[clientsock].fd=clientsock;fds[clientsock].events=POLLIN; fds[clientsock].revents=0; if (maxfd < clientsock) maxfd = clientsock;printf("maxfd=%d\\n",maxfd);continue;}else {// 客户端有数据过来或客户端的socket连接被断开。char buffer[1024];memset(buffer,0,sizeof(buffer));// 读取客户端的数据。ssize_t isize=read(eventfd,buffer,sizeof(buffer));// 发生了错误或socket被对方关闭。if (isize <=0){printf("client(eventfd=%d) disconnected.\\n",eventfd);close(eventfd); // 关闭客户端的socket。fds[eventfd].fd=-1;// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。if (eventfd == maxfd){for (int ii=maxfd;ii>0;ii--){if ( fds[ii].fd != -1){maxfd = ii; break;}}printf("maxfd=%d\\n",maxfd);}continue;}printf("recv(eventfd=%d,size=%d):%s\\n",eventfd,isize,buffer);// 把收到的报文发回给客户端。write(eventfd,buffer,strlen(buffer));}}}return 0;
}// 初始化服务端的监听端口。
int initserver(int port)
{int sock = socket(AF_INET,SOCK_STREAM,0);if (sock < 0){printf("socket() failed.\\n"); return -1;}// Linux如下int opt = 1; unsigned int len = sizeof(opt);setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);setsockopt(sock,SOL_SOCKET,SO_KEEPALIVE,&opt,len);struct sockaddr_in servaddr;servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(port);if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 ){printf("bind() failed.\\n"); close(sock); return -1;}if (listen(sock,5) != 0 ){printf("listen() failed.\\n"); close(sock); return -1;}return sock;
}
client
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main(int argc, char *argv[])
{if (argc != 3){printf("usage:./tcpclient ip port\\n"); return -1;}int sockfd;struct sockaddr_in servaddr;char buf[1024];if ((sockfd=socket(AF_INET,SOCK_STREAM,0))<0) { printf("socket() failed.\\n"); return -1; }memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(atoi(argv[2]));servaddr.sin_addr.s_addr=inet_addr(argv[1]);if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0){printf("connect(%s:%s) failed.\\n",argv[1],argv[2]); close(sockfd); return -1;}printf("connect ok.\\n");for (int ii=0;ii<10000;ii++){// 从命令行输入内容。memset(buf,0,sizeof(buf));printf("please input:"); scanf("%s",buf);// sprintf(buf,"1111111111111111111111ii=%08d",ii);if (write(sockfd,buf,strlen(buf)) <=0){ printf("write() failed.\\n"); close(sockfd); return -1;}memset(buf,0,sizeof(buf));if (read(sockfd,buf,sizeof(buf)) <=0) { printf("read() failed.\\n"); close(sockfd); return -1;}printf("recv:%s\\n",buf);// close(sockfd); break;}
}
测试结果