Мультиплексирование ввода-вывода
Существует множество способов написать сервер, обрабатывающий клиентский запросы. Вот некоторые из них:
Существует множество способов написать сервер, обрабатывающий клиентский запросы. Вот некоторые из них:
- Последовательная обработка каждого запроса;
- Один процесс/поток на клиента;
- Мультиплексирование ввода-вывода.
Каждый подход обладает своими достоинствами и недостатками.
Я опишу мультиплексирование ввода вывода.
Принцип его работы в следующем: дескрипторы ввода/вывода помещается в список ожидания, затем процесс блокируется до наступления одного из событий:
Принцип его работы в следующем: дескрипторы ввода/вывода помещается в список ожидания, затем процесс блокируется до наступления одного из событий:
Для читающего сокета:
- Можно выполнить чтение данных. Т.е. количество байт в буфере приёмнике больше или равно значению минимального количества данных (по умолчанию это значение равно 1, но его можно поменять с помощью параметра сокета SO_RCVLOWAT);
- На противоположном конце соединение закрывается;
- Сокет является прослушиваемым и число установленных соединений больше нуля.
- Ошибка сокета;
Для пишущего сокета:
- Можно выполнить запись данных. Т.е. количество байт в буфере передатчике больше или равно значению минимального количества данных (по умолчанию это значение равно 2048, но его можно поменять с помощью параметра сокета SO_SNDLOWAT);
- На противоположном конце соединение закрывается. При этом если записать данные в сокет, сгенерируется сигнал SIGPIPE;
- Ошибка сокета;
Также процесс выходит из состояния блокировки при получении внеполосных данных.
epoll
Системные вызовы группы epoll присущи только Linux. Они появились в ядре версии 2.6 и призваны исправить проблемы системных вызовов poll и select.
Работа с epoll осуществляется в три этапа:
1. Создаём дескриптор epoll, вызвав функцию
int epoll_create(int size);
2. Добавляем дескриптор ввода/вывода в массив ожидания, вызвав функцию
3. Запускаем ожидание готовности, вызвав функцию
За более подробной информацией по параметрам и возвращающим значениям функций, необходимо обратиться к документации.
Вот небольшой пример кода, использующий epoll:
int epoll_ctl(int efd, int op, int fd, struct epool_event* event);
3. Запускаем ожидание готовности, вызвав функцию
int epoll_wait(int efd, struct epoll_event *events, int maxevents, int timeout);
За более подробной информацией по параметрам и возвращающим значениям функций, необходимо обратиться к документации.
Вот небольшой пример кода, использующий epoll:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <iostream>
namespace
{
int setnonblocking(int sock);
void do_read(int fd);
void do_write(int fd);
void process_error(int fd);
}
int main(int argc, char* argv[])
{
const int MAX_EPOLL_EVENTS = 100;
const int BACK_LOG = 100;
if (argc < 2)
{
std::cout << "Usage: server [port]" << std::endl;
return 0;
}
char* p;
int serv_port = strtol(argv[1], &p, 10);
if (*p)
{
std::cout << "Invalid port number" << std::endl;
return -1;
}
// Игнорируем сигнал ошибки работы с сокетом
signal(SIGPIPE, SIG_IGN);
// Создание дескриптора epoll
int efd = epoll_create(MAX_EPOLL_EVENTS);
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("Socket creation");
return -1;
}
// Неблокирующий режим
setnonblocking(listenfd);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(serv_port);
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
perror("Socket bind");
return -1;
}
if (listen(listenfd, BACK_LOG) < 0)
{
perror("Socket listen");
return -1;
}
// Добавляем дескриптор в массив ожидания
struct epoll_event listenev;
listenev.events = EPOLLIN | EPOLLPRI | EPOLLET;
listenev.data.fd = listenfd;
if (epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &listenev) < 0)
{
perror("Epoll fd add");
return -1;
}
socklen_t client;
// Массив готовых дескрипторов
struct epoll_event events[MAX_EPOLL_EVENTS];
struct epoll_event connev;
struct sockaddr_in cliaddr;
int events_cout = 1;
for (;;)
{
// Блокирование до готовности одно или нескольких дескрипторов
int nfds = epoll_wait(efd, events, MAX_EPOLL_EVENTS, -1);
for (int n = 0; n < nfds; ++n)
{
// Готов слушающий дескриптор
if (events[n].data.fd == listenfd)
{
client = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &client);
if (connfd < 0)
{
perror("accept");
continue;
}
// Недостаточно места в массиве ожидания
if (events_cout == MAX_EPOLL_EVENTS-1)
{
std::cout << "Event array is full" << std::endl;
close(connfd);
continue;
}
// Добавление клиентского дескриптора в массив ожидания
setnonblocking(connfd);
connev.data.fd = connfd;
connev.events = EPOLLIN | EPOLLOUT | EPOLLET | EPOLLRDHUP;
if (!epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &connev) < 0)
{
perror("Epoll fd add");
close(connfd);
continue;
}
++events_cout;
}
// Готов клиентский дескриптор
else
{
// Выполням работу с дескриптором
int fd = events[n].data.fd;
if (events[n].events & EPOLLIN)
do_read(fd);
if (events[n].events & EPOLLOUT)
do_write(fd);
if (events[n].events & EPOLLRDHUP)
process_error(fd);
// В даннoм примере дескриптор просто закрывается и удаляется из массива ожидания.
// В зависимости от логики работы можно не удалять дескриптор и подождать следующую порцию данных
epoll_ctl(efd, EPOLL_CTL_DEL, fd, &connev);
--events_cout;
close(fd);
}
}
}
return 0;
}
namespace
{
int setnonblocking(int sock)
{
int opts;
opts = fcntl(sock,F_GETFL);
if (opts < 0)
{
perror("fcntl(F_GETFL)");
return -1;
}
opts = (opts | O_NONBLOCK);
if (fcntl(sock,F_SETFL,opts) < 0)
{
perror("fcntl(F_SETFL)");
return -1;
}
return 0;
}
void do_read(int fd)
{
std::cout << "do_read" << std::endl;
}
void do_write(int fd)
{
std::cout << "do_write" << std::endl;
}
void process_error(int fd)
{
std::cout << "process_error" << std::endl;
}
}
if (events_cout == MAX_EPOLL_EVENTS-1)
ОтветитьУдалить{
std::cout << "Event array is full" << std::endl;
close(connfd);
continue;
}
в условии, насколько я понимаю, должно быть условие на количество активных подклбючений, а не максимальное кол-во ивентов от epoll-а.
Также при ожидании EPOLLOUT будет спам событий do_write (он срабатывает когда вы можете записать, то есть сокет работает и не переполнен)