Кроссплатформенный https сервер с неблокирующими сокетами. Часть 3

от автора

В этой статье я продолжаю усовершенствовать однопоточный 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_create\n"); 				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» „

ссылка на оригинал статьи http://habrahabr.ru/post/212101/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *