Pull to refresh

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

Reading time10 min
Views11K
Эта статья является продолжением статей:
Простейший крос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»

Продолжение
Tags:
Hubs:
Total votes 26: ↑11 and ↓15-4
Comments26

Articles