Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 24.06.2014 | нет комментариев
Эта статья является продолжением статей:
Примитивный крос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, '', 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»

 

Источник: programmingmaster.ru

 

Оставить комментарий
БАЗА ЗНАНИЙ
СЛУЧАЙНАЯ СТАТЬЯ
СЛУЧАЙНЫЙ БЛОГ
СЛУЧАЙНЫЙ МОД
СЛУЧАЙНЫЙ СКИН
НОВЫЕ МОДЫ
НОВЫЕ СКИНЫ
НАКОПЛЕННЫЙ ОПЫТ
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB