Кроссплатформенный 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: l0.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» „

    Продолжение
    Поделиться публикацией
    Комментарии 11
      +7
      Я оставлю бесконечный цикл в конструкторе по одной причине:

      не нужно гордиться незнанием C++. если тебе в классе нужен всего один метод, то подумай нужен ли тебе класс.
      всего того же самого но без сайд эффектов можно добиться засунув цикл в статическую функцию класса и вызывать его допустим так
      Server::Run()
        0
        Я очень мало знаю о этой теме, но не раз слышал, что в подобных ситуациях использовать select в windows — не самая лучшая идея. Вот, напрмер, тут написано почему.
          +1
          Я продолжу писать код в заголовочных файлах по ряду причин:…
          Те кто уверен, что так как я делать нельзя — можете поучить программированию создателей stl и boost сначала, а потом переименовать файл server.h в server.cpp и будет всем хорошо…

          Если весь код можно писать в заголовках, зачем необходимо линковать программы, использующие stl, с libstdc++.so?
            +1
            >Я не буду использовать std::copy вместо memcpy по одной причине: std::copy — тормоз!
            Да вы издеваетесь! Подсказка: компилятор генерирует *абсолютно* одинаковый код.

            Скрытый текст
            #include <algorithm>
            #include <cstring>
            
            void f(char *dst, const char *src, size_t length) {
                std::copy(src, src + length, dst);
            }
            
            void g(char *dst, const char *src, size_t length) {
                std::memcpy(dst, src, length);
            }
            


            /tmp/tmpi3vn0y.o:     file format elf64-x86-64-freebsd
            
            Disassembly of section .text:
            
            0000000000000000 <_Z1fPcPKcm>:
               0:   55                      push   rbp
               1:   48 89 e5                mov    rbp,rsp
               4:   e8 00 00 00 00          call   9 <_Z1fPcPKcm+0x9>
               9:   5d                      pop    rbp
               a:   c3                      ret
               b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax+0x0]
            
            0000000000000010 <_Z1gPcPKcm>:
              10:   55                      push   rbp
              11:   48 89 e5                mov    rbp,rsp
              14:   e8 00 00 00 00          call   19 <_Z1gPcPKcm+0x9>
              19:   5d                      pop    rbp
              1a:   c3                      ret
            

              0
              Согласен, погорячился. Разница есть, но она видна не в самих функциях copy и memcpy, а при задании их аргументов.
              С++
              int main()
              {
              	std::vector<unsigned char> v1(10), v2(10);
              
              	for (int n=0; n<v1.size(); n++)
              	{
              		v1[n] = n;
              	}
              
              	std::copy(&v2[0], &v2[v2.size() - 1], &v1[0]);
              	memcpy(&v2[0], &v1[0], v2.size());
              
              	return 0;
              }
              


              Ассемблер:
              
              	std::copy(&v2[0], &v2[v2.size() - 1], &v1[0]);
              01371088  mov         ebx,dword ptr [v2]  
              0137108B  mov         esi,dword ptr [ebp-24h]  
              0137108E  sub         esi,ebx  
              01371090  lea         eax,[esi-1]  
              01371093  push        eax  
              01371094  push        ebx  
              01371095  push        edi  
              01371096  call        dword ptr ds:[13720A0h]  
              	memcpy(&v2[0], &v1[0], v2.size());
              0137109C  push        esi  
              0137109D  push        edi  
              0137109E  push        ebx  
              0137109F  call        _memcpy (01371AAAh)  
              


              Пожалуй не такой уж std::copy тормоз, подумаю еще раз над ее использованием )

            +1
            Скажу сразу: весь исходник вашего проекта и статьи целиком не читал, но пробежал глазами.
            Меня всегда радуют люди, которые пытаются развиваться. Самоустремлённость — отличное качество человека. Желаю вам не останавливаться.
            Радует и то, что вы прислушиваетесь к критике. Проект сразу собрался под Linux указанной вами строкой.

            Теперь вопросы.

            1. Планируете ли добавить кросплатформенную систему сборки (к примеру, CMake)? Ведь кросплатформенность одних только исходников ущемляет возможности пользователей ОС, отличных от вашей.
              Плюсы:
              • Для вас: новый полезный инструмент
              • Для других: компиляция одной-двумя привычными командами вместо копирования строки gcc, разрастающейся с ростом проекта
              • Для всех: никаких сложностей при развитии проекта

            2. Планируете ли использовать систему контроля версий? Будет проще отслеживать изменения, делиться исходными кодами и управлять проектом.
            3. Будете ли включать предупреждения компилятора? Смотрите, сколько дефектов (потенциально — багов) я нашёл в коде, всего лишь добавив при компиляции флаги "-Wall -Werror":
              • server.h:77:8: error: `server::CClient::m_pSSL` will be initialized after
                в списке инициализации m_pSSL должно стоять после m_pSSLContext, поскольку оно объявлено ниже в классе. Причина, по которой вам это может быть полезным.
              • server.h:300:45: error: comparison between signed and unsigned integer expressions [-Werror=sign-compare]
                     if (strInputString.find("\r\n\r\n") != -1)
                
                string::find возвращает не -1 в случае отсутствия элемента, а string::npos.
              • ./server.h:446:10: error: logical not is only applied to the left hand side of this comparison [-Werror,-Wlogical-not-parentheses]
                                                        if (!m_events[i].events == EPOLLIN)
                

                (выдаёт Clang) возможно, вы хотели написать
                if (m_events[i].events != EPOLLIN)

              • пачка предупреждений о том, что const для возвращаемых значений RETCODES не обязателен.

            4. Недопонял, что делают строки вида "#define EPOLLIN EPOLLIN". Можете пояснить?
            5. Ну и придирки по стилю: избавление от «using namspace std;» и «printf» улучшит вашу карму (Человеческую, не хабрахабровую. Хотя, кто знает?) и сделает код более единообразным.

            Спасибо за статьи.
              –2
              Спасибо за подробный и объективный комментарий.
              Попробую ответить в меру своих компетенций.

              Q: Планируете ли добавить кросплатформенную систему сборки (к примеру, CMake)?
              A: В принципе да, но пока нет. Объясню почему:
              во-первых, моей основной средой разработки является Visual Studio, по этой причине я очень мало знаю про инструменты разработчиков под Linux и другие платформы. Я несколько раз пытался осилить make, но всякий раз ловил себя на мысли, что не понимаю: «кто и зачем это придумал и кому это нужно».
              Ну правда: мне до сих пор всегда в линуксе хватало создать исполняемый файл с именем «compile» и написать в него:
              #!/bin/sh
              g++ ...
              rm *.o *.gch ...
              

              Но, повторюсь, когда я осознаю необходимость CMake, то постараюсь его добавить.

              Q:Планируете ли использовать систему контроля версий?
              A:Серьезно над этим думаю в настоящее время. Не могу определиться с репозиторием: когда-то пользовался sourceforge, в последнее время много вижу ссылок на github.
              Для меня главные критерии: простой неглючный клиент для Windows и поддержка русского языка (у sourceforge с этим вроде были проблемы).

              Q:Будете ли включать предупреждения компилятора?
              A:Спасибо, за найденные ошибки. Буду исправлять.
              Предупреждения компилятора постараюсь использовать активнее, но только в рабочих версиях. В релизах они не к чему имхо.

              Q:Недопонял, что делают строки вида "#define EPOLLIN EPOLLIN". Можете пояснить?
              A: Понятия не имею )))
              На сколько я помню, это копипаст отсюда.

              Q:Ну и придирки по стилю: избавление от «using namspace std;» и «printf»
              A: От printf пожалуй избавлюсь — это наследие от самого первого исходника, который почти целиком из OpenSSL.
              Насчет «using namspace std» у меня свое мнение:
              я понимаю, что данная строка очень редко кем используется и видимо считается «плохим тоном». Но вот мне лично дурным кажется например такой код
              std::shared_ptr<std::map<std::string, std::vector<std::string>>>
              

              там, где вместо этого ужаса можно написать:
              using namspace std;
              shared_ptr<map<string, vector<string> > >
              

              Не знаю, но по моему, правило «не пишите using namspace» придумали враги c++. Так что не вижу смысла следовать правилам, если они затрудняют жизнь.
                0
                Предупреждения компилятора постараюсь использовать активнее, но только в рабочих версиях. В релизах они не к чему имхо.
                Wut? o_O
                0
                что делают строки вида "#define EPOLLIN EPOLLIN". Можете пояснить?
                Скорее всего, когда-то давно enum не было, а константы были объявлены через #define
                #define EPOLLIN 0x001
                

                Затем их перенесли в enum, но возможно, где-то остался код, который проверяет, что константы объявлены и переобъявляет их в противном случае (ну или еще как-то полагается на тот факт, что константы объявлены через препроцессор)
                #ifndef EPOLLIN
                #define EPOLLIN 1
                #endif
                ...
                foo(EPOLLIN );
                ...
                

                Чтобы не ломать такой код, и были добавлены #define, в актуальной версии просто ссылающиеся на enum.
                +1
                libev (http://libev.schmorp.de/) и libevent (http://libevent.org/) — библиотеки, которые предоставляют event loop (а также много всего вкусного, включая таймауты), абстрагируясь от конкретных особенностей реализации механизмов ожидания событий операционной системы (epoll, kqueue, select). Есть ли хоть одна причина «эмулировать epoll через select»?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое