前文已经介绍了epoll这IO模型,能够处理数百万的并发量,但是epoll只适用于Linux平台,如果想要编写高并发可移植的网络应用程序我们该怎么办呢?答案是用开源的跨平台高性能的网络库Libevent..
libevent 是一个高性能轻量级的跨平台网络库,事件驱动,适用于多个平台,Linux,Windows,Mac os等,支持多种网络IO复用,如常用的select,epoll,pll,dev/poll,支持IO,信号和定时器事件;支持注册事件优先级等,github地址:https://github.com/libevent/libevent,想了解的可以clone下了解,这篇文章不对libevent作详细介绍,仅介绍如何用libevent改善我们的ECHO服务器。
在上文中我们用epoll实现的ECHO服务器,其主干部分如下:
int n = epoll_wait(efd, events, MAX_EVENT_NUM, -1);
for(int i=0; i < n; i++)
{
if(events[i].data.fd == listenfd)
{
if((connfd = doAccept(listenfd)) < 0)
{
perror("accept failed.\n");
continue;
}
if(make_socket_nonblock(connfd) < 0)
{
perror("make non block failed");
return -1;
}
event.data.fd = connfd;
event.events = EPOLLIN | EPOLLET | EPOLLOUT ;
if(epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event) < 0)
{
perror("epoll add");
return -1;
}
}
else if(events[i].events & EPOLLIN)
{
doRead(listenfd);
}
else if(events[i].events & EPOLLOUT)
{
}
else
{
perror("epoll error");
close(events[i].data.fd);
}
}
}
可以看到epoll计数需要对当前发生的事件进行判断,判断的当前发生的是何种事件,然后分别进行处理,此外epoll实现的ECHO服务器仅能够在Linux平台下运行,若想移植到Winsow平台则上述代码无法执行,现在用Libevent改造我们的代码,使得ECHO服务器程度可以跨平台执行。
#include
#include
#include
#include
#include
#include
#include
#include
#include
int make_socket_nonblock(int fd)
{
#ifdef _WIN32
{
unsigned long nonblocking = 1;
if (ioctlsocket(fd, FIONBIO, &nonblocking) == SOCKET_ERROR) {
return -1;
}
}
#else
int flag;
if((flag = fcntl(fd, F_GETFL,0)) < 0)
{
perror("FCNTL FGET");
return -1;
}
flag |= O_NONBLOCK;
if(fcntl(fd,F_SETFL, flag)< 0)
{
perror("FCNTL SET");
return -1;
}
#endif
return 0;
}
int make_socket_reuseable(int fd)
{
#ifndef _WIN32
int reuse = 1;
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
perror("set reuse addr");
return -1;
}
#endif
return 0;
}
void doRead(evutil_socket_t fd, short event, void * arg)
{
char recvBuf[1024] = {0};
while(recv(fd,recvBuf, 1024, 0)>0)
{
send(fd,recvBuf, 1024, 0);
char* temp = recvBuf;
while(*temp != '\r' && *temp != '\n')
temp++;
*temp='\0';
if(strcmp(recvBuf, "quit") == 0)
exit(0);
}
}
void doAccept(evutil_socket_t fd, short event, void * arg)
{
int connfd;
char ipAddr[32] = {0};
struct event* readEvent = NULL;
struct sockaddr_in cliAddr;
bzero(&cliAddr, sizeof(cliAddr));
socklen_t len = sizeof(cliAddr);
struct event_base* base = (struct event_base*)arg;
connfd = accept(fd, (struct sockaddr*)&cliAddr, &len);
inet_ntop(AF_INET,&(cliAddr.sin_addr),ipAddr,32);
printf("accept client connect %s:%d.\n", ipAddr,ntohs(cliAddr.sin_port));
if(connfd < 0)
return;
make_socket_nonblock(connfd);
//接受一个客户端连接后创建一个新的事件,并将事件添加到事件队列里,设置该事件的回调函数。
readEvent = event_new(base, connfd, EV_READ|EV_PERSIST, doRead, NULL);
if(!readEvent)
return;
event_add(readEvent, NULL);
}
int main()
{
int listenfd, connfd;
struct event_base* base;
struct event *listenEvent;
struct sockaddr_in serverAddr;
bzero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(12345);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建一个event_base结构
base = event_base_new();
if(!base)
{
perror("base new");
return -1;
}
if((listenfd = socket(AF_INET, SOCK_STREAM, 0))< 0)
{
perror("create socket");
return -1;
}
//将套接字设置为非阻塞
if(make_socket_nonblock(listenfd) < 0)
{
perror("make socket nonblock");
return -1;
}
//将套接字地址设为可重用
make_socket_reuseable(listenfd);
if(bind(listenfd,(struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
{
perror("bind error");
return -1;
}
if(listen(listenfd, 64) < 0)
{
perror("listen fail");
return -1;
}
//创建一个event,设置该事件为读事件,并设置EV_PERSIST属性,只有事件发生后该事件不从事件队列里删除
//设置事件发生后的回调函数doAccept
listenEvent = event_new(base, listenfd, EV_READ|EV_PERSIST, doAccept, (void*)base);
if(!listenEvent)
{
perror("event new");
return -1;
}
//将创建的事件添加到事件队列里去,类似于epoll的epoll_ctl add
event_add(listenEvent, NULL);
//开始事件循环,类似于epoll的epoll_wait
event_base_dispatch(base);
}
运行该程序,然后telnet 127.0.0.1 12345 这个ECHO服务器仍然支持并发访问。主要注意的是编译该程序需要连接libevent库,要加上编译选项-levent
可以看到的是我们使用libevent改写我们的ECHO服务器代码后,代码的可读性有了很大的提升,我们不再去遍历所有注册的事件,并判断该事件时读是写还是异常,我们仅需要创建一个事件,并标明事件时读是写还是异常,然后设置该事件发生后的回调函数,当事件发生时,libevent会调用我们注册的回调函数,在回调中我们自己处理相应的事件。因此,代码的可读性有了很大的提升,程序员也可以从繁琐的epoll,select编码中解放出来,仅关注注册事件,以及事件发生后的处理问题,libevent的这种机制成为Reactor机制。
Reactor模式UML图如上,主要分为几个部分,Reactor:负责注册事件,以后事件的处理,删除, Event Demultiplexer:事件分发器,接受Reactor事件的注册,并且启动事件循环,当事件发生时调用具体的事件处理器 Concrete Handler处理事件。
Reactor模式的流程图,利用该模式网络编程时,程序员仅关注流程图左边的两部分,即事件的注册和事件的处理。目前libevent已经被广泛应用,作为底层的网络库,如著名的memcached, Vomit.有志于深入网络编程的朋友们可以先从学习libevent入手。