在实际的开发中,我们经常会遇到这样的场景,我们需要接受多个端口的数据、多个终端的数据抑或是多个文件描述符对应的数据。那么,遇到这样的问题,你在程序中该怎么做呢?通常的做法,在程序中对数据交互的描述符进行轮询。那么问题来了,轮询的时间设置为多少呢?设置的太短,可以保证处理性能和速度,但是CPU的使用率太高,一旦处理的描述符数量多了起来,CPU可能就扛不住了。设置的时间太长,描述符处理的时间片太短,处于空闲的时间较长,性能和速度达不到要求。如果是服务器的话,面对多个用户的连接,处理速度和CPU使用性能是必须考虑的,而且最好要兼顾。这里就需要使用到I/O多路复用机制,这就是博主即将要和小伙伴们探讨的内容。
poll简介
# include <poll.h>
int poll( struct pollfd * fds, unsigned int nfds, int timeout);
fds:一个结构数组,struct pollfd结构如下:
structpollfd{
intfd; //文件描述符
shortevents; //请求的事件(即监控事件)
shortrevents; //返回的事件(即已就绪事件)
};
events和revents是通过对代表各种事件的标志进行逻辑或运算构建而成的。events包括要监视的事件,poll用已经发生的事件填充revents。poll函数通过在revents中设置标志POLLHUP、POLLERR和POLLNVAL来反映相关条件的存在。不需要在events中对于这些标志符相关的比特位进行设置。如果fd小于0, 则events字段被忽略,而revents被置为0。标准中没有说明如何处理文件结束。文件结束可以通过revents的标识符POLLHUN或返回0字节的常规读操作来传达。即使POLLIN或POLLRDNORM指出还有数据要读,POLLHUP也可能会被设置。因此,应该在错误检验之前处理正常的读操作。
poll函数的事件标志符值
常量
说明
POLLIN
普通或优先级带数据可读
POLLRDNORM
普通数据可读
POLLRDBAND
优先级带数据可读
POLLPRI
高优先级数据可读
POLLOUT
普通数据可写
POLLWRNORM
普通数据可写
POLLWRBAND
优先级带数据可写
POLLERR
发生错误
POLLHUP
发生挂起
POLLNVAL
描述字不是一个打开的文件
此外,revents域中还可能返回下列事件:
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
注意:后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。
nfds:要监视的描述符的数目。
timeout:超时时间,是一个用毫秒表示的时间,是指定poll在返回前没有接收事件时应该等待的时间。如果 它的值为-1,poll就永远都不会超时。如果整数值为32个比特,那么最大的超时周期大约是30分钟。
timeout值
说明
INFTIM(<0)
永远等待
0
立即返回,不阻塞进程
>0
等待指定数目的毫秒数
返回值和错误代码:成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
程序实例
服务端代码
#include <stdio.h>
#include <arpa/inet.h>
#include <poll.h>
#include <time.h>
#include <unistd.h>
#include <assert.h>
#include <sys/socket.h>
#include <error.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
const int MAXFD = FD_SETSIZE;//FD_SETSIZE1024 可以监控的最大描述符数量
const char *SERVERIP ="127.0.0.1";//服务端IP
const unsigned short PORT = 6666;//服务端端口号
typedef struct server_st
{
int cli_num; //已连接客户端数量
struct pollfd cli_fd[MAXFD]; //存放已连接客户端描述符
int index; //保存最大的索引号(已连接客户端数据下标)
int ready; //已就绪描述符数量(poll返回值)
}server_st_t;
//打印出错信息并推出程序
#define handle_error(msg)\
do {perror(msg); exit(EXIT_FAILURE);}while(0)
//定义类
class pollsocket
{
public:
//构造函数
pollsocket(const char *server_ip = SERVERIP, unsigned short port =PORT);
//析构函数
virtual ~pollsocket();
//客户处理函数(对外接口)
int handle_cli_proc();
private:
int server_init();//初始化server_st_t结构体
int server_uninit();//释放server_st_t结构体
int handle_create_proc();//创建服务端监听套接字
int handle_accept_proc();//处理客户端连接
int handle_recv_proc();//处理客户端数据发送
pollsocket(const pollsocket &ref);
pollsocket& operator=(const pollsocket &ref);
private:
server_st_t *m_server_st;
char m_server_IP[16];
unsigned short m_port;
int m_server_fd;
};
pollsocket::pollsocket(const char*server_ip /*= SERVERIP*/, unsigned short port /*= PORT*/)
{
bzero(m_server_IP, sizeof(m_server_IP));//将类成员地址空间清零
memcpy(m_server_IP, server_ip, strlen(server_ip));//给类成员负值
m_port = port;
m_server_st = NULL;
server_init();//初始化
handle_create_proc();//创建监听套接字
}
pollsocket::~pollsocket()
{
//close(m_server_fd);
//关闭所有的描述符(包括监听描述符)
for (int i = 0; i < MAXFD; ++i)
{
int cli_fd = m_server_st->cli_fd[i].fd;
if (-1 != cli_fd)
{
close(cli_fd);
}
}
server_uninit();//释放结构体
}
int pollsocket::server_init()
{
m_server_st = (server_st_t*)malloc(sizeof(server_st_t));
if (NULL == m_server_st) handle_error("malloc");
//将结构体空间清零
bzero(m_server_st, sizeof(server_st_t));
//初始化描述符数组
for (int i=0; i<MAXFD; ++i)
{
m_server_st->cli_fd[i].fd = -1;
}
return 0;
}
int pollsocket::server_uninit()
{
if (NULL != m_server_st)
{
free(m_server_st);
}
m_server_st = NULL;
}
int pollsocket::handle_create_proc()
{
//创建TCP套接字
//参数1:协议族
//参数2:套接字类型
//参数3:使用的协议(0:使用套接字类型对应的默认协议)
if (-1 == (m_server_fd = socket(AF_INET, SOCK_STREAM, 0)))handle_error("socket");
//地址结构体
struct sockaddr_in serveraddr;
//将结构体变量空间清零
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;//协议族
//注意,设置端口和IP时,要将主机字节序转换为网络字节序
serveraddr.sin_port = htons(m_port);
//serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET, m_server_IP, &serveraddr.sin_addr);
int op = true;
///*一个端口释放后会等待两分钟之后才能再被使用(TIME_WAIT状态),SO_REUSEADDR是让端口释放后立即就可以被再次使用*/
if (-1 == setsockopt(m_server_fd, SOL_SOCKET, SO_REUSEADDR, &op,sizeof(op))) handle_error("setsockopt");
//命名套接字
if (-1 == bind(m_server_fd, (struct sockaddr*)&serveraddr,sizeof(serveraddr))) handle_error("serveraddr");
//将套接字由主动变为被动(接受客户端连接状态)
if (-1 == listen(m_server_fd, SOMAXCONN))handle_error("listen");
return 0;
}
int pollsocket::handle_accept_proc()
{
//地址结构体
struct sockaddr_in cli_addr;
socklen_t len = (socklen_t)sizeof(cli_addr);
//将结构体变量空间清零
bzero(&cli_addr, sizeof(cli_addr));
//接受客户端的连接请求
int cli_fd = accept(m_server_fd, (struct sockaddr*)&cli_addr,&len);
if (-1 == cli_fd) handle_error("accept");
++m_server_st->cli_num;//已连接客户数加1
fprintf(stdout, "#%d %s:%d connected server!\n",m_server_st->cli_num, inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
int i = 0;
//将新链接的客户存入客户端数组中(下标0被监听描述符占用)
for (i = 1; i < MAXFD; ++i)
{
if (-1 == m_server_st->cli_fd[i].fd)
{
//将已连接套接字描述符存入数组
m_server_st->cli_fd[i].fd = cli_fd;
//m_server_st->index保存客户数组中已连接套接字描述符的最大索引
m_server_st->index = m_server_st->index > i ?m_server_st->index : i;
break;
}
}
//如果已连接套接字描述符超过了FD_SETSIZE报错
if (i == MAXFD) handle_error("too manyconnect");
//设置新连接描述符监听事件为POLLIN
m_server_st->cli_fd[i].events = POLLIN;
}
int pollsocket::handle_recv_proc()
{
//如果数组中不至当前一个用户,遍历数组
for (int i = 0; i <= m_server_st->index; ++i)
{
//如果为空位置,continue
if (-1 == m_server_st->cli_fd[i].fd)
{
continue;
}
//如果非空位置,cli_fd保存当前描述符
int cli_fd = m_server_st->cli_fd[i].fd;
//如果当前描述符监听事件到来,执行以下代码
if (m_server_st->cli_fd[i].revents & POLLIN)
{
char buf[256] = {0};
//从网络中读取数据
int r = read(cli_fd, buf, sizeof(buf));
//如果读取出错
if (r <= 0)
{
//在数组中将当前位置设为空位置
m_server_st->cli_fd[i].fd =-1;
//关闭当前描述符(客户端断开)
close(cli_fd);
//已连接客户数减1
--m_server_st->cli_num;
}
//如果接受成功,将数据写回网络
write(cli_fd, buf, sizeof(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
}
//每处理一个描述符,read减1
//如果read为0(所有就绪描述符都处理完毕),就不用继续向后扫描
//if (--m_server_st->ready <= 0) break;
}
return 0;
}
int pollsocket::handle_cli_proc()
{
//将监听描述符加入监听数组中,并设置监听事件
m_server_st->cli_fd[0].fd = m_server_fd;
m_server_st->cli_fd[0].events = POLLIN;
while (1)
{
//poll阻塞等待描述符集合中是否有就绪描述符
//参数1:描述符集合地址
//参数2:监控的描述符数量
//参数3:最长等待时间(-1:无限等待,直到有描述符就绪;0:立即返回;等待指定时间,单位毫秒)
//一旦有描述符就绪则返回,返回值为以就绪描述符的个数
m_server_st->ready = poll(m_server_st->cli_fd,m_server_st->index+1, 5000);
//poll函数返回异常
if (-1 == m_server_st->ready) break;
//poll函数等待超时
if (0 == m_server_st->ready)
{
fprintf(stdout, "poll timeout\n");
continue;
}
//如果有客户连接服务器,监听套接字就绪,执行以下代码
if (m_server_st->cli_fd[0].revents & POLLIN)
{
//fprintf(stdout, "connect\n");
//处理客户端的连接
handle_accept_proc();
//已经处理过来sfd描述符,read减1
//如果此时read<=0,表示所有就绪描述符已处理完毕,返回poll处继续等待就绪描述符
//没有客户端发送数据,退出本次循环
if (--m_server_st->ready <= 0)
{
continue;
}
}
else
{
//处理客户端发送过来的数据
handle_recv_proc();
}
}
return 0;
}
int main()
{
pollsocket pollsock;
pollsock.handle_cli_proc();
}
客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
int main()
{
//创建IPV4 TCP套接字
int sfd =socket(AF_INET, SOCK_STREAM, 0);
if (-1 ==sfd) perror("socket"), exit(EXIT_FAILURE);
//地址结构体
structsockaddr_in addr;
addr.sin_family = AF_INET;//协议族
addr.sin_port = htons(6666);//端口
//将地址串转换为网络字节序,存储到addr.sin_addr中
inet_aton("127.0.0.1", &addr.sin_addr);//连接的服务端IP地址
//连接服务器
if (-1 ==connect(sfd, (struct sockaddr *)&addr, sizeof(addr)))
{
perror("connect");
exit(EXIT_FAILURE);
}
charbuf[256] = {};
//从标准输入读取数据
while (NULL!= fgets(buf, sizeof(buf), stdin))
{
//将数据发送给服务器
write(sfd, buf, strlen(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
//读取服务器放送过来的数据
int r =read(sfd, buf, sizeof(buf));
//接受失败
if (r<= 0)
{
break;
}
//输出
fprintf(stdout, buf, r);
//清空缓存区
memset(buf, 0x00, sizeof(buf));
}
//关闭套接字描述符
close(sfd);
return 0;
}
程序运行结果
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。关于select、poll和epoll的对比,请观看博主的另外一篇博文poll epoll select,在那里博主对他们之间的区别、性能和消息传递方式进行了总结,希望能对你有一点帮助。