前文已经介绍了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 <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> #include <event2/event.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> 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入手。