一 引言
之前在《 网络编程(16)—— IO复用技术之select》介绍了用于IO复用的select函数,其基本原理就是将被监视的读、写、或者异常的所有文件操作符分别放置到一个位数组fd_set类型的变量中,然后调用select对上述变量进行检查并进入阻塞状态,若集合中某个文件操作符存在待读数据、待传输数据、或者异常时,select就会结束阻塞状态,该发生状态变化的文件操作符在集合中被保留,我们可以通过宏来检查某个文件操作符是否发生了状态的改变。
利用select进行IO复用,适合比较少连接或者有跨平台需求的服务器的开发,因为有两处制约其在处理多连接时的性能:1、每次都需要将包含文件操作符的集合向操作系统进行传递(文件操作符是操作系统级别的资源),由操作系统对集合中的文件操作符进行监视。
2、需要遍历每个文件操作符(有时候甚至可能从0开始遍历标准输出和标准输入的文件操作符等等)。
二 epoll的使用步骤
基于上述select本身的不足之处,可使用epoll函数代替select进行IO复用服务器的开发。 epoll的使用过程如下:
2.1 创建epoll文件描述符
利用epoll_create()函数在操作系统中创建一个epoll文件描述符,该描述符指向一片内存空间,用来存储需要进行监视的文件操作符。如下,为epoll_create()函数的函数原型,其接受一个int类型的参数,用来指定开辟的空间的大小,函数返回epoll的文件描述符。 #include<sys/epoll.h>
int epoll_create(int size);
2.2 在epoll中注册文件描述符
利用epoll_ctl()函数在上述开辟的空间中注册需要监视的文件操作符,其函数原型如下, int epoll_ctl(int epfd,int op,int fd,struct epoll_event &event);
epfd — epoll例程的文件描述符,就是用epoll_create()返回的文件描述符。
op — 注册选项,用来控制文件描述符的注册或者删除,EPOLL_CTL_ADD,表示注册一个文件描述符;EPOLL_CTL_DEL,表示删除一个文件描述符;
fd —- 需要进行注册的文件描述符,socket或者标准io的文件描述符等等event — 需要监视的事件,是需要读取、还是写入等等。该参数的类型是一个epoll_event类型的结构体,它的定义形式如下:
struct epoll_event
{
__unit32_t events;
epoll_data_t data;
}
events是一个整数类型的数值,用来设置要监视的事件的类型,如设置成EPOLLIN是要监视有数据待读取的事件,设置成EPOLLOUT是要监视有数据待发送的事件。而我们在data的fd成员中注册我们的socket文件描述符。
epoll_ctl的使用过程可以用以下代码来概括:
struct epoll_event event;
event.events=EPOLLOUT;
event.data.fd=serv_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event);
2.3 利用epoll_wait进行文件描述符发生信号的等待
epoll_wait()函数的功能和select类似,用来定位发生状态变化的文件描述符的原型如下:
int epoll_wait(int epfd,struct epoll_event *events,
int maxevents,int timeout);
epfd — epoll文件描述符
events — 表示被激活的事件,该结构体里面包含发生事件对应的文件描述符
maxevents — 表示第二个参数中可以保存的最大的事件数
timeout — 设置等待超时,单位是毫秒,设置成-1时,epoll_wait()会一直阻塞,直到有事件发生。
函数返回发生事件的文件操作符的个数。
以下代码是利用epoll实现的一个实现io复用的socket服务端,稍后对关键处进行解析。示例代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc,char* argv[])
{
int serv_sock,clnt_sock;
struct sockaddr_in serv_addr,clnt_addr;
int clnt_addr_sz;
int epoll_fd;
struct epoll_event event;
struct epoll_event* pevents;
int fd_num,i;
int str_len;
char buf[BUF_SIZE];
if(argc!=2)
{
printf("Uasge %s <port>\n",argv[0]);
exit(1);
}
//创建socket
serv_sock=socket(AF_INET,SOCK_STREAM,0);
//准备地址
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
//绑定
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
error_handling("bind error");
}
//监听
if(listen(serv_sock,5)==-1)
{
error_handling("listen error");
}
//创建epoll描述符,以及用来接收发生变化事件的数组,空间占用50个字节
epoll_fd=epoll_create(50);
pevents=malloc(sizeof(struct epoll_event)*50);
//利用epoll_ctl注册socket描述符
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,serv_sock,&event);
while(1)
{
//等待socket状态的变化,返回发生状态变化的文件描述符数
fd_num=epoll_wait(epoll_fd,pevents,50,-1);
puts("wait succeed");
for(i=0;i<fd_num;i++)
{
if(serv_sock==pevents[i].data.fd)
{
clnt_addr_sz=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
puts("accept succeed");
//把客户端添加到监视事件中
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,clnt_sock,&event);
}
else
{
str_len=read(pevents[i].data.fd,buf,BUF_SIZE);
if(str_len==0)
{
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,pevents[i].data.fd,NULL);
close(pevents[i].data.fd);
}
else
{
printf("client:%s\n",buf);
write(pevents[i].data.fd,buf,str_len);
}
}
}
}
close(epoll_fd);
close(serv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
第18、19、20行分别定义了epoll的操作符epoll_fd、以及用于监视socket描述符的事件event、用于存放被触发的事件的数组pevents。
第48行,我们创建了epoll描述符。
第49行,为事件数组malloc一段内存。
第51、52行,在事件中注册socket描述符,以及定义事件类型。
第53行,领用epoll_ctl实现epoll操作符和socket的关联
第57行,利用epoll_wait等待socket文件描述符发生状态的变化,该函数返回发生状态变化的socket文件描述符的个数,同时会将变化的event事件存放到
数组pevent中,这样我们就能在59行遍历这里数组,来处理对应的文件描述符。
第61行是一个if分支,主要判断文件描述符是服务器的socket还是客户端的socket。很显然,是服务端的socket的话,说明有新的客户端连接,我们就要
在62到70这段代码中接收连接并将客户端socket的监视放到epoll文件描述符中。
第72行开始是对客户端socket的一个处理,读取客户端发送的数据并进行返回,另外在客户端断开连接时,利用第76行的epoll_ctl从epool的文件描述符中
删除对该客户端socket的监控。
最后,第89和90行,分别关闭epoll的文件描述符和客户端的socket。
epoll的整个使用过程就,先介绍到这,如想要验证该功能,可以随便从之前的专栏文章中挑选一个回声客户端,也可以从Giyhub上下载本文的源代码。
里面包含一个回声客户端和服务端。
Github位置:
https://github.com/HymanLiuTS/NetDevelopment
克隆本项目:
git clone git@github.com:HymanLiuTS/NetDevelopment.git
获取本文源代码:
git checkout NL21