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

от автора

Эта статья является продолжением статей:
Простейший крос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 можно скачать здесь: 00.3s3s.org
Чтобы скомпилировать в Linux надо скопировать в одну директорию файлы: serv.cpp, server.h, ca-cert.pem и в командной строке набрать: «g++ -std=c++0x -L/usr/lib -lssl -lcrypto serv.cpp»

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


Комментарии

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

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