вторник, 25 января 2011 г.

Пишем многопоточный сервер с использованием libev

Понадобилось тут вдруг у одного проекта переписать его сетевую часть, т.к. старая не справлялась, долго искал в инете примеры реализации, но увы безуспешно, поэтому и дал себе обещание написать об этом в блоге. Статья предназначена для тех кто уже имеет представление о сетевом программировании на Си.
Задачи:

  • Сервер должен иметь реализацию сокетов через libev
  • Сокеты должны быть не блокирующими (non-blocking)
  • Должно быть N нитей (pthread), которые независимо будут слать сообщения всем подключенным клиентам
  • Как минимум, не гнутся от DoS`елки по типу slowloris.
Не буду перегружать статью большими рассказами, да и не умею я их, по этому выложу только основные цитаты и ссылки:
libev:
A full-featured and high-performance (see benchmark) event loop that is loosely modelled after libevent, but without its limitations and bugs. It is used, among others, in the GNU Virtual Private Ethernet and rxvt-unicodepackages, and in the Deliantra MORPG Server and Client.
Документация, достаточно полная: http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod
Чем ещё хорош libev, кроме простоты написания с помощью его API, так это то, что библиотека может использовать различные методы получения состояния сокетов (select, kqueue, epoll(что в нашем случае), и некоторые другие).
FAQ appendix 1: как писать сервера:
https://groups.google.com/group/fido7.ru.unix.prog/browse_thread/thread/e8f8edf4f2f2447b/?hl=ru&pli=1
Linux: epoll performance and gotcha's:
http://radialmind.blogspot.com/2009/09/linux-epoll-performance-and-gotchas.html
Хабр. Еще раз об архитектуре сетевых демонов:
http://habrahabr.ru/blogs/hi/108294/
The C10K problem: (старый, но познавательный документ)
http://www.kegel.com/c10k.html
POSIX thread (pthread) libraries:
http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html
Расписывать участки кода не буду, думаю комментариев в нём должно хватить, если нет - спрашивайте.

libev_server_nonblock.c :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <fcntl.h>
#include <errno.h>
#include <ev.h>

#define BIND_PORT 12345

struct ev_loop *loop;
long minfd, maxfd;
char fds[4096];  // Массив с подключенными клиентами.

/*
 * Нить, отсылающая текущее время каждому подключенному клиенту.
 */

static void *sender(void *_arg)
{
time_t now;
char *ct, buf[1024];
long i;

while (1) {
now = time(NULL);
ct = ctime(&now);
sprintf(buf, "%ld: %s\n", (long)_arg, ct);

/* Цикл поиска клиентов, шелестит массив fds[] */
for (= minfd; i <= maxfd; i++)
if (fds[i] == 1) {
write(i, buf, strlen(buf));
printf("sended to fd %ld\n", i);
}

sleep((long)_arg);
}

return 0;
}

/*
 * Функция, которую вызываем когда сокет переключается на приём.
 */

void read_connection(EV_P_ struct ev_io *w, int revents)
{
int size, buf_size = 1024;
char buf[1024];
size = read(w->fd, buf, buf_size);
printf("read message - '%s' from fd #%i\n", buf, w->fd);

/* Если размер пришедшего пакета <= 0 - отрубаем. */
if (size <= 0) {
if( size == -1 && errno == EAGAIN )
printf("\t EAGAIN\n");

fds[(long)w->fd] = 0;
ev_io_stop(loop, w);
close(w->fd);
free(w);
printf("\t -> closed connection (fd %i)\n", w->fd);

return;
}

write(w->fd, "Hi\n", strlen("Hi\n")); // Отправка пакета
}

/*
 * Функция, вызываемая при инициализации соединения.
 */

void accept_connection(EV_P_ struct ev_io *w, int revents)
{
printf("accept connection from fd #%i\n", w->fd);
struct ev_io *io = malloc(sizeof(struct ev_io));
struct sockaddr sa;
socklen_t sizeof_sa = sizeof(sa);
long fd = accept(w->fd, &sa, &sizeof_sa);
if (fd <= 0) return;

fds[fd] = 1; // Делаем пометку что клиент подключен.
printf("fds[%ld] = 1;\n", fd);

if (fd > maxfd)
maxfd = fd;

fcntl(fd, F_SETFL, O_NONBLOCK);  // Превращаем сокет fd в неблокирующий.
ev_io_init(io, read_connection, fd, EV_READ);
ev_io_start(loop, io);
}

/*
 * Мейн, он и в Африке мейн.
 */

int main()
{
pthread_t thread_sender;
struct sockaddr_in addr;
int fd;

/* Создание и запуск нитей, с передачей им времени рецикла */
pthread_create( &thread_sender, NULL, sender, (void *)5);
pthread_create( &thread_sender, NULL, sender, (void *)3);

/* Создаём сокет сервера */
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
return -1;
}

/* Создание сокета сервера и привязки к адресу сокета */
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(BIND_PORT);
addr.sin_addr.s_addr = INADDR_ANY;

/* Биндимся */
if (bind(fd, (struct sockaddr*) &addr, sizeof(addr)) != 0) {
perror("bind error");
return -1;
}

/* Начинаем слушать сокет, сервер стартовал.
 * Чтобы мы могли одновременно принимать много соединений,
 * выставляем backlog на 128 */

if (listen(fd, 128) < 0) {
perror("listen error");
return -1;
}

minfd = fd + 1;
maxfd = minfd;

loop = ev_default_loop(EVBACKEND_EPOLL);

/* Создаём наблюдателя за принятыми соединениями,
 * принимаем соединения от клиентов
 * с использованием 'accept' API libev. */

struct ev_io *io = malloc(sizeof(struct ev_io));
ev_io_init(io, accept_connection, fd, EV_READ);
ev_io_start(loop, io);

/* Начало цикла событий.
 * Последним шагом будет запустить
 * бесконечный цикл событий ждать их.*/

while (1)
ev_loop(loop, 10);

return 0;
}
Компилируем командой `gcc -o "libev_server_nonblock" "libev_server_nonblock.c" -lpthread -lev`,
Версии софта:
Linux 2.6.37-lqx x86_64 AMD Athlon(tm) 64 X2 Dual Core Processor 3800+ AuthenticAMD GNU/Linux
gcc (GCC) 4.5.2

Отдельное спасибо людям с ЛОРа.

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

  1. Скоро подправлю/дополню эту версию, т.к. в ней есть несколько не очень хороших моментов.

    ОтветитьУдалить
  2. А можно на пальцах в чём примущество над libevent? И что-то я не нашёл, кто её использует в своих проектах.

    ОтветитьУдалить
  3. http://libev.schmorp.de/bench.html

    Перед выбором libev, я пробежался по обсуждениям по этой теме, в основном аргументы были "libev написан адекватнее, меньше воды/мусора, перспективнее"

    Сам не сравнивал их, особо сказать ничего не могу, только пожалуй то, что меня вполне устраивает (не считая малой распространённости, в частности рунета)

    ОтветитьУдалить
  4. Вдруг кто будет пробовать на FreeBSD
    чуток нужно поправить:

    diff -r 0b9505dab680 test-libev-nonblock/compile-server
    --- a/test-libev-nonblock/compile-server Thu Oct 25 15:09:37 2012 +0400
    +++ b/test-libev-nonblock/compile-server Thu Oct 25 12:37:25 2012 +0100
    @@ -1,1 +1,2 @@
    -gcc -o "libev_server_nonblock" "libev_server_nonblock.c" -lpthread -lev
    +gcc -o "libev_server_nonblock" "libev_server_nonblock.c" -lpthread -lev -I/usr/local/include -L/usr/local/lib
    +
    diff -r 0b9505dab680 test-libev-nonblock/libev_server_nonblock.c
    --- a/test-libev-nonblock/libev_server_nonblock.c Thu Oct 25 15:09:37 2012 +0400
    +++ b/test-libev-nonblock/libev_server_nonblock.c Thu Oct 25 12:37:25 2012 +0100
    @@ -1,4 +1,4 @@
    - #include
    + #include
    #include
    #include
    #include
    @@ -7,6 +7,9 @@
    #include
    #include
    #include
    + #include //[+]PPA
    + #include
    +

    ОтветитьУдалить
  5. Упс.
    Сожрал блог инклуды.
    #include netinet/in.h
    #include sys/socket.h

    ОтветитьУдалить
  6. А что это за магическое число 10 передается вторым параметром в ev_loop? Поделитесь сакральным знанием, пожалуйста

    ОтветитьУдалить
  7. Это ад, как можно такое вообще выкладывать, не понимаю.

    ОтветитьУдалить
  8. не правильно как-то все...
    потоки сами по себе шерстят дескрипторы

    ОтветитьУдалить
  9. Я тупой. Нет, правда. 2016-ый год уже скоро.

    А что значит (void*) 5, и (void*) 3 ? Я правильно понимаю что вы кастуете число 5 как адрес ? как 0x5. А что там по этому адресу ?
    pthread_create( &thread_sender, NULL, sender, (void *)5);
    pthread_create( &thread_sender, NULL, sender, (void *)3);

    ОтветитьУдалить
    Ответы
    1. Видимо, да. И, судя по всему, потом забираем как "(long)_arg".

      Удалить