使用 libevent 和 libev 提高網(wǎng)絡(luò)應(yīng)用性能的方法
更新時(shí)間:2011年05月22日 19:34:54 作者:
構(gòu)建現(xiàn)代的服務(wù)器應(yīng)用程序需要以某種方法同時(shí)接收數(shù)百、數(shù)千甚至數(shù)萬個(gè)事件,無論它們是內(nèi)部請求還是網(wǎng)絡(luò)連接,都要有效地處理它們的操作
有許多解決方案,但是 libevent 庫和 libev 庫能夠大大提高性能和事件處理能力。在本文中,我們要討論在 UNIX® 應(yīng)用程序中使用和部署這些解決方案所用的基本結(jié)構(gòu)和方法。libev 和 libevent 都可以在高性能應(yīng)用程序中使用,包括部署在 IBM Cloud 或 Amazon EC2 環(huán)境中的應(yīng)用程序,這些應(yīng)用程序需要支持大量并發(fā)客戶端或操作。
簡介
許多服務(wù)器部署(尤其是 web 服務(wù)器部署)面對的最大問題之一是必須能夠處理大量連接。無論是通過構(gòu)建基于云的服務(wù)來處理網(wǎng)絡(luò)通信流,還是把應(yīng)用程序分布在 IBM Amazon EC 實(shí)例上,還是為網(wǎng)站提供高性能組件,都需要能夠處理大量并發(fā)連接。
一個(gè)好例子是,web 應(yīng)用程序最近越來越動(dòng)態(tài)了,尤其是使用 AJAX 技術(shù)的應(yīng)用程序。如果要部署的系統(tǒng)允許數(shù)千客戶端直接在網(wǎng)頁中更新信息,比如提供事件或問題實(shí)時(shí)監(jiān)視的系統(tǒng),那么提供信息的速度就非常重要了。在網(wǎng)格或云環(huán)境中,可能有來自數(shù)千客戶端的持久連接同時(shí)打開著,必須能夠處理每個(gè)客戶端的請求并做出響應(yīng)。
在討論 libevent 和 libev 如何處理多個(gè)網(wǎng)絡(luò)連接之前,我們先簡要回顧一下處理這類連接的傳統(tǒng)解決方案。
處理多個(gè)客戶端
處理多個(gè)連接有許多不同的傳統(tǒng)方法,但是在處理大量連接時(shí)它們往往會(huì)產(chǎn)生問題,因?yàn)樗鼈兪褂玫膬?nèi)存或 CPU 太多,或者達(dá)到了某個(gè)操作系統(tǒng)限制。
使用的主要方法如下:
循環(huán):早期系統(tǒng)使用簡單的循環(huán)選擇解決方案,即循環(huán)遍歷打開的網(wǎng)絡(luò)連接的列表,判斷是否有要讀取的數(shù)據(jù)。這種方法既緩慢(尤其是隨著連接數(shù)量增加越來越慢),又低效(因?yàn)樵谔幚懋?dāng)前連接時(shí)其他連接可能正在發(fā)送請求并等待響應(yīng))。在系統(tǒng)循環(huán)遍歷每個(gè)連接時(shí),其他連接不得不等待。如果有 100 個(gè)連接,其中只有一個(gè)有數(shù)據(jù),那么仍然必須處理其他 99 個(gè)連接,才能輪到真正需要處理的連接。
poll、epoll 和變體:這是對循環(huán)方法的改進(jìn),它用一個(gè)結(jié)構(gòu)保存要監(jiān)視的每個(gè)連接的數(shù)組,當(dāng)在網(wǎng)絡(luò)套接字上發(fā)現(xiàn)數(shù)據(jù)時(shí),通過回調(diào)機(jī)制調(diào)用處理函數(shù)。poll 的問題是這個(gè)結(jié)構(gòu)會(huì)非常大,在列表中添加新的網(wǎng)絡(luò)連接時(shí),修改結(jié)構(gòu)會(huì)增加負(fù)載并影響性能。
選擇:select() 函數(shù)調(diào)用使用一個(gè)靜態(tài)結(jié)構(gòu),它事先被硬編碼為相當(dāng)小的數(shù)量(1024 個(gè)連接),因此不適用于非常大的部署。
在各種平臺(tái)上還有其他實(shí)現(xiàn)(比如 Solaris 上的 /dev/poll 或 FreeBSD/NetBSD 上的 kqueue),它們在各自的 OS 上性能可能更好,但是無法移植,也不一定能夠解決處理請求的高層問題。
上面的所有解決方案都用簡單的循環(huán)等待并處理請求,然后把請求分派給另一個(gè)函數(shù)以處理實(shí)際的網(wǎng)絡(luò)交互。關(guān)鍵在于循環(huán)和網(wǎng)絡(luò)套接字需要大量管理代碼,這樣才能監(jiān)聽、更新和控制不同的連接和接口。
處理許多連接的另一種方法是,利用現(xiàn)代內(nèi)核中的多線程支持監(jiān)聽和處理連接,為每個(gè)連接啟動(dòng)一個(gè)新線程。這把責(zé)任直接交給操作系統(tǒng),但是會(huì)在 RAM 和 CPU 方面增加相當(dāng)大的開銷,因?yàn)槊總€(gè)線程都需要自己的執(zhí)行空間。另外,如果每個(gè)線程都忙于處理網(wǎng)絡(luò)連接,線程之間的上下文切換會(huì)很頻繁。最后,許多內(nèi)核并不適于處理如此大量的活躍線程。
libevent 方法
libevent 庫實(shí)際上沒有更換 select()、poll() 或其他機(jī)制的基礎(chǔ)。而是使用對于每個(gè)平臺(tái)最高效的高性能解決方案在實(shí)現(xiàn)外加上一個(gè)包裝器。
為了實(shí)際處理每個(gè)請求,libevent 庫提供一種事件機(jī)制,它作為底層網(wǎng)絡(luò)后端的包裝器。事件系統(tǒng)讓為連接添加處理函數(shù)變得非常簡便,同時(shí)降低了底層 I/O 復(fù)雜性。這是 libevent 系統(tǒng)的核心。
libevent 庫的其他組件提供其他功能,包括緩沖的事件系統(tǒng)(用于緩沖發(fā)送到客戶端/從客戶端接收的數(shù)據(jù))以及 HTTP、DNS 和 RPC 系統(tǒng)的核心實(shí)現(xiàn)。
創(chuàng)建 libevent 服務(wù)器的基本方法是,注冊當(dāng)發(fā)生某一操作(比如接受來自客戶端的連接)時(shí)應(yīng)該執(zhí)行的函數(shù),然后調(diào)用主事件循環(huán) event_dispatch()。執(zhí)行過程的控制現(xiàn)在由 libevent 系統(tǒng)處理。注冊事件和將調(diào)用的函數(shù)之后,事件系統(tǒng)開始自治;在應(yīng)用程序運(yùn)行時(shí),可以在事件隊(duì)列中添加(注冊)或刪除(取消注冊)事件。事件注冊非常方便,可以通過它添加新事件以處理新打開的連接,從而構(gòu)建靈活的網(wǎng)絡(luò)處理系統(tǒng)。
例如,可以打開一個(gè)監(jiān)聽套接字,然后注冊一個(gè)回調(diào)函數(shù),每當(dāng)需要調(diào)用 accept() 函數(shù)以打開新連接時(shí)調(diào)用這個(gè)回調(diào)函數(shù),這樣就創(chuàng)建了一個(gè)網(wǎng)絡(luò)服務(wù)器。清單 1 所示的代碼片段說明基本過程:
清單 1. 打開監(jiān)聽套接字,注冊一個(gè)回調(diào)函數(shù)(每當(dāng)需要調(diào)用 accept() 函數(shù)以打開新連接時(shí)調(diào)用它),由此創(chuàng)建網(wǎng)絡(luò)服務(wù)器
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() 函數(shù)創(chuàng)建新的事件結(jié)構(gòu),event_add() 在事件隊(duì)列機(jī)制中添加事件。然后,event_dispatch() 啟動(dòng)事件隊(duì)列系統(tǒng),開始監(jiān)聽(并接受)請求。
清單 2 給出一個(gè)更完整的示例,它構(gòu)建一個(gè)非常簡單的回顯服務(wù)器:
清單 2. 構(gòu)建簡單的回顯服務(wù)器
#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;
}
下面討論各個(gè)函數(shù)及其操作:
main():主函數(shù)創(chuàng)建用來監(jiān)聽連接的套接字,然后創(chuàng)建 accept() 的回調(diào)函數(shù)以便通過事件處理函數(shù)處理每個(gè)連接。
accept_callback():當(dāng)接受連接時(shí),事件系統(tǒng)調(diào)用此函數(shù)。此函數(shù)接受到客戶端的連接;添加客戶端套接字信息和一個(gè) bufferevent 結(jié)構(gòu);在事件結(jié)構(gòu)中為客戶端套接字上的讀/寫/錯(cuò)誤事件添加回調(diào)函數(shù);作為參數(shù)傳遞客戶端結(jié)構(gòu)(和嵌入的 eventbuffer 和客戶端套接字)。每當(dāng)對應(yīng)的客戶端套接字包含讀、寫或錯(cuò)誤操作時(shí),調(diào)用對應(yīng)的回調(diào)函數(shù)。
buf_read_callback():當(dāng)客戶端套接字有要讀的數(shù)據(jù)時(shí)調(diào)用它。作為回顯服務(wù),此函數(shù)把 "you said..." 寫回客戶端。套接字仍然打開,可以接受新請求。
buf_write_callback():當(dāng)有要寫的數(shù)據(jù)時(shí)調(diào)用它。在這個(gè)簡單的服務(wù)中,不需要此函數(shù),所以定義是空的。
buf_error_callback():當(dāng)出現(xiàn)錯(cuò)誤時(shí)調(diào)用它。這包括客戶端中斷連接。在出現(xiàn)錯(cuò)誤的所有場景中,關(guān)閉客戶端套接字,從事件列表中刪除客戶端套接字的事件條目,釋放客戶端結(jié)構(gòu)的內(nèi)存。
setnonblock():設(shè)置網(wǎng)絡(luò)套接字以開放 I/O。
當(dāng)客戶端連接時(shí),在事件隊(duì)列中添加新事件以處理客戶端連接;當(dāng)客戶端中斷連接時(shí)刪除事件。在幕后,libevent 處理網(wǎng)絡(luò)套接字,識(shí)別需要服務(wù)的客戶端,分別調(diào)用對應(yīng)的函數(shù)。
為了構(gòu)建這個(gè)應(yīng)用程序,需要編譯 C 源代碼并添加 libevent 庫:$ gcc -o basic basic.c -levent。
從客戶端的角度來看,這個(gè)服務(wù)器僅僅把發(fā)送給它的任何文本發(fā)送回來(見 清單 3)。
清單 3. 服務(wù)器把發(fā)送給它的文本發(fā)送回來
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello!
You said Hello!
這樣的網(wǎng)絡(luò)應(yīng)用程序非常適合需要處理多個(gè)連接的大規(guī)模分布式部署,比如 IBM Cloud 系統(tǒng)。
很難通過簡單的解決方案觀察處理大量并發(fā)連接的情況和性能改進(jìn)。可以使用嵌入的 HTTP 實(shí)現(xiàn)幫助了解可伸縮性。
使用內(nèi)置的 HTTP 服務(wù)器
如果希望構(gòu)建本機(jī)應(yīng)用程序,可以使用一般的基于網(wǎng)絡(luò)的 libevent 接口;但是,越來越常見的場景是開發(fā)基于 HTTP 協(xié)議的應(yīng)用程序,以及裝載或動(dòng)態(tài)地重新裝載信息的網(wǎng)頁。如果使用任何 AJAX 庫,客戶端就需要 HTTP,即使您返回的信息是 XML 或 JSON。
libevent 中的 HTTP 實(shí)現(xiàn)并不是 Apache HTTP 服務(wù)器的替代品,而是適用于與云和 web 環(huán)境相關(guān)聯(lián)的大規(guī)模動(dòng)態(tài)內(nèi)容的實(shí)用解決方案。例如,可以在 IBM Cloud 或其他解決方案中部署基于 libevent 的接口。因?yàn)榭梢允褂?HTTP 進(jìn)行通信,服務(wù)器可以與其他組件集成。
要想使用 libevent 服務(wù),需要使用與主要網(wǎng)絡(luò)事件模型相同的基本結(jié)構(gòu),但是還必須處理網(wǎng)絡(luò)接口,HTTP 包裝器會(huì)替您處理。這使整個(gè)過程變成四個(gè)函數(shù)調(diào)用(初始化、啟動(dòng) HTTP 服務(wù)器、設(shè)置 HTTP 回調(diào)函數(shù)和進(jìn)入事件循環(huán)),再加上發(fā)送回?cái)?shù)據(jù)的回調(diào)函數(shù)。清單 4 給出一個(gè)非常簡單的示例:
清單 4. 使用 libevent 服務(wù)的簡單示例
#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);
}
應(yīng)該可以通過前面的示例看出代碼的基本結(jié)構(gòu),不需要解釋。主要元素是 evhttp_set_gencb() 函數(shù)(它設(shè)置當(dāng)收到 HTTP 請求時(shí)要使用的回調(diào)函數(shù))和 generic_request_handler() 回調(diào)函數(shù)本身(它用一個(gè)表示成功的簡單消息填充響應(yīng)緩沖區(qū))。
HTTP 包裝器提供許多其他功能。例如,有一個(gè)請求解析器,它會(huì)從典型的請求中提取出查詢參數(shù)(就像處理 CGI 請求一樣)。還可以設(shè)置在不同的請求路徑中要觸發(fā)的處理函數(shù)。通過設(shè)置不同的回調(diào)函數(shù)和處理函數(shù),可以使用路徑 '/db/' 提供到數(shù)據(jù)庫的接口,或使用 '/memc' 提供到 memcached 的接口。
libevent 工具包的另一個(gè)特性是支持通用計(jì)時(shí)器??梢栽谥付ǖ臅r(shí)間段之后觸發(fā)事件??梢酝ㄟ^結(jié)合使用計(jì)時(shí)器和 HTTP 實(shí)現(xiàn)提供輕量的服務(wù),從而自動(dòng)地提供文件內(nèi)容,在修改文件內(nèi)容時(shí)更新返回的數(shù)據(jù)。例如,以前要想在新聞?lì)l發(fā)的活動(dòng)期間提供即時(shí)更新服務(wù),前端 web 應(yīng)用程序就需要定期重新裝載新聞稿,而現(xiàn)在可以輕松地提供內(nèi)容。整個(gè)應(yīng)用程序(和 web 服務(wù))都在內(nèi)存中,因此響應(yīng)非常快。
這就是 清單 5 中的示例的主要用途:
清單 5. 使用計(jì)時(shí)器在新聞?lì)l發(fā)的活動(dòng)期間提供即時(shí)更新服務(wù)
#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();
}
這個(gè)服務(wù)器的基本原理與前面的示例相同。首先,腳本設(shè)置一個(gè) HTTP 服務(wù)器,它只響應(yīng)對基本 URL 主機(jī)/端口組合的請求(不處理請求 URI)。第一步是裝載文件 (read_file())。在裝載最初的文件時(shí)和在計(jì)時(shí)器觸發(fā)回調(diào)時(shí)都使用此函數(shù)。
read_file() 函數(shù)使用 stat() 函數(shù)調(diào)用檢查文件的修改時(shí)間,只有在上一次裝載之后修改了文件的情況下,它才重新讀取文件的內(nèi)容。此函數(shù)通過調(diào)用 fread() 裝載文件數(shù)據(jù),把數(shù)據(jù)復(fù)制到另一個(gè)結(jié)構(gòu)中,然后使用 strcpy() 把數(shù)據(jù)從裝載的字符串轉(zhuǎn)移到全局字符串中。
load_file() 函數(shù)是觸發(fā)計(jì)時(shí)器時(shí)調(diào)用的函數(shù)。它通過調(diào)用 read_file() 裝載內(nèi)容,然后使用 RELOAD_TIMEOUT 值設(shè)置計(jì)時(shí)器,作為嘗試裝載文件之前的秒數(shù)。libevent 計(jì)時(shí)器使用 timeval 結(jié)構(gòu),允許按秒和毫秒指定計(jì)時(shí)器。計(jì)時(shí)器不是周期性的;當(dāng)觸發(fā)計(jì)時(shí)器事件時(shí)設(shè)置它,然后從事件隊(duì)列中刪除事件。
使用與前面的示例相同的格式編譯代碼:$ gcc -o basichttpfile basichttpfile.c -levent。
現(xiàn)在,創(chuàng)建作為數(shù)據(jù)使用的靜態(tài)文件;默認(rèn)文件是 sample.html,但是可以通過命令行上的第一個(gè)參數(shù)指定任何文件(見 清單 6)。
清單 6. 創(chuàng)建作為數(shù)據(jù)使用的靜態(tài)文件
$ ./basichttpfile
Loading file: sample.html (8046 bytes)
Server started on port 8081
現(xiàn)在,程序可以接受請求了,重新裝載計(jì)時(shí)器也啟動(dòng)了。如果修改 sample.html 的內(nèi)容,應(yīng)該會(huì)重新裝載此文件并在日志中記錄一個(gè)消息。例如,清單 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)
注意,要想獲得最大的收益,必須確保環(huán)境沒有限制打開的文件描述符數(shù)量??梢允褂?ulimit 命令修改限制(需要適當(dāng)?shù)臋?quán)限或根訪問)。具體的設(shè)置取決與您的 OS,但是在 Linux® 上可以用 -n 選項(xiàng)設(shè)置打開的文件描述符(和網(wǎng)絡(luò)套接字)的數(shù)量:
清單 8. 用 -n 選項(xiàng)設(shè)置打開的文件描述符數(shù)量
$ ulimit -n
1024
通過指定數(shù)字提高限制:$ ulimit -n 20000。
可以使用 Apache Bench 2 (ab2) 等性能基準(zhǔn)測試應(yīng)用程序檢查服務(wù)器的性能??梢灾付úl(fā)查詢的數(shù)量以及請求的總數(shù)。例如,使用 100,000 個(gè)請求運(yùn)行基準(zhǔn)測試,并發(fā)請求數(shù)量為 1000 個(gè):$ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/。
使用服務(wù)器示例中所示的 8K 文件運(yùn)行這個(gè)示例系統(tǒng),獲得的結(jié)果為大約每秒處理 11,000 個(gè)請求。請記住,這個(gè) libevent 服務(wù)器在單一線程中運(yùn)行,而且單一客戶端不太可能給服務(wù)器造成壓力,因?yàn)樗€受到打開請求的方法的限制。盡管如此,在交換的文檔大小適中的情況下,這樣的處理速率對于單線程應(yīng)用程序來說仍然令人吃驚。
--------------------------------------------------------------------------------
回頁首
使用其他語言的實(shí)現(xiàn)
盡管 C 語言很適合許多系統(tǒng)應(yīng)用程序,但是在現(xiàn)代環(huán)境中不經(jīng)常使用 C 語言,腳本語言更靈活、更實(shí)用。幸運(yùn)的是,Perl 和 PHP 等大多數(shù)腳本語言是用 C 編寫的,所以可以通過擴(kuò)展模塊使用 libevent 等 C 庫。
例如,清單 9 給出 Perl 網(wǎng)絡(luò)服務(wù)器腳本的基本結(jié)構(gòu)。accept_callback() 函數(shù)與 清單 1 所示核心 libevent 示例中的 accept 函數(shù)相同。
清單 9. Perl 網(wǎng)絡(luò)服務(wù)器腳本的基本結(jié)構(gòu)
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 實(shí)現(xiàn)通常支持 libevent 系統(tǒng)的核心,但是不一定支持 HTTP 包裝器。因此,對腳本編程的應(yīng)用程序使用這些解決方案會(huì)比較復(fù)雜。有兩種方法:要么把腳本語言嵌入到基于 C 的 libevent 應(yīng)用程序中,要么使用基于腳本語言環(huán)境構(gòu)建的眾多 HTTP 實(shí)現(xiàn)之一。例如,Python 包含功能很強(qiáng)的 HTTP 服務(wù)器類 (httplib/httplib2)。
應(yīng)該指出一點(diǎn):在腳本語言中沒有什么東西是無法用 C 重新實(shí)現(xiàn)的。但是,要考慮到開發(fā)時(shí)間的限制,而且與現(xiàn)有代碼集成可能更重要。
libev 庫
與 libevent 一樣,libev 系統(tǒng)也是基于事件循環(huán)的系統(tǒng),它在 poll()、select() 等機(jī)制的本機(jī)實(shí)現(xiàn)的基礎(chǔ)上提供基于事件的循環(huán)。到我撰寫本文時(shí),libev 實(shí)現(xiàn)的開銷更低,能夠?qū)崿F(xiàn)更好的基準(zhǔn)測試結(jié)果。libev API 比較原始,沒有 HTTP 包裝器,但是 libev 支持在實(shí)現(xiàn)中內(nèi)置更多事件類型。例如,一種 evstat 實(shí)現(xiàn)可以監(jiān)視多個(gè)文件的屬性變動(dòng),可以在 清單 4 所示的 HTTP 文件解決方案中使用它。
但是,libevent 和 libev 的基本過程是相同的。創(chuàng)建所需的網(wǎng)絡(luò)監(jiān)聽套接字,注冊在執(zhí)行期間要調(diào)用的事件,然后啟動(dòng)主事件循環(huán),讓 libev 處理過程的其余部分。
例如,可以使用 Ruby 接口按照與清單 1 相似的方式提供回顯服務(wù)器,見 清單 10。
清單 10. 使用 Ruby 接口提供回顯服務(wù)器
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 實(shí)現(xiàn)尤其出色,因?yàn)樗鼮樵S多常用的網(wǎng)絡(luò)解決方案提供了包裝器,包括 HTTP 客戶端、OpenSSL 和 DNS。其他腳本語言實(shí)現(xiàn)包括功能全面的 Perl 和 Python 實(shí)現(xiàn),您可以試一試。
結(jié)束語
libevent 和 libev 都提供靈活且強(qiáng)大的環(huán)境,支持為處理服務(wù)器端或客戶端請求實(shí)現(xiàn)高性能網(wǎng)絡(luò)(和其他 I/O)接口。目標(biāo)是以高效(CPU/RAM 使用量低)的方式支持?jǐn)?shù)千甚至數(shù)萬個(gè)連接。在本文中,您看到了一些示例,包括 libevent 中內(nèi)置的 HTTP 服務(wù),可以使用這些技術(shù)支持基于 IBM Cloud、EC2 或 AJAX 的 web 應(yīng)用程序。
簡介
許多服務(wù)器部署(尤其是 web 服務(wù)器部署)面對的最大問題之一是必須能夠處理大量連接。無論是通過構(gòu)建基于云的服務(wù)來處理網(wǎng)絡(luò)通信流,還是把應(yīng)用程序分布在 IBM Amazon EC 實(shí)例上,還是為網(wǎng)站提供高性能組件,都需要能夠處理大量并發(fā)連接。
一個(gè)好例子是,web 應(yīng)用程序最近越來越動(dòng)態(tài)了,尤其是使用 AJAX 技術(shù)的應(yīng)用程序。如果要部署的系統(tǒng)允許數(shù)千客戶端直接在網(wǎng)頁中更新信息,比如提供事件或問題實(shí)時(shí)監(jiān)視的系統(tǒng),那么提供信息的速度就非常重要了。在網(wǎng)格或云環(huán)境中,可能有來自數(shù)千客戶端的持久連接同時(shí)打開著,必須能夠處理每個(gè)客戶端的請求并做出響應(yīng)。
在討論 libevent 和 libev 如何處理多個(gè)網(wǎng)絡(luò)連接之前,我們先簡要回顧一下處理這類連接的傳統(tǒng)解決方案。
處理多個(gè)客戶端
處理多個(gè)連接有許多不同的傳統(tǒng)方法,但是在處理大量連接時(shí)它們往往會(huì)產(chǎn)生問題,因?yàn)樗鼈兪褂玫膬?nèi)存或 CPU 太多,或者達(dá)到了某個(gè)操作系統(tǒng)限制。
使用的主要方法如下:
循環(huán):早期系統(tǒng)使用簡單的循環(huán)選擇解決方案,即循環(huán)遍歷打開的網(wǎng)絡(luò)連接的列表,判斷是否有要讀取的數(shù)據(jù)。這種方法既緩慢(尤其是隨著連接數(shù)量增加越來越慢),又低效(因?yàn)樵谔幚懋?dāng)前連接時(shí)其他連接可能正在發(fā)送請求并等待響應(yīng))。在系統(tǒng)循環(huán)遍歷每個(gè)連接時(shí),其他連接不得不等待。如果有 100 個(gè)連接,其中只有一個(gè)有數(shù)據(jù),那么仍然必須處理其他 99 個(gè)連接,才能輪到真正需要處理的連接。
poll、epoll 和變體:這是對循環(huán)方法的改進(jìn),它用一個(gè)結(jié)構(gòu)保存要監(jiān)視的每個(gè)連接的數(shù)組,當(dāng)在網(wǎng)絡(luò)套接字上發(fā)現(xiàn)數(shù)據(jù)時(shí),通過回調(diào)機(jī)制調(diào)用處理函數(shù)。poll 的問題是這個(gè)結(jié)構(gòu)會(huì)非常大,在列表中添加新的網(wǎng)絡(luò)連接時(shí),修改結(jié)構(gòu)會(huì)增加負(fù)載并影響性能。
選擇:select() 函數(shù)調(diào)用使用一個(gè)靜態(tài)結(jié)構(gòu),它事先被硬編碼為相當(dāng)小的數(shù)量(1024 個(gè)連接),因此不適用于非常大的部署。
在各種平臺(tái)上還有其他實(shí)現(xiàn)(比如 Solaris 上的 /dev/poll 或 FreeBSD/NetBSD 上的 kqueue),它們在各自的 OS 上性能可能更好,但是無法移植,也不一定能夠解決處理請求的高層問題。
上面的所有解決方案都用簡單的循環(huán)等待并處理請求,然后把請求分派給另一個(gè)函數(shù)以處理實(shí)際的網(wǎng)絡(luò)交互。關(guān)鍵在于循環(huán)和網(wǎng)絡(luò)套接字需要大量管理代碼,這樣才能監(jiān)聽、更新和控制不同的連接和接口。
處理許多連接的另一種方法是,利用現(xiàn)代內(nèi)核中的多線程支持監(jiān)聽和處理連接,為每個(gè)連接啟動(dòng)一個(gè)新線程。這把責(zé)任直接交給操作系統(tǒng),但是會(huì)在 RAM 和 CPU 方面增加相當(dāng)大的開銷,因?yàn)槊總€(gè)線程都需要自己的執(zhí)行空間。另外,如果每個(gè)線程都忙于處理網(wǎng)絡(luò)連接,線程之間的上下文切換會(huì)很頻繁。最后,許多內(nèi)核并不適于處理如此大量的活躍線程。
libevent 方法
libevent 庫實(shí)際上沒有更換 select()、poll() 或其他機(jī)制的基礎(chǔ)。而是使用對于每個(gè)平臺(tái)最高效的高性能解決方案在實(shí)現(xiàn)外加上一個(gè)包裝器。
為了實(shí)際處理每個(gè)請求,libevent 庫提供一種事件機(jī)制,它作為底層網(wǎng)絡(luò)后端的包裝器。事件系統(tǒng)讓為連接添加處理函數(shù)變得非常簡便,同時(shí)降低了底層 I/O 復(fù)雜性。這是 libevent 系統(tǒng)的核心。
libevent 庫的其他組件提供其他功能,包括緩沖的事件系統(tǒng)(用于緩沖發(fā)送到客戶端/從客戶端接收的數(shù)據(jù))以及 HTTP、DNS 和 RPC 系統(tǒng)的核心實(shí)現(xiàn)。
創(chuàng)建 libevent 服務(wù)器的基本方法是,注冊當(dāng)發(fā)生某一操作(比如接受來自客戶端的連接)時(shí)應(yīng)該執(zhí)行的函數(shù),然后調(diào)用主事件循環(huán) event_dispatch()。執(zhí)行過程的控制現(xiàn)在由 libevent 系統(tǒng)處理。注冊事件和將調(diào)用的函數(shù)之后,事件系統(tǒng)開始自治;在應(yīng)用程序運(yùn)行時(shí),可以在事件隊(duì)列中添加(注冊)或刪除(取消注冊)事件。事件注冊非常方便,可以通過它添加新事件以處理新打開的連接,從而構(gòu)建靈活的網(wǎng)絡(luò)處理系統(tǒng)。
例如,可以打開一個(gè)監(jiān)聽套接字,然后注冊一個(gè)回調(diào)函數(shù),每當(dāng)需要調(diào)用 accept() 函數(shù)以打開新連接時(shí)調(diào)用這個(gè)回調(diào)函數(shù),這樣就創(chuàng)建了一個(gè)網(wǎng)絡(luò)服務(wù)器。清單 1 所示的代碼片段說明基本過程:
清單 1. 打開監(jiān)聽套接字,注冊一個(gè)回調(diào)函數(shù)(每當(dāng)需要調(diào)用 accept() 函數(shù)以打開新連接時(shí)調(diào)用它),由此創(chuàng)建網(wǎng)絡(luò)服務(wù)器
復(fù)制代碼 代碼如下:
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() 函數(shù)創(chuàng)建新的事件結(jié)構(gòu),event_add() 在事件隊(duì)列機(jī)制中添加事件。然后,event_dispatch() 啟動(dòng)事件隊(duì)列系統(tǒng),開始監(jiān)聽(并接受)請求。
清單 2 給出一個(gè)更完整的示例,它構(gòu)建一個(gè)非常簡單的回顯服務(wù)器:
清單 2. 構(gòu)建簡單的回顯服務(wù)器
復(fù)制代碼 代碼如下:
#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;
}
下面討論各個(gè)函數(shù)及其操作:
main():主函數(shù)創(chuàng)建用來監(jiān)聽連接的套接字,然后創(chuàng)建 accept() 的回調(diào)函數(shù)以便通過事件處理函數(shù)處理每個(gè)連接。
accept_callback():當(dāng)接受連接時(shí),事件系統(tǒng)調(diào)用此函數(shù)。此函數(shù)接受到客戶端的連接;添加客戶端套接字信息和一個(gè) bufferevent 結(jié)構(gòu);在事件結(jié)構(gòu)中為客戶端套接字上的讀/寫/錯(cuò)誤事件添加回調(diào)函數(shù);作為參數(shù)傳遞客戶端結(jié)構(gòu)(和嵌入的 eventbuffer 和客戶端套接字)。每當(dāng)對應(yīng)的客戶端套接字包含讀、寫或錯(cuò)誤操作時(shí),調(diào)用對應(yīng)的回調(diào)函數(shù)。
buf_read_callback():當(dāng)客戶端套接字有要讀的數(shù)據(jù)時(shí)調(diào)用它。作為回顯服務(wù),此函數(shù)把 "you said..." 寫回客戶端。套接字仍然打開,可以接受新請求。
buf_write_callback():當(dāng)有要寫的數(shù)據(jù)時(shí)調(diào)用它。在這個(gè)簡單的服務(wù)中,不需要此函數(shù),所以定義是空的。
buf_error_callback():當(dāng)出現(xiàn)錯(cuò)誤時(shí)調(diào)用它。這包括客戶端中斷連接。在出現(xiàn)錯(cuò)誤的所有場景中,關(guān)閉客戶端套接字,從事件列表中刪除客戶端套接字的事件條目,釋放客戶端結(jié)構(gòu)的內(nèi)存。
setnonblock():設(shè)置網(wǎng)絡(luò)套接字以開放 I/O。
當(dāng)客戶端連接時(shí),在事件隊(duì)列中添加新事件以處理客戶端連接;當(dāng)客戶端中斷連接時(shí)刪除事件。在幕后,libevent 處理網(wǎng)絡(luò)套接字,識(shí)別需要服務(wù)的客戶端,分別調(diào)用對應(yīng)的函數(shù)。
為了構(gòu)建這個(gè)應(yīng)用程序,需要編譯 C 源代碼并添加 libevent 庫:$ gcc -o basic basic.c -levent。
從客戶端的角度來看,這個(gè)服務(wù)器僅僅把發(fā)送給它的任何文本發(fā)送回來(見 清單 3)。
清單 3. 服務(wù)器把發(fā)送給它的文本發(fā)送回來
復(fù)制代碼 代碼如下:
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello!
You said Hello!
這樣的網(wǎng)絡(luò)應(yīng)用程序非常適合需要處理多個(gè)連接的大規(guī)模分布式部署,比如 IBM Cloud 系統(tǒng)。
很難通過簡單的解決方案觀察處理大量并發(fā)連接的情況和性能改進(jìn)。可以使用嵌入的 HTTP 實(shí)現(xiàn)幫助了解可伸縮性。
使用內(nèi)置的 HTTP 服務(wù)器
如果希望構(gòu)建本機(jī)應(yīng)用程序,可以使用一般的基于網(wǎng)絡(luò)的 libevent 接口;但是,越來越常見的場景是開發(fā)基于 HTTP 協(xié)議的應(yīng)用程序,以及裝載或動(dòng)態(tài)地重新裝載信息的網(wǎng)頁。如果使用任何 AJAX 庫,客戶端就需要 HTTP,即使您返回的信息是 XML 或 JSON。
libevent 中的 HTTP 實(shí)現(xiàn)并不是 Apache HTTP 服務(wù)器的替代品,而是適用于與云和 web 環(huán)境相關(guān)聯(lián)的大規(guī)模動(dòng)態(tài)內(nèi)容的實(shí)用解決方案。例如,可以在 IBM Cloud 或其他解決方案中部署基于 libevent 的接口。因?yàn)榭梢允褂?HTTP 進(jìn)行通信,服務(wù)器可以與其他組件集成。
要想使用 libevent 服務(wù),需要使用與主要網(wǎng)絡(luò)事件模型相同的基本結(jié)構(gòu),但是還必須處理網(wǎng)絡(luò)接口,HTTP 包裝器會(huì)替您處理。這使整個(gè)過程變成四個(gè)函數(shù)調(diào)用(初始化、啟動(dòng) HTTP 服務(wù)器、設(shè)置 HTTP 回調(diào)函數(shù)和進(jìn)入事件循環(huán)),再加上發(fā)送回?cái)?shù)據(jù)的回調(diào)函數(shù)。清單 4 給出一個(gè)非常簡單的示例:
清單 4. 使用 libevent 服務(wù)的簡單示例
復(fù)制代碼 代碼如下:
#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);
}
應(yīng)該可以通過前面的示例看出代碼的基本結(jié)構(gòu),不需要解釋。主要元素是 evhttp_set_gencb() 函數(shù)(它設(shè)置當(dāng)收到 HTTP 請求時(shí)要使用的回調(diào)函數(shù))和 generic_request_handler() 回調(diào)函數(shù)本身(它用一個(gè)表示成功的簡單消息填充響應(yīng)緩沖區(qū))。
HTTP 包裝器提供許多其他功能。例如,有一個(gè)請求解析器,它會(huì)從典型的請求中提取出查詢參數(shù)(就像處理 CGI 請求一樣)。還可以設(shè)置在不同的請求路徑中要觸發(fā)的處理函數(shù)。通過設(shè)置不同的回調(diào)函數(shù)和處理函數(shù),可以使用路徑 '/db/' 提供到數(shù)據(jù)庫的接口,或使用 '/memc' 提供到 memcached 的接口。
libevent 工具包的另一個(gè)特性是支持通用計(jì)時(shí)器??梢栽谥付ǖ臅r(shí)間段之后觸發(fā)事件??梢酝ㄟ^結(jié)合使用計(jì)時(shí)器和 HTTP 實(shí)現(xiàn)提供輕量的服務(wù),從而自動(dòng)地提供文件內(nèi)容,在修改文件內(nèi)容時(shí)更新返回的數(shù)據(jù)。例如,以前要想在新聞?lì)l發(fā)的活動(dòng)期間提供即時(shí)更新服務(wù),前端 web 應(yīng)用程序就需要定期重新裝載新聞稿,而現(xiàn)在可以輕松地提供內(nèi)容。整個(gè)應(yīng)用程序(和 web 服務(wù))都在內(nèi)存中,因此響應(yīng)非常快。
這就是 清單 5 中的示例的主要用途:
清單 5. 使用計(jì)時(shí)器在新聞?lì)l發(fā)的活動(dòng)期間提供即時(shí)更新服務(wù)
復(fù)制代碼 代碼如下:
#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();
}
這個(gè)服務(wù)器的基本原理與前面的示例相同。首先,腳本設(shè)置一個(gè) HTTP 服務(wù)器,它只響應(yīng)對基本 URL 主機(jī)/端口組合的請求(不處理請求 URI)。第一步是裝載文件 (read_file())。在裝載最初的文件時(shí)和在計(jì)時(shí)器觸發(fā)回調(diào)時(shí)都使用此函數(shù)。
read_file() 函數(shù)使用 stat() 函數(shù)調(diào)用檢查文件的修改時(shí)間,只有在上一次裝載之后修改了文件的情況下,它才重新讀取文件的內(nèi)容。此函數(shù)通過調(diào)用 fread() 裝載文件數(shù)據(jù),把數(shù)據(jù)復(fù)制到另一個(gè)結(jié)構(gòu)中,然后使用 strcpy() 把數(shù)據(jù)從裝載的字符串轉(zhuǎn)移到全局字符串中。
load_file() 函數(shù)是觸發(fā)計(jì)時(shí)器時(shí)調(diào)用的函數(shù)。它通過調(diào)用 read_file() 裝載內(nèi)容,然后使用 RELOAD_TIMEOUT 值設(shè)置計(jì)時(shí)器,作為嘗試裝載文件之前的秒數(shù)。libevent 計(jì)時(shí)器使用 timeval 結(jié)構(gòu),允許按秒和毫秒指定計(jì)時(shí)器。計(jì)時(shí)器不是周期性的;當(dāng)觸發(fā)計(jì)時(shí)器事件時(shí)設(shè)置它,然后從事件隊(duì)列中刪除事件。
使用與前面的示例相同的格式編譯代碼:$ gcc -o basichttpfile basichttpfile.c -levent。
現(xiàn)在,創(chuàng)建作為數(shù)據(jù)使用的靜態(tài)文件;默認(rèn)文件是 sample.html,但是可以通過命令行上的第一個(gè)參數(shù)指定任何文件(見 清單 6)。
清單 6. 創(chuàng)建作為數(shù)據(jù)使用的靜態(tài)文件
復(fù)制代碼 代碼如下:
$ ./basichttpfile
Loading file: sample.html (8046 bytes)
Server started on port 8081
現(xiàn)在,程序可以接受請求了,重新裝載計(jì)時(shí)器也啟動(dòng)了。如果修改 sample.html 的內(nèi)容,應(yīng)該會(huì)重新裝載此文件并在日志中記錄一個(gè)消息。例如,清單 7 中的輸出顯示初始裝載和兩次重新裝載:
清單 7. 輸出顯示初始裝載和兩次重新裝載
復(fù)制代碼 代碼如下:
$ ./basichttpfile
Loading file: sample.html (8046 bytes)
Server started on port 8081
Reloading file: sample.html (8047 bytes)
Reloading file: sample.html (8048 bytes)
注意,要想獲得最大的收益,必須確保環(huán)境沒有限制打開的文件描述符數(shù)量??梢允褂?ulimit 命令修改限制(需要適當(dāng)?shù)臋?quán)限或根訪問)。具體的設(shè)置取決與您的 OS,但是在 Linux® 上可以用 -n 選項(xiàng)設(shè)置打開的文件描述符(和網(wǎng)絡(luò)套接字)的數(shù)量:
清單 8. 用 -n 選項(xiàng)設(shè)置打開的文件描述符數(shù)量
$ ulimit -n
1024
通過指定數(shù)字提高限制:$ ulimit -n 20000。
可以使用 Apache Bench 2 (ab2) 等性能基準(zhǔn)測試應(yīng)用程序檢查服務(wù)器的性能??梢灾付úl(fā)查詢的數(shù)量以及請求的總數(shù)。例如,使用 100,000 個(gè)請求運(yùn)行基準(zhǔn)測試,并發(fā)請求數(shù)量為 1000 個(gè):$ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/。
使用服務(wù)器示例中所示的 8K 文件運(yùn)行這個(gè)示例系統(tǒng),獲得的結(jié)果為大約每秒處理 11,000 個(gè)請求。請記住,這個(gè) libevent 服務(wù)器在單一線程中運(yùn)行,而且單一客戶端不太可能給服務(wù)器造成壓力,因?yàn)樗€受到打開請求的方法的限制。盡管如此,在交換的文檔大小適中的情況下,這樣的處理速率對于單線程應(yīng)用程序來說仍然令人吃驚。
--------------------------------------------------------------------------------
回頁首
使用其他語言的實(shí)現(xiàn)
盡管 C 語言很適合許多系統(tǒng)應(yīng)用程序,但是在現(xiàn)代環(huán)境中不經(jīng)常使用 C 語言,腳本語言更靈活、更實(shí)用。幸運(yùn)的是,Perl 和 PHP 等大多數(shù)腳本語言是用 C 編寫的,所以可以通過擴(kuò)展模塊使用 libevent 等 C 庫。
例如,清單 9 給出 Perl 網(wǎng)絡(luò)服務(wù)器腳本的基本結(jié)構(gòu)。accept_callback() 函數(shù)與 清單 1 所示核心 libevent 示例中的 accept 函數(shù)相同。
清單 9. Perl 網(wǎng)絡(luò)服務(wù)器腳本的基本結(jié)構(gòu)
復(fù)制代碼 代碼如下:
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 實(shí)現(xiàn)通常支持 libevent 系統(tǒng)的核心,但是不一定支持 HTTP 包裝器。因此,對腳本編程的應(yīng)用程序使用這些解決方案會(huì)比較復(fù)雜。有兩種方法:要么把腳本語言嵌入到基于 C 的 libevent 應(yīng)用程序中,要么使用基于腳本語言環(huán)境構(gòu)建的眾多 HTTP 實(shí)現(xiàn)之一。例如,Python 包含功能很強(qiáng)的 HTTP 服務(wù)器類 (httplib/httplib2)。
應(yīng)該指出一點(diǎn):在腳本語言中沒有什么東西是無法用 C 重新實(shí)現(xiàn)的。但是,要考慮到開發(fā)時(shí)間的限制,而且與現(xiàn)有代碼集成可能更重要。
libev 庫
與 libevent 一樣,libev 系統(tǒng)也是基于事件循環(huán)的系統(tǒng),它在 poll()、select() 等機(jī)制的本機(jī)實(shí)現(xiàn)的基礎(chǔ)上提供基于事件的循環(huán)。到我撰寫本文時(shí),libev 實(shí)現(xiàn)的開銷更低,能夠?qū)崿F(xiàn)更好的基準(zhǔn)測試結(jié)果。libev API 比較原始,沒有 HTTP 包裝器,但是 libev 支持在實(shí)現(xiàn)中內(nèi)置更多事件類型。例如,一種 evstat 實(shí)現(xiàn)可以監(jiān)視多個(gè)文件的屬性變動(dòng),可以在 清單 4 所示的 HTTP 文件解決方案中使用它。
但是,libevent 和 libev 的基本過程是相同的。創(chuàng)建所需的網(wǎng)絡(luò)監(jiān)聽套接字,注冊在執(zhí)行期間要調(diào)用的事件,然后啟動(dòng)主事件循環(huán),讓 libev 處理過程的其余部分。
例如,可以使用 Ruby 接口按照與清單 1 相似的方式提供回顯服務(wù)器,見 清單 10。
清單 10. 使用 Ruby 接口提供回顯服務(wù)器
復(fù)制代碼 代碼如下:
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 實(shí)現(xiàn)尤其出色,因?yàn)樗鼮樵S多常用的網(wǎng)絡(luò)解決方案提供了包裝器,包括 HTTP 客戶端、OpenSSL 和 DNS。其他腳本語言實(shí)現(xiàn)包括功能全面的 Perl 和 Python 實(shí)現(xiàn),您可以試一試。
結(jié)束語
libevent 和 libev 都提供靈活且強(qiáng)大的環(huán)境,支持為處理服務(wù)器端或客戶端請求實(shí)現(xiàn)高性能網(wǎng)絡(luò)(和其他 I/O)接口。目標(biāo)是以高效(CPU/RAM 使用量低)的方式支持?jǐn)?shù)千甚至數(shù)萬個(gè)連接。在本文中,您看到了一些示例,包括 libevent 中內(nèi)置的 HTTP 服務(wù),可以使用這些技術(shù)支持基于 IBM Cloud、EC2 或 AJAX 的 web 應(yīng)用程序。
學(xué)習(xí)
- C10K problem 對處理 10,000 個(gè)連接的問題做了精彩的概述。
- IBM Cloud Computing 網(wǎng)站提供不同云實(shí)現(xiàn)的相關(guān)信息。
- 閱讀 系統(tǒng)管理工具包: 標(biāo)準(zhǔn)化您的 UNIX 命令行工具(Martin Brown,developerWorks,2006 年 5 月),學(xué)習(xí)如何跨多臺(tái)機(jī)器使用相同的命令。
- 讓 UNIX 和 Linux 一起工作(Martin Brown,developerWorks,2006 年 4 月)講解如何讓傳統(tǒng)的 UNIX 發(fā)行版和 Linux 一起工作。
- 揭秘云計(jì)算(Brett McLaughlin,developerWorks,2009 年 3 月):幫助您根據(jù)自己的應(yīng)用程序需求選擇最好的云計(jì)算平臺(tái)。
- 閱讀 用 Amazon Web Services 進(jìn)行云計(jì)算(Prabhakar Chaganti,developerWorks,2008 年 7 月):詳細(xì)講解如何使用 Amazon Web Services。
- 可以通過 developerWorks Cloud Computing Resource Center 使用適用于 Amazon EC2 平臺(tái)的 IBM 產(chǎn)品。
- developerWorks Cloud Computing Resource Center 使用適用于 Amazon EC2 平臺(tái)的 IBM 產(chǎn)品。
- 在 developerWorks 的 云開發(fā)人員資源 中,發(fā)現(xiàn)和共享應(yīng)用程序和服務(wù)開發(fā)人員構(gòu)建其云部署項(xiàng)目的知識(shí)和經(jīng)驗(yàn)。
- AIX and UNIX 專區(qū):developerWorks 的“AIX and UNIX 專區(qū)”提供了大量與 AIX 系統(tǒng)管理的所有方面相關(guān)的信息,您可以利用它們來擴(kuò)展自己的 UNIX 技能。
- AIX and UNIX 新手入門:訪問“AIX and UNIX 新手入門”頁面可了解更多關(guān)于 AIX 和 UNIX 的內(nèi)容。
- AIX and UNIX 專題匯總:AIX and UNIX 專區(qū)已經(jīng)為您推出了很多的技術(shù)專題,為您總結(jié)了很多熱門的知識(shí)點(diǎn)。我們在后面還會(huì)繼續(xù)推出很多相關(guān)的熱門專題給您,為了方便您的訪問,我們在這里為您把本專區(qū)的所有專題進(jìn)行匯總,讓您更方便的找到您需要的內(nèi)容。
- AIX and UNIX 下載中心:在這里你可以下載到可以運(yùn)行在 AIX 或者是 UNIX 系統(tǒng)上的 IBM 服務(wù)器軟件以及工具,讓您可以提前免費(fèi)試用他們的強(qiáng)大功能。
- IBM Systems Magazine for AIX 中文版:本雜志的內(nèi)容更加關(guān)注于趨勢和企業(yè)級(jí)架構(gòu)應(yīng)用方面的內(nèi)容,同時(shí)對于新興的技術(shù)、產(chǎn)品、應(yīng)用方式等也有很深入的探討。IBM Systems Magazine 的內(nèi)容都是由十分資深的業(yè)內(nèi)人士撰寫的,包括 IBM 的合作伙伴、IBM 的主機(jī)工程師以及高級(jí)管理人員。所以,從這些內(nèi)容中,您可以了解到更高層次的應(yīng)用理念,讓您在選擇和應(yīng)用 IBM 系統(tǒng)時(shí)有一個(gè)更好的認(rèn)識(shí)。
- 在 developerWorks 播客 上收聽面向軟件開發(fā)人員的有趣訪談和討論。
- developerWorks 技術(shù)活動(dòng)和網(wǎng)絡(luò)廣播:隨時(shí)關(guān)注 developerWorks 技術(shù)活動(dòng)和網(wǎng)絡(luò)廣播。
獲得產(chǎn)品和技術(shù)
- 獲取 libev 庫,包括下載和文檔。
- 獲取 libevent 庫。
- ruby libev (rev) 庫和文檔。
- Memcached 是用于存儲(chǔ)和處理數(shù)據(jù)的 RAM 緩存(其核心使用 libevent,也可以使用其他 libevent 服務(wù)器)。
- 使用 IBM 試用軟件 改進(jìn)您的下一個(gè)開放源碼開發(fā)項(xiàng)目,這些軟件可以通過下載或從 DVD 獲得。
相關(guān)文章
使用FileZilla從Linux系統(tǒng)下載文件的方法
最近做項(xiàng)目,遇到這樣的需求,要求將Linux系統(tǒng)的的某個(gè)文件夾下載到我Windows系統(tǒng)某個(gè)文件夾里,怎么實(shí)現(xiàn)這個(gè)功能呢?下面腳本之家小編給大家?guī)砹耸褂肍ileZilla從Linux系統(tǒng)下載文件的方法,感興趣的朋友一起看看吧2018-07-07LNMP下FTP服務(wù)器的安裝與使用方法(Pureftpd和Proftpd)
FTP是網(wǎng)站文件維護(hù)中使用比較多的,目前LNMP一鍵安裝包中有Pureftpd和Proftpd服務(wù)器安裝腳本2013-06-06Apache開啟并實(shí)現(xiàn)網(wǎng)站偽靜態(tài)化的方法
Apache的Mod_Rewrite對URL進(jìn)行重寫,實(shí)現(xiàn)網(wǎng)站偽靜態(tài)化,這里就為大家介紹一下2022-08-08