许多服务器部署(尤为是 web 服务器部署)面对的最大问题之一是必须可以处理大量链接。不管是经过构建基于云的服务来处理网络通讯流,仍是把应用程序分布在 IBM Amazon EC 实例上,仍是为网站提供高性能组件,都须要可以处理大量并发链接。linux
一个好例子是,web 应用程序最近愈来愈动态了,尤为是使用 AJAX 技术的应用程序。若是要部署的系统容许数千客户端直接在网页中更新信息,好比提供事件或问题实时监视的系统,那么提供信息的速度就很是重要了。在网格或云环境中,可能有来自数千客户端的持久链接同时打开着,必须可以处理每一个客户端的请求并作出响应。web
在讨论 libevent 和 libev 如何处理多个网络链接以前,咱们先简要回顾一下处理这类链接的传统解决方案。数据库
回页首编程
处理多个链接有许多不一样的传统方法,可是在处理大量链接时它们每每会产生问题,由于它们使用的内存或 CPU 太多,或者达到了某个操做系统限制。数组
使用的主要方法以下:缓存
- 循环:早期系统使用简单的循环选择解决方案,即循环遍历打开的网络链接的列表,判断是否有要读取的数据。这种方法既缓慢(尤为是随着链接数量增长愈来愈慢),又低效(由于在处理当前链接时其余链接可能正在发送请求并等待响应)。在系统循环遍历每一个链接时,其余链接不得不等待。若是有 100 个链接,其中只有一个有数据,那么仍然必须处理其余 99 个链接,才能轮到真正须要处理的链接。
- poll、epoll 和变体:这是对循环方法的改进,它用一个结构保存要监视的每一个链接的数组,当在网络套接字上发现数据时,经过回调机制调用处理函数。poll 的问题是这个结构会很是大,在列表中添加新的网络链接时,修改结构会增长负载并影响性能。
- 选择:
select()
函数调用使用一个静态结构,它事先被硬编码为至关小的数量(1024 个链接),所以不适用于很是大的部署。
在各类平台上还有其余实现(好比 Solaris 上的 /dev/poll 或 FreeBSD/NetBSD 上的 kqueue),它们在各自的 OS 上性能可能更好,可是没法移植,也不必定可以解决处理请求的高层问题。ruby
上面的全部解决方案都用简单的循环等待并处理请求,而后把请求分派给另外一个函数以处理实际的网络交互。关键在于循环和网络套接字须要大量管理代码,这样才能监听、更新和控制不一样的链接和接口。
处理许多链接的另外一种方法是,利用现代内核中的多线程支持监听和处理链接,为每一个链接启动一个新线程。这把责任直接交给操做系统,可是会在 RAM 和 CPU 方面增长至关大的开销,由于每一个线程都须要本身的执行空间。另外,若是每一个线程都忙于处理网络链接,线程之间的上下文切换会很频繁。最后,许多内核并不适于处理如此大量的活跃线程。
libevent 库实际上没有更换 select()
、poll()
或其余机制的基础。而是使用对于每一个平台最高效的高性能解决方案在实现外加上一个包装器。
为了实际处理每一个请求,libevent 库提供一种事件机制,它做为底层网络后端的包装器。事件系统让为链接添加处理函数变得很是简便,同时下降了底层 I/O 复杂性。这是 libevent 系统的核心。
libevent 库的其余组件提供其余功能,包括缓冲的事件系统(用于缓冲发送到客户端/从客户端接收的数据)以及 HTTP、DNS 和 RPC 系统的核心实现。
建立 libevent 服务器的基本方法是,注册当发生某一操做(好比接受来自客户端的链接)时应该执行的函数,而后调用主事件循环event_dispatch()
。执行过程的控制如今由 libevent 系统处理。注册事件和将调用的函数以后,事件系统开始自治;在应用程序运行时,能够在事件队列中添加(注册)或删除(取消注册)事件。事件注册很是方便,能够经过它添加新事件以处理新打开的链接,从而构建灵活的网络处理系统。
例如,能够打开一个监听套接字,而后注册一个回调函数,每当须要调用 accept()
函数以打开新链接时调用这个回调函数,这样就建立了一个网络服务器。清单 1 所示的代码片断说明基本过程:
清单 1. 打开监听套接字,注册一个回调函数(每当须要调用
accept()
函数以打开新链接时调用它),由此建立网络服务器
int main(int argc, char **argv) { ... ev_init(); /* Setup listening socket */ event_set(&ev_accept, listen_fd, EV_READ|EV_PERSIST, on_accept, NULL); event_add(&ev_accept, NULL); /* Start the event loop. */ event_dispatch(); } |
event_set()
函数建立新的事件结构,event_add()
在事件队列机制中添加事件。而后,event_dispatch()
启动事件队列系统,开始监听(并接受)请求。
清单 2 给出一个更完整的示例,它构建一个很是简单的回显服务器:
清单 2. 构建简单的回显服务器
#include <event.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #define SERVER_PORT 8080 int debug = 0; struct client { int fd; struct bufferevent *buf_ev; }; int setnonblock(int fd) { int flags; flags = fcntl(fd, F_GETFL); flags |= O_NONBLOCK; fcntl(fd, F_SETFL, flags); } void buf_read_callback(struct bufferevent *incoming, void *arg) { struct evbuffer *evreturn; char *req; req = evbuffer_readline(incoming->input); if (req == NULL) return; evreturn = evbuffer_new(); evbuffer_add_printf(evreturn,"You said %s\n",req); bufferevent_write_buffer(incoming,evreturn); evbuffer_free(evreturn); free(req); } void buf_write_callback(struct bufferevent *bev, void *arg) { } void buf_error_callback(struct bufferevent *bev, short what, void *arg) { struct client *client = (struct client *)arg; bufferevent_free(client->buf_ev); close(client->fd); free(client); } void accept_callback(int fd, short ev, void *arg) { int client_fd; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); struct client *client; client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { warn("Client: accept() failed"); return; } setnonblock(client_fd); client = calloc(1, sizeof(*client)); if (client == NULL) err(1, "malloc failed"); client->fd = client_fd; client->buf_ev = bufferevent_new(client_fd, buf_read_callback, buf_write_callback, buf_error_callback, client); bufferevent_enable(client->buf_ev, EV_READ); } int main(int argc, char **argv) { int socketlisten; struct sockaddr_in addresslisten; struct event accept_event; int reuse = 1; event_init(); socketlisten = socket(AF_INET, SOCK_STREAM, 0); if (socketlisten < 0) { fprintf(stderr,"Failed to create listen socket"); return 1; } memset(&addresslisten, 0, sizeof(addresslisten)); addresslisten.sin_family = AF_INET; addresslisten.sin_addr.s_addr = INADDR_ANY; addresslisten.sin_port = htons(SERVER_PORT); if (bind(socketlisten, (struct sockaddr *)&addresslisten, sizeof(addresslisten)) < 0) { fprintf(stderr,"Failed to bind"); return 1; } if (listen(socketlisten, 5) < 0) { fprintf(stderr,"Failed to listen to socket"); return 1; } setsockopt(socketlisten, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); setnonblock(socketlisten); event_set(&accept_event, socketlisten, EV_READ|EV_PERSIST, accept_callback, NULL); event_add(&accept_event, NULL); event_dispatch(); close(socketlisten); return 0; } |
下面讨论各个函数及其操做:
main()
:主函数建立用来监听链接的套接字,而后建立accept()
的回调函数以便经过事件处理函数处理每一个链接。accept_callback()
:当接受链接时,事件系统调用此函数。此函数接受到客户端的链接;添加客户端套接字信息和一个 bufferevent 结构;在事件结构中为客户端套接字上的读/写/错误事件添加回调函数;做为参数传递客户端结构(和嵌入的 eventbuffer 和客户端套接字)。每当对应的客户端套接字包含读、写或错误操做时,调用对应的回调函数。buf_read_callback()
:当客户端套接字有要读的数据时调用它。做为回显服务,此函数把 "you said..." 写回客户端。套接字仍然打开,能够接受新请求。buf_write_callback()
:当有要写的数据时调用它。在这个简单的服务中,不须要此函数,因此定义是空的。buf_error_callback()
:当出现错误时调用它。这包括客户端中断链接。在出现错误的全部场景中,关闭客户端套接字,从事件列表中删除客户端套接字的事件条目,释放客户端结构的内存。setnonblock()
:设置网络套接字以开放 I/O。
当客户端链接时,在事件队列中添加新事件以处理客户端链接;当客户端中断链接时删除事件。在幕后,libevent 处理网络套接字,识别须要服务的客户端,分别调用对应的函数。
为了构建这个应用程序,须要编译 C 源代码并添加 libevent 库:$ gcc -o basic basic.c -levent
。
从客户端的角度来看,这个服务器仅仅把发送给它的任何文本发送回来(见 清单 3)。
清单 3. 服务器把发送给它的文本发送回来
$ telnet localhost 8080 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello! You said Hello! |
这样的网络应用程序很是适合须要处理多个链接的大规模分布式部署,好比 IBM Cloud 系统。
很难经过简单的解决方案观察处理大量并发链接的状况和性能改进。可使用嵌入的 HTTP 实现帮助了解可伸缩性。
若是但愿构建本机应用程序,可使用通常的基于网络的 libevent 接口;可是,愈来愈常见的场景是开发基于 HTTP 协议的应用程序,以及装载或动态地从新装载信息的网页。若是使用任何 AJAX 库,客户端就须要 HTTP,即便您返回的信息是 XML 或 JSON。
libevent 中的 HTTP 实现并非 Apache HTTP 服务器的替代品,而是适用于与云和 web 环境相关联的大规模动态内容的实用解决方案。例如,能够在 IBM Cloud 或其余解决方案中部署基于 libevent 的接口。由于可使用 HTTP 进行通讯,服务器能够与其余组件集成。
要想使用 libevent 服务,须要使用与主要网络事件模型相同的基本结构,可是还必须处理网络接口,HTTP 包装器会替您处理。这使整个过程变成四个函数调用(初始化、启动 HTTP 服务器、设置 HTTP 回调函数和进入事件循环),再加上发送回数据的回调函数。清单 4 给出一个很是简单的示例:
清单 4. 使用 libevent 服务的简单示例
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <event.h> #include <evhttp.h> void generic_request_handler(struct evhttp_request *req, void *arg) { struct evbuffer *returnbuffer = evbuffer_new(); evbuffer_add_printf(returnbuffer, "Thanks for the request!"); evhttp_send_reply(req, HTTP_OK, "Client", returnbuffer); evbuffer_free(returnbuffer); return; } int main(int argc, char **argv) { short http_port = 8081; char *http_addr = "192.168.0.22"; struct evhttp *http_server = NULL; event_init(); http_server = evhttp_start(http_addr, http_port); evhttp_set_gencb(http_server, generic_request_handler, NULL); fprintf(stderr, "Server started on port %d\n", http_port); event_dispatch(); return(0); } |
应该能够经过前面的示例看出代码的基本结构,不须要解释。主要元素是 evhttp_set_gencb()
函数(它设置当收到 HTTP 请求时要使用的回调函数)和 generic_request_handler()
回调函数自己(它用一个表示成功的简单消息填充响应缓冲区)。
HTTP 包装器提供许多其余功能。例如,有一个请求解析器,它会从典型的请求中提取出查询参数(就像处理 CGI 请求同样)。还能够设置在不一样的请求路径中要触发的处理函数。经过设置不一样的回调函数和处理函数,可使用路径 '/db/' 提供到数据库的接口,或使用 '/memc' 提供到 memcached 的接口。
libevent 工具包的另外一个特性是支持通用计时器。能够在指定的时间段以后触发事件。能够经过结合使用计时器和 HTTP 实现提供轻量的服务,从而自动地提供文件内容,在修改文件内容时更新返回的数据。例如,之前要想在新闻频发的活动期间提供即时更新服务,前端 web 应用程序就须要按期从新装载新闻稿,而如今能够轻松地提供内容。整个应用程序(和 web 服务)都在内存中,所以响应很是快。
这就是 清单 5 中的示例的主要用途:
清单 5. 使用计时器在新闻频发的活动期间提供即时更新服务
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <event.h> #include <evhttp.h> #define RELOAD_TIMEOUT 5 #define DEFAULT_FILE "sample.html" char *filedata; time_t lasttime = 0; char filename[80]; int counter = 0; void read_file() { int size = 0; char *data; struct stat buf; stat(filename,&buf); if (buf.st_mtime > lasttime) { if (counter++) fprintf(stderr,"Reloading file: %s",filename); else fprintf(stderr,"Loading file: %s",filename); FILE *f = fopen(filename, "rb"); if (f == NULL) { fprintf(stderr,"Couldn't open file\n"); exit(1); } fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); data = (char *)malloc(size+1); fread(data, sizeof(char), size, f); filedata = (char *)malloc(size+1); strcpy(filedata,data); fclose(f); fprintf(stderr," (%d bytes)\n",size); lasttime = buf.st_mtime; } } void load_file() { struct event *loadfile_event; struct timeval tv; read_file(); tv.tv_sec = RELOAD_TIMEOUT; tv.tv_usec = 0; loadfile_event = malloc(sizeof(struct event)); evtimer_set(loadfile_event, load_file, loadfile_event); evtimer_add(loadfile_event, &tv); } void generic_request_handler(struct evhttp_request *req, void *arg) { struct evbuffer *evb = evbuffer_new(); evbuffer_add_printf(evb, "%s",filedata); evhttp_send_reply(req, HTTP_OK, "Client", evb); evbuffer_free(evb); } int main(int argc, char *argv[]) { short http_port = 8081; char *http_addr = "192.168.0.22"; struct evhttp *http_server = NULL; if (argc > 1) { strcpy(filename,argv[1]); printf("Using %s\n",filename); } else { strcpy(filename,DEFAULT_FILE); } event_init(); load_file(); http_server = evhttp_start(http_addr, http_port); evhttp_set_gencb(http_server, generic_request_handler, NULL); fprintf(stderr, "Server started on port %d\n", http_port); event_dispatch(); } |
这个服务器的基本原理与前面的示例相同。首先,脚本设置一个 HTTP 服务器,它只响应对基本 URL 主机/端口组合的请求(不处理请求 URI)。第一步是装载文件 (read_file()
)。在装载最初的文件时和在计时器触发回调时都使用此函数。
read_file()
函数使用 stat()
函数调用检查文件的修改时间,只有在上一次装载以后修改了文件的状况下,它才从新读取文件的内容。此函数经过调用 fread()
装载文件数据,把数据复制到另外一个结构中,而后使用 strcpy()
把数据从装载的字符串转移到全局字符串中。
load_file()
函数是触发计时器时调用的函数。它经过调用 read_file()
装载内容,而后使用 RELOAD_TIMEOUT 值设置计时器,做为尝试装载文件以前的秒数。libevent 计时器使用 timeval 结构,容许按秒和毫秒指定计时器。计时器不是周期性的;当触发计时器事件时设置它,而后从事件队列中删除事件。
使用与前面的示例相同的格式编译代码:$ gcc -o basichttpfile basichttpfile.c -levent
。
如今,建立做为数据使用的静态文件;默认文件是 sample.html,可是能够经过命令行上的第一个参数指定任何文件(见 清单 6)。
清单 6. 建立做为数据使用的静态文件
$ ./basichttpfile Loading file: sample.html (8046 bytes) Server started on port 8081 |
如今,程序能够接受请求了,从新装载计时器也启动了。若是修改 sample.html 的内容,应该会从新装载此文件并在日志中记录一个消息。例如,清单 7 中的输出显示初始装载和两次从新装载:
清单 7. 输出显示初始装载和两次从新装载
$ ./basichttpfile Loading file: sample.html (8046 bytes) Server started on port 8081 Reloading file: sample.html (8047 bytes) Reloading file: sample.html (8048 bytes) |
注意,要想得到最大的收益,必须确保环境没有限制打开的文件描述符数量。可使用 ulimit 命令修改限制(须要适当的权限或根访问)。具体的设置取决与您的 OS,可是在 Linux® 上能够用 -n
选项设置打开的文件描述符(和网络套接字)的数量:
清单 8. 用
-n
选项设置打开的文件描述符数量
$ ulimit -n 1024 |
经过指定数字提升限制:$ ulimit -n 20000
。
可使用 Apache Bench 2 (ab2) 等性能基准测试应用程序检查服务器的性能。能够指定并发查询的数量以及请求的总数。例如,使用 100,000 个请求运行基准测试,并发请求数量为 1000 个:$ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/
。
使用服务器示例中所示的 8K 文件运行这个示例系统,得到的结果为大约每秒处理 11,000 个请求。请记住,这个 libevent 服务器在单一线程中运行,并且单一客户端不太可能给服务器形成压力,由于它还受到打开请求的方法的限制。尽管如此,在交换的文档大小适中的状况下,这样的处理速率对于单线程应用程序来讲仍然使人吃惊。
尽管 C 语言很适合许多系统应用程序,可是在现代环境中不常用 C 语言,脚本语言更灵活、更实用。幸运的是,Perl 和 PHP 等大多数脚本语言是用 C 编写的,因此能够经过扩展模块使用 libevent 等 C 库。
例如,清单 9 给出 Perl 网络服务器脚本的基本结构。accept_callback()
函数与 清单 1 所示核心 libevent 示例中的 accept 函数相同。
清单 9. Perl 网络服务器脚本的基本结构
my $server = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => 8081, Proto => 'tcp', ReuseAddr => SO_REUSEADDR, Listen => 1, Blocking => 0, ) or die $@; my $accept = event_new($server, EV_READ|EV_PERSIST, \&accept_callback); $main->add; event_mainloop(); |
用这些语言编写的 libevent 实现一般支持 libevent 系统的核心,可是不必定支持 HTTP 包装器。所以,对脚本编程的应用程序使用这些解决方案会比较复杂。有两种方法:要么把脚本语言嵌入到基于 C 的 libevent 应用程序中,要么使用基于脚本语言环境构建的众多 HTTP 实现之一。例如,Python 包含功能很强的 HTTP 服务器类 (httplib/httplib2)。
应该指出一点:在脚本语言中没有什么东西是没法用 C 从新实现的。可是,要考虑到开发时间的限制,并且与现有代码集成可能更重要。
与 libevent 同样,libev 系统也是基于事件循环的系统,它在 poll()
、select()
等机制的本机实现的基础上提供基于事件的循环。到我撰写本文时,libev 实现的开销更低,可以实现更好的基准测试结果。libev API 比较原始,没有 HTTP 包装器,可是 libev 支持在实现中内置更多事件类型。例如,一种 evstat 实现能够监视多个文件的属性变更,能够在 清单 4 所示的 HTTP 文件解决方案中使用它。
可是,libevent 和 libev 的基本过程是相同的。建立所需的网络监听套接字,注册在执行期间要调用的事件,而后启动主事件循环,让 libev 处理过程的其他部分。
例如,可使用 Ruby 接口按照与清单 1 类似的方式提供回显服务器,见 清单 10。
清单 10. 使用 Ruby 接口提供回显服务器
require 'rubygems' require 'rev' PORT = 8081 class EchoServerConnection < Rev::TCPSocket def on_read(data) write 'You said: ' + data end end server = Rev::TCPServer.new('192.168.0.22', PORT, EchoServerConnection) server.attach(Rev::Loop.default) puts "Listening on localhost:#{PORT}" Rev::Loop.default.run |
Ruby 实现尤为出色,由于它为许多经常使用的网络解决方案提供了包装器,包括 HTTP 客户端、OpenSSL 和 DNS。其余脚本语言实现包括功能全面的 Perl 和 Python 实现,您能够试一试。
libevent 和 libev 都提供灵活且强大的环境,支持为处理服务器端或客户端请求实现高性能网络(和其余 I/O)接口。目标是以高效(CPU/RAM 使用量低)的方式支持数千甚至数万个链接。在本文中,您看到了一些示例,包括 libevent 中内置的 HTTP 服务,可使用这些技术支持基于 IBM Cloud、EC2 或 AJAX 的 web 应用程序。
学习
- C10K problem 对处理 10,000 个链接的问题作了精彩的概述。
- IBM Cloud Computing 网站提供不一样云实现的相关信息。
- 阅读 系统管理工具包: 标准化您的 UNIX 命令行工具(Martin Brown,developerWorks,2006 年 5 月),学习如何跨多台机器使用相同的命令。
- 让 UNIX 和 Linux 一块儿工做(Martin Brown,developerWorks,2006 年 4 月)讲解如何让传统的 UNIX 发行版和 Linux 一块儿工做。
- 揭秘云计算(Brett McLaughlin,developerWorks,2009 年 3 月):帮助您根据本身的应用程序需求选择最好的云计算平台。
- 阅读 用 Amazon Web Services 进行云计算(Prabhakar Chaganti,developerWorks,2008 年 7 月):详细讲解如何使用 Amazon Web Services。
- 能够经过 developerWorks Cloud Computing Resource Center 使用适用于 Amazon EC2 平台的 IBM 产品。
- developerWorks Cloud Computing Resource Center 使用适用于 Amazon EC2 平台的 IBM 产品。
- 在 developerWorks 的 云开发人员资源 中,发现和共享应用程序和服务开发人员构建其云部署项目的知识和经验。
- AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的全部方面相关的信息,您能够利用它们来扩展本身的 UNIX 技能。
- AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面可了解更多关于 AIX 和 UNIX 的内容。
- AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了不少的技术专题,为您总结了不少热门的知识点。咱们在后面还会继续推出不少相关的热门专题给您,为了方便您的访问,咱们在这里为您把本专区的全部专题进行汇总,让您更方便的找到您须要的内容。
- AIX and UNIX 下载中心:在这里你能够下载到能够运行在 AIX 或者是 UNIX 系统上的 IBM 服务器软件以及工具,让您能够提早免费试用他们的强大功能。
- IBM Systems Magazine for AIX 中文版:本杂志的内容更加关注于趋势和企业级架构应用方面的内容,同时对于新兴的技术、产品、应用方式等也有很深刻的探讨。IBM Systems Magazine 的内容都是由十分资深的业内人士撰写的,包括 IBM 的合做伙伴、IBM 的主机工程师以及高级管理人员。因此,从这些内容中,您能够了解到更高层次的应用理念,让您在选择和应用 IBM 系统时有一个更好的认识。
- 在 developerWorks 播客 上收听面向软件开发人员的有趣访谈和讨论。
- developerWorks 技术活动和网络广播:随时关注 developerWorks 技术活动和网络广播。
得到产品和技术
- 获取 libev 库,包括下载和文档。
- 获取 libevent 库。
- ruby libev (rev) 库和文档。
- Memcached 是用于存储和处理数据的 RAM 缓存(其核心使用 libevent,也可使用其余 libevent 服务器)。
- 使用 IBM 试用软件 改进您的下一个开放源码开发项目,这些软件能够经过下载或从 DVD 得到。
讨论
- 参与 developerWorks 博客 并加入 developerWorks 社区。
- 加入 My developerWorks 中文社区
- 参与 AIX 和 UNIX® 论坛:
Martin Brown 成为专业做家已有八年多的时间了。他是题材普遍的众多书籍和文章的做者。他的专业技术涉及各类开发语言和平台 — Perl、 Python、Java、JavaScript、Basic、Pascal、Modula-二、C、C++、Rebol、Gawk、 Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X 等等 — 还涉及 Web 编程、系统管理和集成。Martin 是 Microsoft 的主题专家(SME),而且是 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 的按期投稿人,他仍是 Computerworld、The Apple Blog 和其余站点的正式博客。您能够经过他的 Web 站点 http://www.mcslp.com 与他联络。