2 слова о серверах
Сегодня я хочу поделиться технологией создания масштабируемого сервера на основе linux pthreads и epoll.
Общий принцип программы прост как паровоз: предварительно созданный пул потоков, обрабатывает входящие соединения в порядке живой очереди.
Теперь подробнее:
создаём слушающий сокет, причём настоятельно рекомендую использовать опции SO_REUSEADDR и TCP_NODELAY. Да, ещё рекомендую явно указать линжер в нули.
После этого – время создавать потоки. Каждому создаваемому потоку – передаём параметром сокет созданный ранее.
pthread_create(&th[t_cnt], &ta,(void*(*)(void *)) server_thread,(void *)&sock)
server_thread – это указатель на функцию потока
sock – это и есть указатель на серверный сокет созданный в начале.
Вот как-то так.
Теперь самое главное – функция потока обрабатывающая соединения:
void *server_thread(void *fd) { int *sock = (int *)fd; int localSock = *sock;
Так преобразовываем поинтер в сокет. Ну, на всякий случай, если кто не знает.
struct epoll_event ev, *events; ep_fd = epoll_create(num_sockets_per_sender); events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * epoll_room_in_receiver); ev.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLET; ev.data.fd = localSock; epoll_ctl(ep_fd, EPOLL_CTL_ADD,localSock, &ev);
Создаём структуру для еполла. Вообще принцип epoll – прост. В структуру записываются сокеты, на которых отслеживаются изменения. Т.е. если в какой-то сокет прошла запись клиентом – ядро (epoll обслуживается ядром Linux) – подаёт сигнал, который вытаскивает программу из ожидания в функции epoll_wait.
Вот как-то так:
while (1) { int num_fds = epoll_wait(ep_fd, events, num_sockets_per_sender, -1); ......... };
Сиё работает крайне просто, и является абсолютно thread-safe.
Все потоки которые вы создали, заваливаются на epoll_wait, и ждут когда в ep_fd появятся изменения – когда подключится новый клиент, или напишет чего-нибудь кто-то из уже подключенных клиентов.
Ну а дальше – совсем просто. Как только что-то появляется (в количестве в пределах 3 параметра функции epoll_wait) – функция возвращает количество евентов, ну и структуру с сокетами в которых произошли эвенты, и описания происшедших эвентов.
Т.е. например:
if (events.events & (EPOLLHUP | EPOLLERR)) { fputs("epoll: EPOLLERR", stderr); close(events.data.fd); continue; };
В общем-то просто и понятно.
В этом цикле и должна происходить обработка соединений – чтение, запись, закрытие сокетов.
Самое важное в этом процессе – добавлять новые сокеты в структуру epoll.
if (events.data.fd == localSock) { struct sockaddr remote_addr; socklen_t addr_size = sizeof(remote_addr); int connection = accept(localSock, &remote_addr, &addr_size); if (connection == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { serr("accept"); } continue; }; fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0)); ev.data.fd = connection; epoll_ctl(ep_fd, EPOLL_CTL_ADD, connection, &ev); continue; };
Идея проста – если событие произошло на серверном сокете – это означает только одно – появился новый клиент. Т.е. надо принимать коннект и добавлять в структуру epoll для дальнейшего мониторинга.
Ну и двигаясь к концу – пара общих советов, которые вы и так наверняка знаете:
При работе с еполлом – используйте не блокирующиеся сокеты. Это показано выше, но обращу отдельное внимание.
fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0));
При работе с многопоточными приложениями, не надо бояться использовать потоки. В некоторых случаях потоки вообще единственно приемлемое решение.
Мутексы. Мутексы это основа многопоточности. Однако, злоупотребляя мутексами – можно свести на ноль все преимущества многопоточной системы.
Да, два слова о производительности:
Сервер, кусочки кода которого я тут приводил, продемонстрировал примерно вот такие результаты: при 900 обращениях в секунду, полная обработка 1 соединения занимает примерно 27 миллисекунд. Со всей математикой, регекспами, обсчётами, массивами и даже поиском по огромным бинарным деревьям.
Сервер – Dual Quad Core Xeon 2.4Ghz, x64 Debian, 24GB ram.
Из дополнительных параметров компиляции – только -msse3 (только если у вас современный Intel), -O3 (общий набор оптимизации компилятора g++).



