В этой статье я продолжаю усовершенствовать однопоточный https сервер на неблокирующих сокетах. Предыдущие статьи с ссылками на исходный код, можно найти здесь:
Простейший кросcплатформенный сервер с поддержкой ssl
Кроссплатформенный https сервер с неблокирующими сокетами
Кроссплатформенный https сервер с неблокирующими сокетами. Часть 2
В конце этой статьи будет ссылка на исходный код сервера, который я протестировал в Visual Studio 2012 (Windows 8 64bit), g++4.4 (Linux 32bit), g++4.6 (Linux 64bit). Сервер принимает соединения от любого количества клиентов и отправляет в ответ заголовки запроса.
Но начну я статью пожалуй, с ответов на некоторые комментарии к предыдущим.
Во-первых, получив массу негативных откликов о необычности своего кода, отныне я решил свои статьи помещать еще и в хаб «Ненормальное программирование».
Во-вторых, я решил больше не ставить пометку «tutorial»: кто-то найдет что-то новое в моих статьях, а кому-то они покажутся дилетантскими. Я не против…
Теперь про мой стиль программирования:
1. Я продолжу писать код в заголовочных файлах по ряду причин:
а) Я хочу без дополнительных телодвижений знать полное количество строк кода и поэтому мне так удобней.
б) В любой момент я могу захотеть прикрутить к клиенту или серверу template, и не хотелось бы ради этого переписывать весь код.
Те кто уверен, что так как я делать нельзя — можете поучить программированию создателей stl и boost сначала, а потом переименовать файл server.h в server.cpp и будет всем хорошо…
2. Я оставлю бесконечный цикл в конструкторе по одной причине: считаю этот подход правильным. Если класс не делает больше ничего, кроме изменения своих внутренних переменных, то самым правильным будет оставить у этого класса публичной одну единственную функцию: его конструктор.
Можно конечно в этом случае вообще без класса, но с классом мне как-то привычней, да и глобальные функции на пустом месте тоже не нужны.
3. Я не буду использовать std::copy вместо memcpy по одной причине: std::copy — тормоз!
Наконец хочу поблагодарить всех, кто не поленился откомпилировать исходник и указать на некоторые ошибки. Я постарался их учесть и исправить.
Теперь о главном.
Чтобы сервер из предыдущей статьи наконец подготовить для парсинга заголовков запроса и раздаче файлов, осталось сделать одно маленькое дополнение: начать вместо бесконечного цикла использовать специально предназначенные для пассивного ожидания сетевых событий функции.
В Windows и Linux есть несколько таких функций, я предлагаю использовать select в Windows и epoll в Linux.
Есть проблема в том, что функции epoll в Windows не существует. Чтобы код выглядел единообразно во всех системах, давайте напишем код сервера так, как будто epoll в Windows есть!
Простая реализация epoll для Windows с помощью select
1. Добавим в проект Visual Studio два пустых файла из той же директории, где расположен «server.h». Файлы: «epoll.h» и «epoll.cpp».
2. Перенесем в файл epoll.h определения констант, структур и функций из документации по epoll:
#ifndef __linux__
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLONESHOT = (1 << 30),
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = (1 << 31)
#define EPOLLET EPOLLET
};
/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
#define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */
typedef union epoll_data
{
void *ptr;
int fd;
unsigned int u32;
unsigned __int64 u64;
} epoll_data_t;
struct epoll_event
{
unsigned __int64 events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
#endif
3. В файл epoll.cpp добавляем заголовки, а так же глобальную переменную, в которой будут храниться сокеты и их состояния:
#include "epoll.h"
#include <map>
#ifndef WIN32
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#else
#include <io.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#endif
std::map<int, epoll_event> g_mapSockets;
4. Добавляем код для первой функции:
int epoll_create(int size)
{
return 1;
}
Что тут происходит?
На сколько я могу судить по документации: оригинальный код в линуксе при каждом вызове epoll_create создает файл, в котором хранятся состояния сокетов. Видимо это нужно в многопоточных процессах.
У нас же процесс однопоточный и нам не нужно более одной структуры для хранения сокетов. Поэтому epoll_create у нас это «заглушка».
5. С помощью stl добавление и удаление сокетов в памяти происходит элементарно:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
{
switch(op)
{
case EPOLL_CTL_ADD:
case EPOLL_CTL_MOD:
g_mapSockets[fd] = *event;
return 0;
case EPOLL_CTL_DEL:
if (g_mapSockets.find(fd) == g_mapSockets.end())
return -1;
g_mapSockets.erase(fd);
return 0;
}
return 0;
}
6. Наконец главное: функцию ожидания реализуем через select
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
if ((!events) || (!maxevents))
return -1;
//Создаем и обнуляем структуры для функции select
fd_set readfds, writefds, exceptfds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
//Заполняем структуры сокетами
int nFDS = 0;
for (auto it=g_mapSockets.begin(); it != g_mapSockets.end(); ++it)
{
if (it->first == -1)
continue;
if (it->first > nFDS)
nFDS = it->first;
FD_SET(it->first, &readfds);
FD_SET(it->first, &writefds);
FD_SET(it->first, &exceptfds);
}
//Задаем интервал ожидания
struct timeval tv;
tv.tv_sec = timeout/1000;
tv.tv_usec = timeout - tv.tv_sec*1000;
//Ждем событий
nFDS++;
select(nFDS, &readfds, &writefds, &exceptfds, &tv);
//Заполняем структуру для отправки программе так, как будто она вызвала epoll
int nRetEvents = 0;
for (auto it=g_mapSockets.begin(); (it != g_mapSockets.end() && nRetEvents < maxevents); ++it)
{
if (it->first == -1)
continue;
if (!FD_ISSET(it->first, &readfds) && !FD_ISSET(it->first, &writefds) && !FD_ISSET(it->first, &exceptfds))
continue;
memcpy(&events[nRetEvents].data, &it->second.data, sizeof(epoll_data));
if (FD_ISSET(it->first, &readfds))
events[nRetEvents].events |= EPOLLIN;
if (FD_ISSET(it->first, &writefds))
events[nRetEvents].events |= EPOLLOUT;
if (FD_ISSET(it->first, &exceptfds))
events[nRetEvents].events |= EPOLLERR;
nRetEvents++;
}
return nRetEvents;
}
Вот и все. Функция epoll для Windows реализована!
Добавление epoll в сервер
1. Добавляем в заголовки:
#ifdef __linux__
#include <sys/epoll.h>
#else
#include "epoll.h"
#endif
2. В класс CServer добавляем строки:
private:
//События слушающего сокета
struct epoll_event m_ListenEvent;
//События клиентских сокетов
vector<struct epoll_event> m_events;
int m_epoll;
3. В конструкторе CServer все, что после вызова функции listen меняем на:
m_epoll = epoll_create (1);
if (m_epoll == -1)
{
printf("error: epoll_createn");
return;
}
m_ListenEvent.data.fd = listen_sd;
m_ListenEvent.events = EPOLLIN | EPOLLET;
epoll_ctl (m_epoll, EPOLL_CTL_ADD, listen_sd, &m_ListenEvent);
while(true)
{
m_events.resize(m_mapClients.size()+1);
int n = epoll_wait (m_epoll, &m_events[0], m_events.size(), 5000);
if (n == -1)
continue;
Callback(n);
}
4. Старую функцию CServer::Callback меняем на новую:
void Callback(const int nCount)
{
for (int i = 0; i < nCount; i++)
{
SOCKET hSocketIn = m_events[i].data.fd;
if (m_ListenEvent.data.fd == (int)hSocketIn)
{
if (!m_events[i].events == EPOLLIN)
continue;
struct sockaddr_in sa_cli;
size_t client_len = sizeof(sa_cli);
#ifdef WIN32
const SOCKET sd = accept (hSocketIn, (struct sockaddr*) &sa_cli, (int *)&client_len);
#else
const SOCKET sd = accept (hSocketIn, (struct sockaddr*) &sa_cli, (socklen_t *)&client_len);
#endif
if (sd != INVALID_SOCKET)
{
//Добавляем нового клиента в класс сервера
m_mapClients[sd] = shared_ptr<CClient>(new CClient(sd));
auto it = m_mapClients.find(sd);
if (it == m_mapClients.end())
continue;
//Добавляем нового клиента в epoll
struct epoll_event ev = it->second->GetEvent();
epoll_ctl (m_epoll, EPOLL_CTL_ADD, it->first, &ev);
}
continue;
}
auto it = m_mapClients.find(hSocketIn); //Находим клиента по сокету
if (it == m_mapClients.end())
continue;
if (!it->second->Continue()) //Делаем что-нибудь с клиентом
{
//Если клиент вернул false, то удаляем клиента из epoll и из класса сервера
epoll_ctl (m_epoll, EPOLL_CTL_DEL, it->first, NULL);
m_mapClients.erase(it);
}
}
}
С классом сервера закончили, осталось разобраться с классом CClient.
Добавим в него такой код:
private:
//События сокета клиента
struct epoll_event m_ClientEvent;
public:
const struct epoll_event GetEvent() const {return m_ClientEvent;}
И на этом добавление кода поддержки epoll закончено!
Вот тут находится проект для Visual Studio: c0.3s3s.org
Для компиляции в Linux файлы epoll.h и epoll.cpp не нужны, т.е все как обычно: «скопировать в одну директорию файлы: serv.cpp, server.h, ca-cert.pem и в командной строке набрать: «g++ -std=c++0x -L/usr/lib -lssl -lcrypto serv.cpp» „
Автор: 3s3s