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

  • Tutorial
Эта статья является продолжением статей:
Простейший кросcплатформенный сервер с поддержкой ssl
Кроссплатформенный https сервер с неблокирующими сокетами
В этих статьях я постепенно из простенького примера, входящего в состав OpenSSL стараюсь сделать полноценный однопоточный веб-сервер.
В предыдущей статье я «научил» сервер принимать соединение от одного клиента и отсылать обратно html страницу с заголовками запроса.
Сегодня я исправлю код сервера так, чтобы он мог обрабатывать соединения от произвольного количества клиентов в одном потоке.

Для начала я разобью код на два файла: serv.cpp и server.h
При этом файл serv.cpp будет содержать такой вот «высокоинтелектуальный» код:
#include "server.h"

int main()
{
	server::CServer();
	return 0;
}


Да, можете пинать меня ногами, но я все равно писал, пишу и буду писать код в заголовочных файлах если мне это удобно. За то я собственно и люблю с++, что он дает свободу выбора, но это отдельный разговор…

Переходим к файлу server.h
В его начало я перенес все заголовки, макросы и определения, которые раньше были в serv.cpp, и добавил еще пару заголовков из STL:

#ifndef _SERVER
#define _SERVER
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>

#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

#include <openssl/rsa.h>       /* SSLeay stuff */
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#include <vector>
#include <string>
#include <sstream>
#include <map>
#include <memory>

#ifdef WIN32
#define SET_NONBLOCK(socket)	\
	if (true)					\
	{							\
		DWORD dw = true;			\
		ioctlsocket(socket, FIONBIO, &dw);	\
	}
#else
#include <fcntl.h>
#define SET_NONBLOCK(socket)	\
	if (fcntl( socket, F_SETFL, fcntl( socket, F_GETFL, 0 ) | O_NONBLOCK ) < 0)	\
		printf("error in fcntl errno=%i\n", errno);
#define closesocket(socket)  close(socket)
#define Sleep(a) usleep(a*1000)
#define SOCKET	int
#define INVALID_SOCKET	-1
#endif


/* define HOME to be dir for key and cert files... */
#define HOME "./"
/* Make these what you want for cert & key files */
#define CERTF  HOME "ca-cert.pem"
#define KEYF  HOME  "ca-cert.pem"

#define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(1); }



Дальше создаем сначала классы CServer и CClient внутри namespace server:
using namespace std;
namespace server
{
	class CClient
	{
		//Дескриптор клиентского сокета
		SOCKET m_hSocket;
		//В этом буфере клиент будет хранить принятые данные
		vector<unsigned char> m_vRecvBuffer; 
		//В этом буфере клиент будет хранить отправляемые данные
		vector<unsigned char> m_vSendBuffer; 
		
		//Указатели для взаимодействия с OpenSSL
		SSL_CTX* m_pSSLContext;
		SSL* m_pSSL;

		//Нам не понадобится конструктор копирования для клиентов
		explicit CClient(const CClient &client) {} 
	public:
		CClient(const SOCKET hSocket) : 
			m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL) {}
		~CClient()
		{
			if(m_hSocket != INVALID_SOCKET) 
				closesocket(m_hSocket);
			if (m_pSSL)
				SSL_free (m_pSSL);
			if (m_pSSLContext)
				SSL_CTX_free (m_pSSLContext);
		}	 
	};

	class CServer
	{
		//Здесь сервер будет хранить всех клиентов
		map<SOCKET, shared_ptr<CClient> > m_mapClients;
		
		//Нам не понадобится конструктор копирования для сервера
		explicit CServer(const CServer &server) {}
	public:
		CServer() {}
	};
}

#endif


Как видите, это лишь заготовка для нашего сервера. Начнем потихоньку наполнять эту заготовку кодом, большая часть которого уже есть в предыдущей статье.
Для каждого клиента инициируется свой контекст SSL, очевидно делать это нужно в конструкторе класса CClient
		CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL)
		{
#ifdef WIN32
			const SSL_METHOD *meth = SSLv23_server_method();
#else
			SSL_METHOD *meth = SSLv23_server_method();
#endif
			m_pSSLContext = SSL_CTX_new (meth);
			if (!m_pSSLContext)
				ERR_print_errors_fp(stderr);
		
			if (SSL_CTX_use_certificate_file(m_pSSLContext, CERTF, SSL_FILETYPE_PEM) <= 0)
				ERR_print_errors_fp(stderr);
			if (SSL_CTX_use_PrivateKey_file(m_pSSLContext, KEYF, SSL_FILETYPE_PEM) <= 0)
				ERR_print_errors_fp(stderr);

			if (!SSL_CTX_check_private_key(m_pSSLContext))
				fprintf(stderr,"Private key does not match the certificate public key\n");
		}


Инициализацию библиотек, создание и привязку слушающего сокета перенесем с минимальными изменениями в конструктор CServer:
		CServer()
		{
#ifdef WIN32
			WSADATA wsaData;
			if ( WSAStartup( MAKEWORD( 2, 2 ), &wsaData ) != 0 )
			{
				printf("Could not to find usable WinSock in WSAStartup\n");
				return;
			}
#endif
			SSL_load_error_strings();
			SSLeay_add_ssl_algorithms();
			
			/* ----------------------------------------------- */
			/* Prepare TCP socket for receiving connections */

			SOCKET listen_sd = socket (AF_INET, SOCK_STREAM, 0);	  CHK_ERR(listen_sd, "socket");
			SET_NONBLOCK(listen_sd);
  
			struct sockaddr_in sa_serv;
			memset (&sa_serv, '\0', sizeof(sa_serv));
			sa_serv.sin_family      = AF_INET;
			sa_serv.sin_addr.s_addr = INADDR_ANY;
			sa_serv.sin_port        = htons (1111);          /* Server Port number */
  
			int err = ::bind(listen_sd, (struct sockaddr*) &sa_serv, sizeof (sa_serv));      CHK_ERR(err, "bind");
	     
			/* Receive a TCP connection. */
			
			err = listen (listen_sd, 5);            CHK_ERR(err, "listen");
		}


Дальше в этом же конструкторе я предлагаю принимать входящие TCP соединения.
Мне никто до сих пор не привел ни одного аргумента против, поэтому слушать TCP соединения мы будем в бесконечном цикле, как и в предыдущей статье.
После каждого вызова accept мы можем что-нибудь сделать с вновь подключившимся и с уже подключенными клиентами, вызвав callback функцию.
Добавим в конструктор CServer после функции listen код:


			while(true)
			{
				Sleep(1);

				struct sockaddr_in sa_cli;  
				size_t client_len = sizeof(sa_cli);
#ifdef WIN32
				const SOCKET sd = accept (listen_sd, (struct sockaddr*) &sa_cli, (int *)&client_len);
#else
				const SOCKET sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len);
#endif  
				Callback(sd);
			}


А сразу после конструктора, собственно callback функцию:
	private:
		void Callback(const SOCKET hSocket)
		{
			if (hSocket != INVALID_SOCKET)
				m_mapClients[hSocket] = shared_ptr<CClient>(new CClient(hSocket)); //Добавляем нового клиента

			auto it = m_mapClients.begin();
			while (it != m_mapClients.end()) //Перечисляем всех клиентов
			{
			   if (!it->second->Continue()) //Делаем что-нибудь с клиентом
				  m_mapClients.erase(it++); //Если клиент вернул false, то удаляем клиента
			   else
				  it++;
			}
		}


На этом код класса CServer закончен! Вся остальная логика приложения будет в классе CClient.
Важно заметить, что для критичных к скорости проектов, вместо перебора всех клиентов в цикле, надо перебирать только тех клиентов, чьи сокеты готовы для чтения или записи.
Сделать этот перебор легко с помощью функций select в Windows или epoll в Linux. Я покажу как это делается в следующей статье,
А пока (рискуя опять нарваться на критику) все таки ограничусь простым циклом.

Переходим к основной «рабочей лошадке» нашего сервера: к классу CClient.
Класс CClient должен хранить в себе не только информацию о своем сокете, но и информацию о том, на каком этапе находится его взаимодействие с сервером.
Добавим в определение класса CClient следующий код:
	private:
		//Перечисляем все возможные состояния клиента. При желании можно добавлять новые.
		enum STATES 
		{ 
			S_ACCEPTED_TCP,
			S_ACCEPTED_SSL,
			S_READING,
			S_ALL_READED,
			S_WRITING,
			S_ALL_WRITED
		};
		STATES m_stateCurrent; //Здесь хранится текущее состояние

		//Функции для установки и получения состояния
		void SetState(const STATES state) {m_stateCurrent = state;}
		const STATES GetState() const {return m_stateCurrent;}
	public:
		//Функция для обработки текужего состояния клиента
		const bool Continue()
		{
			if (m_hSocket == INVALID_SOCKET)
				return false;

			switch (GetState())
			{
				case S_ACCEPTED_TCP:
					break;
				case S_ACCEPTED_SSL:
					break;
				case S_READING:
					break;
				case S_ALL_READED:
					break;
				case S_WRITING:
					break;
				case S_ALL_WRITED:
					break;
				default:

					return false;
			}
			return true;
		}


Здесь Continue() это пока только функция-заглушка, чуть ниже мы ее научим выполнять все действия с подключенным клиентом.

В конструкторе изменим:
CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL)

на
CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL), m_stateCurrent(S_ACCEPTED_TCP)


В зависимости от текущего состояния, клиент вызывает разные функции. Договоримся, что состояния клиента можно менять только в конструкторе и в функции Continue(), это немного увеличит размер кода, но зато сильно облегчит его отладку.

Итак первое состояние, которое клиент получает при создании в конструкторе: S_ACCEPTED_TCP.
Напишем функцию, которая будет вызываться клиентом до тех пор, пока у него это состояние:
Для этого строки:
				case S_ACCEPTED_TCP:
					break;


изменим на следующие:
				case S_ACCEPTED_TCP:
				{
					switch (AcceptSSL())
					{
						case RET_READY:
							printf ("SSL connection using %s\n", SSL_get_cipher (m_pSSL));
							SetState(S_ACCEPTED_SSL);
							break;
						case RET_ERROR:
							return false;
					}

					return true;
				}


А так же добавим следующий код в класс CClient:
	private:
		enum RETCODES
		{
			RET_WAIT,
			RET_READY,
			RET_ERROR
		};
		const RETCODES AcceptSSL()
		{
			if (!m_pSSLContext) //Наш сервер предназначен только для SSL
				return RET_ERROR;

			if (!m_pSSL)
			{
				m_pSSL = SSL_new (m_pSSLContext);
				
				if (!m_pSSL)
					return RET_ERROR;

				SSL_set_fd (m_pSSL, m_hSocket);
			}

			const int err = SSL_accept (m_pSSL); 

			const int nCode = SSL_get_error(m_pSSL, err);
			if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
				return RET_READY;

			return RET_WAIT;
		}


Теперь функция AcceptSSL() будет вызываться клиентом до тех пор, пока не произойдет зашифрованное подключение или пока не возникнет ошибка.

1. В случае ошибки функция CClient::AcceptSSL() вернет код RET_ERROR в вызваашую ее функцию CClient::Continue(), которая в этом случае вернет false вызвавшей ее функции CServer::Callback, которая в этом случае удалит клиента из памяти сервера.
2. В случае удачного подключения функция CClient::AcceptSSL() вернет код RET_READY в вызвавшую ее функцию CClient::Continue(), которая в этом случае изменит состояние клиента на S_ACCEPTED_SSL.

Теперь добавим функцию обработки состояния S_ACCEPTED_SSL. Для этого строки

				case S_ACCEPTED_SSL:
					break;


исправим на следующие:
				case S_ACCEPTED_SSL:
				{
					switch (GetSertificate())
					{
						case RET_READY:
							SetState(S_READING);
							break;
						case RET_ERROR:
							return false;
					}

					return true;
				}


И добавим в CClient функцию:
		const RETCODES GetSertificate()
		{
			if (!m_pSSLContext || !m_pSSL) //Наш сервер предназначен только для SSL
				return RET_ERROR;
			
			/* Get client's certificate (note: beware of dynamic allocation) - opt */

			X509* client_cert = SSL_get_peer_certificate (m_pSSL);
			if (client_cert != NULL) 
			{
				printf ("Client certificate:\n");
    
				char* str = X509_NAME_oneline (X509_get_subject_name (client_cert), 0, 0);
				if (!str)
					return RET_ERROR;

				printf ("\t subject: %s\n", str);
				OPENSSL_free (str);
    
				str = X509_NAME_oneline (X509_get_issuer_name  (client_cert), 0, 0);
				if (!str)
					return RET_ERROR;

				printf ("\t issuer: %s\n", str);
				OPENSSL_free (str);
    
				/* We could do all sorts of certificate verification stuff here before
					deallocating the certificate. */
    
				X509_free (client_cert);
			} 
			else
				printf ("Client does not have certificate.\n");

			return RET_READY;
		}


Эта функция, в отличие от предыдущей, вызовется всего один раз и вернет в CClient::Continue либо RET_ERROR либо RET_READY. Соответственно CClient::Continue вернет либо false, либо изменит состояние клиента на S_READING.

Дальше все аналогично: изменим код
				case S_READING:
					break;
				case S_ALL_READED:
					break;
				case S_WRITING:
					break;


на такой:
				case S_READING:
				{
					switch (ContinueRead())
					{
						case RET_READY:
							SetState(S_ALL_READED);
							break;
						case RET_ERROR:
							return false;
					}

					return true;
				}
				case S_ALL_READED:
				{
					switch (InitRead())
					{
						case RET_READY:
							SetState(S_WRITING);
							break;
						case RET_ERROR:
							return false;
					}

					return true;
				}
				case S_WRITING:
				{
					switch (ContinueWrite())
					{
						case RET_READY:
							SetState(S_ALL_WRITED);
							break;
						case RET_ERROR:
							return false;
					}

					return true;
				}


И добавляем соответствующие функции обработки состояний:
		const RETCODES ContinueRead()
		{
			if (!m_pSSLContext || !m_pSSL) //Наш сервер предназначен только для SSL
				return RET_ERROR;

			unsigned char szBuffer[4096];
			
			const int err = SSL_read (m_pSSL, szBuffer, 4096); //читаем данные от клиента в буфер
			if (err > 0)
			{
				//Сохраним прочитанные данные в переменной m_vRecvBuffer
				m_vRecvBuffer.resize(m_vRecvBuffer.size()+err);
				memcpy(&m_vRecvBuffer[m_vRecvBuffer.size()-err], szBuffer, err);
		 
				//Ищем конец http заголовка в прочитанных данных
				const std::string strInputString((const char *)&m_vRecvBuffer[0]);
				if (strInputString.find("\r\n\r\n") != -1)
					return RET_READY;

				return RET_WAIT;
			}

			const int nCode = SSL_get_error(m_pSSL, err);
			if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
				return RET_ERROR;
			
			return RET_WAIT;
		}

		const RETCODES InitRead()
		{
			if (!m_pSSLContext || !m_pSSL) //Наш сервер предназначен только для SSL
				return RET_ERROR;

			//Преобразуем буфер в строку для удобства
			const std::string strInputString((const char *)&m_vRecvBuffer[0]);

			//Формируем html страницу с ответом сервера
			const std::string strHTML = 
				  "<html><body><h2>Hello! Your HTTP headers is:</h2><br><pre>" + 
				  strInputString.substr(0, strInputString.find("\r\n\r\n")) + 
				  "</pre></body></html>";

			//Добавляем в начало ответа http заголовок
 			std::ostringstream strStream;
			strStream << 
					"HTTP/1.1 200 OK\r\n"
					<< "Content-Type: text/html; charset=utf-8\r\n"
					<< "Content-Length: " << strHTML.length() << "\r\n" <<
					"\r\n" <<
					strHTML.c_str();

			//Запоминаем ответ, который хотим послать
			m_vSendBuffer.resize(strStream.str().length());
			memcpy(&m_vSendBuffer[0], strStream.str().c_str(), strStream.str().length());

			return RET_READY;
		}
		const RETCODES ContinueWrite()
		{
			if (!m_pSSLContext || !m_pSSL) //Наш сервер предназначен только для SSL
				return RET_ERROR;

			int err = SSL_write (m_pSSL, &m_vSendBuffer[0], m_vSendBuffer.size());
			if (err > 0)
			{
				//Если удалось послать все данные, то переходим к следующему состоянию
				if (err == m_vSendBuffer.size())
					return RET_READY;

				//Если отослали не все данные, то оставим в буфере только то, что еще не послано
				vector<unsigned char> vTemp(m_vSendBuffer.size()-err);
				memcpy(&vTemp[0], &m_vSendBuffer[err], m_vSendBuffer.size()-err);
				m_vSendBuffer = vTemp;

				return RET_WAIT;
			}
	 
			const int nCode = SSL_get_error(m_pSSL, err);
			if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
				return RET_ERROR;

			return RET_WAIT;
		}


Наш сервер пока предназначен лишь для того, чтобы показывать клиенту заголовки его http запроса.
После того, как сервер выполнил свое предназначение, он может закрыть соединение и забыть про клиента.
Поэтому в наш код осталось внести последнее небольшое изменение:

				case S_ALL_WRITED:
					break;


нужно исправить на
				case S_ALL_WRITED:
					return false;


Вот и все! Теперь у нас есть кроссплатформенный однопоточный https сервер на неблокирующих сокетах, который может обрабатывать произвольное (ограниченное лишь памятью и настройками операционной системы) количество соединений.

Архив с проектом для Visual Studio 2012 можно скачать здесь: 01.3s3s.org
Чтобы скомпилировать в Linux надо скопировать в одну директорию файлы: serv.cpp, server.h, ca-cert.pem и в командной строке набрать: «g++ -std=c++0x -L/usr/lib -lssl -lcrypto serv.cpp»

Продолжение
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 26

    +5
    Ну ё-моё, select ведь прост как не знаю что. Почему нельзя сразу рассказывать правильно, а не эту ересь со sleep?
      –1
      select не подходит для очень большого количества соединений…
      Я select использую в винде, а в Linux epoll. В кроссплатформенном коде надо это учесть. В данной статье я счел нецелесообразным на это отвлекаться.
        +2
        Тогда уж в Windows следует использовать IOCP (Input/output completion port).
          +1
          select действительно плохо масштабируется, однако общая идея работы с ним мало отличается от оной в epoll/kqueue. Про IOCP не скажу.
            0
            Количество обрабатываемых сокетов в виндовом select зависит от размера FD_SET, этот размер меняется специальным дефайном. Подробности внизу этой страницы msdn.microsoft.com/en-us/library/windows/desktop/ms740141(v=vs.85).aspx
              0
              В винде начиная с висты есть WSAPoll, вроде как аналог линуксового pool.
              +3
              Скажите пожалуйста, а почему класс сервера написан так, что в конструкторе находится бесконечный цикл, и внутри вызываются методы этого же класса, когда он фактически является «недоинициализированным»?

              Может быть стоило добавить какой-нибудь метод, что-то вроде Start?
                +8
                Думаете, после заявления: «пишу и буду писать код в заголовочных файлах если мне это удобно», стоит читать код? :-)
                  0
                  Ну, я подумал, вдруг у человека на то есть необоснованные причины, равно как и смешивать два разных класса в одном заголовке.

                  А насчёт конструктора, может он руководствовался чем-нибудь…
                  –8
                  Сто раз так делал и ни разу не возникало проблем с «недоинициированностью». А если нет разницы, то зачем платить больше? (ц)
                    +4
                    Чисто для ржаки отнаследуйся и вызови виртуальную функцию наследника. Разница есть…
                      +3
                      Разница есть, и она значительна. То что вы лично до сих пор ее не обнаружили ничего не доказывает.
                      Как вам уже сказали будут проблемы с вызовами виртуальных функций. То что сегодня вам они не нужны не значит что не понадобятся завтра. Но что еще хуже — раз уж вы выложили свой код как учебный, то будьте так добры напишите код так чтобы он приносил больше добра чем зла.
                      А ваш проект, извините конечно, можно использовать как пособие по антипаттернам создания проектов.

                      И кстати вы пробовали собрать ваш кроссплатформенный проект под другими платформами? Я вот попробовал. Ошибок было две:
                      стока 81: добавить const к указателю.
                      строка 401: тут неправильный тип передается в функцию accept(). Дело в том что я собирал ваш код на 64 битной версии линукса, и здесь size_t имеют разную размерность socklen_t

                      Впрочем это были мелочи. Попробую перечислить более серьезные недостатки:

                      1. Конструктор Сервера это простите жопа. Да в си++ можно и не так писать, можно вообще значения из функций возвращать через throw-catch ( и тоже вам никто не запретит и может даже не докажет что так нельзя ). Но программа должна быть прозрачна и предсказуема. Если вы в коробке из-под перца с надписью чай лежит сахар — значит что-то у вас не так в консерватории.
                      Конструктор создает объект. Так уж повелось. Другие программисты не всегда смогут досконально изучить вашу программу — в идеале человеку нужно взглянуть на заголовки хорошо спроектированных классов чтобы представить себе как ими пользоваться. Вот поэтому нужны общие для всех понятия. Компилятору то на них наплевать.

                      Кстати в Кт3 была такая неприятная особенность у класса сокет — при создании объекта этого класса — сам сокет не создавался. А создавался он лишь при вызове функций сокет.акцепт или сокет.коннект.
                      Вроде бы логично ( эдакая оптимизация от разработчиков библиотеки ), но эта долбаная оптимизация забрала кучу нашего времени на попытки понять, почему дескриптр созданного объекта всегда невалидный. Это банальный пример неожиданного поведения чужой библиотеки. Так вот — поведение должно быть ожидаемым и логичным.

                      Идем дальше.
                      memcpy(&vTemp[0], &m_vSendBuffer[err], m_vSendBuffer.size()-err);
                      Так делать нельзя — memcpy не умеет копировать данные из пересекающихся участков памяти, тут нужен memmove ( а еще лучше уйти от сишных фукнций на std алгоритмы вроде copy )

                      Это из того на что упал взгляд. Вообще же мешанина стилей и подходов. Нет четкого разграничения уровней http, ssl и собственно сервера.

                      И попробую ответить на вопрос чем библиотеки лучше вашего собственного кода. Библиотеки хороши тем что решают огромное количество проблем о существовании которых вы не подозреваете. Кроме того они уже протестированы и уже работают.
                      Написание же своего кода — хорошо тем что вы лучше понимаете как и что работает ( что прекрасно подходит для обучения ). А также тем, что вы можете создать код для оптимального решения именно своей задачи. Впрочем это удел очень хороших специалистов.

                      зы. После статьи и комментариев остались смешанные чувства. С одной стороны — мне бы не хотелось чтобы кто-то начинал учить сетевое программирование ( или упаси Страуструп ) Си++ по этой статье. Слишком много тут дыр и ошибок.
                      С другой — мне не нравится когда старожилы хабра уводят в минуса людей с желанием писать программы и статьи. Пусть даже уровень этих статей далеко не дотягивает до уровня желаемого. Предлагаю автору попробовать переосмыслить свой подход, и хм надеюсь что он сумеет допустить что то что ему тут советуют имеет смысл.
                        +1
                        А мой взгляд вот за это зацепился:

                        while (it != m_mapClients.end()) //Перечисляем всех клиентов
                        {
                            if (!it->second->Continue()) //Делаем что-нибудь с клиентом
                                m_mapClients.erase(it++); //Если клиент вернул false, то удаляем клиента
                            else
                                it++;
                        }
                        


                        Должно быть вот так:

                        it = m_mapClients.erase(it);
                        


                        С другой — мне не нравится когда старожилы хабра уводят в минуса людей с желанием писать программы и статьи. Пусть даже уровень этих статей далеко не дотягивает до уровня желаемого.


                        Я не минусовал но подозреваю, что причина минусов в том, что автор категорически не хочет прислушиваться к мнению более опытных разработчиков (особенно это заметно по комментариям к первой статье). А с таким подходом далеко не продвинуться.
                          0
                          По-моему этот код как раз у автора написан правильно. Именно так и делается удаление элементов из мэпа, во время движения по нему, чтобы не сломать итератор. Ваш вариант по-моему не будет работать, во-первых map.erase возвращает void или size_t ( если передать ключ а не итератор ). Может у вас какая-то другая реализация мэпа, которая возвращает следующий за удаленным итератор?

                          >подозреваю, что причина минусов в том, что автор категорически не хочет прислушиваться к мнению более опытных разработчиков
                          Полностью с вами согласен. Автор сжав зубы отбивается от гадких людишек, посягнувших на его код и право писать так как он хочет. Жаль что он не допускает что ему тоже пытаются помочь. С другой стороны это и правда сложно — признать себя неправым и попытаться разобраться. Впрочем я в него верю =)
                            0
                            По-моему этот код как раз у автора написан правильно.
                            


                            Вы правы, что-то я уже забываю C++03 и полагаюсь на C++11 :-)
                          +2
                          Во-первых, разрешите Вас поблагодарить за столь подробный и в общем доброжелательный комментарий.
                          Теперь, несмотря на то, что мои ответы всегда притягивают только минусы, я все таки попробую прокомментировать ваш комментарий )))

                          1. Конструктор сервера
                          Вообще, у меня появилась мысль, что про это можно написать отдельную статью, но пока попробую тут описать идею.Вы пишите «конструктор создает объект». По моему, правильнее так:: «конструктор инициирует объект».
                          В чем отличие? В том, что инициация это задание начального состояния или, другими словами, задание начальных значений для внутренних переменных объекта. Согласны? Если нет, то поправьте меня, но я вижу роль конструкторов именно так.
                          Если же Вы со мной тут согласны, то дальше все логично: мой класс CServer имеет лишь одну внутреннюю переменную и весь смысл его существования — инициирование этой переменной.
                          Что качается вызовов виртуальных функций: опять же, следуя своему пониманию роли конструктора, у меня даже в мыслях не возникнет вызвать из конструктора не только виртуальную, но и любую другую не приватную функцию.

                          2. Пример с классом сокета
                          Честно говоря не понял — что вы им хотели сказать, на мой взгляд он лишь подтверждает правильность моего подхода: после конструктора все переменные класса должны иметь предсказуемое, валидное и желательно константное значение. В противном случае поведение класса может вызвать недоумение у сторонних разработчиков.

                          3. memcpy(&vTemp[0], &m_vSendBuffer[err], m_vSendBuffer.size()-err);
                          Я правда наверное очень плохо знаю с++ в целом и stl в частности, но убейте — не вижу здесь пересекающихся областей памяти (((
                          vTemp — временный локальный буфер, выделенный в функции, а m_vSendBuffer — переменная класса, память для которой выделяется в конструкторе. Как они могут пересекаться???

                          4. "… библиотеки лучше вашего собственного кода."
                          Полностью согласен с тем, что Вы пишите. Кроме пункта:
                          Впрочем это удел очень хороших специалистов.

                          Я лично убежден, что прежде чем использовать библиотеки, надо сначала научиться программировать без них. Если меня не забанят за рекордное количество минусов, то после статей про сервер на голых сокетах я планирую написать такую же серию про сервер на boost::asio. Но именно в таком порядке, а не наоборот. Уж простите…
                            +1
                            1. Про конструктор. Попробую представить это иначе. Используя ООП мы подразумеваем что мы оперируем объектами. Объекты имеют время жизни, состояние, набор событий которые с ними могут происходить. Так вначале они создаются, затем получают различные сигналы ( вызовы функций) меняют свое состояние и в конце уничтожаются. Объект — модель некой другой ментальной модели в голове разработчика. В случае сервера у объекта может быть функция выключающая его, которая корректно завершит все соединения. Может быть функция вызываются переинциалазацию каких-то данных, или функция приостанавливающая прием новых соединений.
                            В вашем случае не было никакого смыла городить целый класс ради того чтобы написать в нем код лишь конструктора — обошлись бы простой функций — никто бы вам слова не сказал. Просто использование классов подразумевает некую общепринятую модель, который вы не последовали.

                            2. Пример с классом сокета
                            Если бы я был пользоватем библиотеки в который реализован такой подход к серверу — я бы долго не мог понять как использовать ваш сервер. Скорее всего я бы решил что здесь какая-то ошибка, и что этот код не тут. Поэтому я говорю про схожую неожиданность поведения кода.

                            3. memcpy
                            Да тут я кажется ошибся. Видимо показлось что там копирование в ту же самую переменную. Кстати рекомендую вам посмотреть на std::copy. Если честно ваше использование memcpy вкупе с std::vector вызвало у меня легкий разрыв шаблона. Хоть я и узнал что это вполне допустимая операция с ним.

                            4. "… библиотеки лучше вашего собственного кода."
                            полностью согласен — учится лучше на своем коде. Может вы просто не написали что этот проект для вас учебный, я счел что вы хотите им научить чему-то других, менее опытных программистов.
                      +1
                      Небольшие замечания по коду, не более того…

                      Первой строкой в server.h было бы здорово написать
                      #pragma once

                      Коллеги по цеху Вас не поймут за такое:
                      using namespace std;

                      И в конце хотелось бы отметить, что заголовочные файлы C++ как-то принято именовать *.hh или *.hpp для обозначения намерений, а *.h — слишком общий вариант (это уже imho).
                        +2
                        В статье, со словами «кроссплатформенный» и «неблокирующие сокеты» в заголовке, должно быть как минимум упоминание о libev/libevent, а вообще странно что вы их не захотели использовать в реализации.
                          –3
                          Зачем о них упоминать?
                          Кто-то считает, что без libevent неблокирующих сокетов не бывает, кто-то считает что без boost::asio…
                          Я вот считаю, что с голыми сокетами вполне можно комфортно жить.
                          Сервер из статьи умещается в 425 строк кода. Сколько строк будет занимать точно такой же сервер как у меня, но реализованный на ваших любимых библиотеках?
                            +1
                            Столько же, но при этом он будет действительно кроссплатформенным (с автоматическим выбором лучшего асинхронного механизма для данной платформы, коих побольше, чем просто «линукс» и «виндоус»).
                            –1
                            Словари считают, что кроссплатформенность это «работа на нескольких платформах», а не «работа на любой платформе».
                              +2
                              Жесть какая… книг почитайте что ли по С++ и вообще по программированию, прежде чем других учить.
                              Чтобы скомпилировать в Linux надо скопировать в одну директорию файлы: serv.cpp, server.h, ca-cert.pem и в командной строке набрать: «g++ -std=c++0x -L/usr/lib -lssl -lcrypto serv.cpp»
                              Запомните: если хотите избавиться от дурацких ошибок ещё на этапе компиляции, всегда компилируйте код с параметром -Wall, а ещё лучше с -Wextra иногда прогонять. В MSVS после создания проекта нужно привыкнуть сразу же устанавливать ворнинги с дефолтного /W3 на /W4. Сразу же выбросятся примитивнейшие ошибки сравнения signed и unsigned, которые у вас на 284-й и 335-й строках.

                              Ещё как можно чаще нужно заглядывать в man\msdn:
                              sa_serv.sin_addr.s_addr = INADDR_ANY;
                              И можно узнать, что адрес в эту структуру передаётся так же как и порт — в сетевом порядке байт. Вам просто тут повезло с константой INADDR_ANY равной нулю. Ну ладно, спихнём на невнимательность.

                              Также, у вас тут не С++, а солянка какая-то получается из С99, С++03 и С++11. Не тащите за собой libc, раз уж используете libstdc++. Используйте что-то одно, все эти Си'шные printf и memcpy вполне имеют замену в С++.

                              И уж, если хотите писать сетевые приложения, почитайте Стивенса что ли. Научитесь использовать всякие SO_REUSEADDR, без которого ваш сервер невозможно будет перезапустить по скрипту.

                              А писать код в хидерах это жесть =). Как вы планируете распространять библиотеки с таким «оригинальным» способом написания кода? Потратьте недельку на изучение стиля и приёмов из библиотеки Qt. Хидеры, при хорошем стиле программирования, должны заменять документацию и не содержать ничего лишнего. Достаточно заглянуть в него, и всё становится понятно о библиотеке.
                                –2
                                Согласен со всеми Вашими замечаниями, кроме
                                А писать код в хидерах это жесть

                                Насколько я в курсе, библиотека boost распространяется именно в хедерах? Поправьте, если ошибаюсь.
                                  +2
                                  За это буст большинство и не любит. Адское мета-программирование на шаблонах, постоянные перекомпиляции всего кода целиком, если нет возможности делать precompiled-header. У них свой стиль просто, выработанный годами, и многими за это ненавидится =).

                                  К тому же, это опенсорс. Стоит делать что-то проприетарное и коммерческое, тут уже header-only не прокатит, либо придётся снова извращаться сидеть, вместо того, чтобы просто писать код.

                                  На том же бусте что-то написать, даже в юниксах приходится тащить за собой все исходники. Вместо того, чтобы установить одиночную .so-шку в систему, и к ней 1-2 хидера на 50кб. А тут же, хидеров на 150 метров и рантайма на 20. Ну да ладно, это моя личная неприязнь к данной библиотеке =).

                                  На вкус и цвет, короче. Писать то можно и на брейнфаке, и юникод использовать в именах переменных, и ещё много чего, что не является критически неправильным, но вот кто после вас будет поддерживать такой код?
                                    +1
                                    Код и темплейты это не одно и то же. Шаблоны пишутся в хидерах, а код — .cpp. И буст не полностью в хедерах, у него есть компилируемые части (хоть и не всех библиотеках).

                                Only users with full accounts can post comments. Log in, please.