четверг, 7 мая 2009 г.

Написание своего HTTP сервера с использованием libevent

Библиотека libevent содержит в себе простейший асинхронный HTTP сервер, который можно
без особого труда встроить в собственное приложение для обслуживания HTTP запросов.

Для использования этой возможности достаточно добавить в код следующее:

1. Подключить заголовочный файл <evhttp.h>:
#include <evhttp.h>

2. Инициализировать базу событийного движка:
event_base * serv_base = (event_base *)event_init();

3. Инициализировать HTTP сервер:
evhttp * http_server = evhttp_new(serv_base);

4. Указать, на каком сокете слушать подключения:
evhttp_accept_socket(http_server, server_sock);

5. Выставить callback'и на запросы. Можно добавлять на каждый URI свой обработчик:
evhttp_set_cb(http_server, "/news", on_request_news, NULL);

6. Выставить обработчик на остальные запросы:
evhttp_set_gencb(http_server, on_request, NULL);

7. Запустить цикл обработки запросов:
event_base_dispatch(serv_base);


Реализация HTTP сервера является потоко-безопасной (thread safe).

Есть еще один нюанс: настоятельно рекомендуется игнорировать сигнал SIGPIPE.
Делается это следующим вызовом:
signal(SIGPIPE, SIG_IGN);


Описание всех функция для работы с HTTP протоколом
можно найти в документации.

Вот работающий пример простого HTTP сервера, который на все запросы отдает динамическую
страничку с некоторой информацией о клиенте.

/*
* \file: http_server.cpp
* \description: Simple HTTP server
*
*/
#include <errno.h>
#include <event.h>
#include <evhttp.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <signal.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <iostream>

const short  SERVER_BACKLOG    = 128;
const short  BUF_LEN           = 26;
const char   RESPONCE[BUF_LEN] = "<H1>Hello there</H1><BR/>";
const char * SERVER_NAME       = "Simple HTTP Server";

void on_request(struct evhttp_request *, void *);

int main(int argc, char **argv)
{
if (argc < 3) {
std::cout << "Start as:" << std::endl 
<< argv[0] << " host_address port" << std::endl;
return 1;
}

int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
std::cout << "Error socket(): " << strerror(errno) << std::endl;
return 1;
}

u_short      port  = atol(argv[2]);
const char * host  = argv[1];
sockaddr_in  sa;
int          on    = 1;
sa.sin_family      = AF_INET;
sa.sin_port        = htons(port);
sa.sin_addr.s_addr = inet_addr(host);

if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
std::cout << "Error setsockopt(): " << strerror(errno) << std::endl;
return 1;
}

// Bind server socket to ip:port
if (bind(server_sock, (const sockaddr*)&sa, sizeof(sa)) == -1) {
std::cout << "Error bind(): " << strerror(errno) << " on: " << host << ":" << port << std::endl;
return 1;
}
// Make server to listen
if (listen(server_sock, SERVER_BACKLOG) == -1) {
std::cout << "Error listen(): " << strerror(errno) << std::endl;
return 1;
}
// Init events
event_base * serv_base   = (event_base *)event_init();
evhttp     * http_server = evhttp_new(serv_base);

// Ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);

if (evhttp_accept_socket(http_server, server_sock) == -1) {
std::cout << "Error evhttp_accept_socket(): " << strerror(errno) << std::endl;
return 1;
}
// Set HTTP request callback                               
evhttp_set_gencb(http_server, on_request, NULL);
// Dispatch events
event_base_dispatch(serv_base);

return 0;
}

void on_request(struct evhttp_request * req, void * arg)
{
// Create responce buffer
struct evbuffer *evb = evbuffer_new();
if (!evb) { return; }

// Add heading text
evbuffer_add_printf(evb, "<HTML><HEAD><TITLE>%s Page</TITLE></HEAD><BODY>\n", SERVER_NAME);
// Add buffer
evbuffer_add(evb, RESPONCE, BUF_LEN);
// Add formatted text
evbuffer_add_printf(evb, "Your request is <B>%s</B> from <B>%s</B>.<BR/>Your user agent is '%s'\n", 
req->uri, req->remote_host, evhttp_find_header(req->input_headers, "User-Agent"));
// Add footer
evbuffer_add_printf(evb, "</BODY></HTML>");

// Set HTTP headers
evhttp_add_header(req->output_headers, "Server", SERVER_NAME);
evhttp_add_header(req->output_headers, "Connection", "close");

// Send reply
evhttp_send_reply(req, HTTP_OK, "OK", evb);

// Free memory
evbuffer_free(evb);
}


Makefile для сборки:
SRCS=http_server.cpp
LDFLAGS=-levent
CXXFLAGS=-I/usr/local/include -L/usr/local/lib
PROG=http_server

all:
c++ ${SRCS} -o ${PROG} ${LDFLAGS} ${CXXFLAGS}

clean:
rm -f ${PROG}


А вот результаты тестирования Apache Benchmark:
ab -c 100 -n 1000 "http://192.168.1.110:10000/test"

Server Software:        SimpleHTTPServer
Server Hostname:        192.168.1.110
Server Port:            10000

Document Path:          /test
Document Length:        199 bytes

Concurrency Level:      100
Time taken for tests:   0.185 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      308000 bytes
HTML transferred:       199000 bytes
Requests per second:    5393.74 [#/sec] (mean)
Time per request:       18.540 [ms] (mean)
Time per request:       0.185 [ms] (mean, across all concurrent requests)
Transfer rate:          1622.34 [Kbytes/sec] received

Connection Times (ms)
min  mean[+/-sd] median   max
Connect:        0    5   2.8      5      16
Processing:     5   13   3.9     13      23
Waiting:        2   10   3.9      9      22
Total:         11   18   3.4     18      27

Percentage of the requests served within a certain time (ms)
50%     18
66%     19
75%     20
80%     21
90%     22
95%     24
98%     25
99%     26
100%     27 (longest request)


Сервер был запущен локально на моем ноутбуке, на сервере результаты конечно же получше.
Но для сотни параллельных запросов – это неплохо.

5 комментариев:

T-Rex комментирует...

Круть. А все ж интересно, это добро под WinMobile собирается? Было бы неплохо HTTP-сервер на мобилке настроить :) Я даже у кого-то такое видел. Можно было через Интернет на мобилку зайти и там чото типа новоестей было или какое-то простенькое подобие твиттера.

romartyn комментирует...

Скомпилировал. Запускаю. Вешаю на localhost 32. Ни один http-клиент не может к нему подсоединиться, говорят 'connection refued' ... Печально!(( Хотя, вот как здесь http://www.linuxhowtos.org/C_C++/socket.htm всё пашет, то есть проблем с сетями у меня нет.

romartyn комментирует...

Всё же удалось заставить работать. Не знаю, почему у Вас сокет не открывался, но у меня сервер отдаёт ответы. У Вас ещё ошибка в описании того, как добавлять события: пропущен слеш перед news:

evhttp_set_cb(http_server, "/news", on_request_news, NULL);

Без него выполняется обработчик по-умолчанию.

Андрей Булавинов комментирует...
Этот комментарий был удален автором.
Андрей Булавинов комментирует...

Спасибо, поправлю.