Эта статья является продолжением статей:
Простейший кросcплатформенный сервер с поддержкой ssl
Кроссплатформенный https сервер с неблокирующими сокетами
В этих статьях я постепенно из простенького примера, входящего в состав OpenSSL стараюсь сделать полноценный однопоточный веб-сервер.
В предыдущей статье я «научил» сервер принимать соединение от одного клиента и отсылать обратно html страницу с заголовками запроса.
Сегодня я исправлю код сервера так, чтобы он мог обрабатывать соединения от произвольного количества клиентов в одном потоке.
Для начала я разобью код на два файла: serv.cpp и server.h
При этом файл serv.cpp будет содержать такой вот «высокоинтелектуальный» код:
Да, можете пинать меня ногами, но я все равно писал, пишу и буду писать код в заголовочных файлах если мне это удобно. За то я собственно и люблю с++, что он дает свободу выбора, но это отдельный разговор…
Переходим к файлу server.h
В его начало я перенес все заголовки, макросы и определения, которые раньше были в serv.cpp, и добавил еще пару заголовков из STL:
Дальше создаем сначала классы CServer и CClient внутри namespace server:
Как видите, это лишь заготовка для нашего сервера. Начнем потихоньку наполнять эту заготовку кодом, большая часть которого уже есть в предыдущей статье.
Для каждого клиента инициируется свой контекст SSL, очевидно делать это нужно в конструкторе класса CClient
Инициализацию библиотек, создание и привязку слушающего сокета перенесем с минимальными изменениями в конструктор CServer:
Дальше в этом же конструкторе я предлагаю принимать входящие TCP соединения.
Мне никто до сих пор не привел ни одного аргумента против, поэтому слушать TCP соединения мы будем в бесконечном цикле, как и в предыдущей статье.
После каждого вызова accept мы можем что-нибудь сделать с вновь подключившимся и с уже подключенными клиентами, вызвав callback функцию.
Добавим в конструктор CServer после функции listen код:
А сразу после конструктора, собственно callback функцию:
На этом код класса CServer закончен! Вся остальная логика приложения будет в классе CClient.
Важно заметить, что для критичных к скорости проектов, вместо перебора всех клиентов в цикле, надо перебирать только тех клиентов, чьи сокеты готовы для чтения или записи.
Сделать этот перебор легко с помощью функций select в Windows или epoll в Linux. Я покажу как это делается в следующей статье,
А пока (рискуя опять нарваться на критику) все таки ограничусь простым циклом.
Переходим к основной «рабочей лошадке» нашего сервера: к классу CClient.
Класс CClient должен хранить в себе не только информацию о своем сокете, но и информацию о том, на каком этапе находится его взаимодействие с сервером.
Добавим в определение класса CClient следующий код:
Здесь Continue() это пока только функция-заглушка, чуть ниже мы ее научим выполнять все действия с подключенным клиентом.
В конструкторе изменим:
на
В зависимости от текущего состояния, клиент вызывает разные функции. Договоримся, что состояния клиента можно менять только в конструкторе и в функции Continue(), это немного увеличит размер кода, но зато сильно облегчит его отладку.
Итак первое состояние, которое клиент получает при создании в конструкторе: S_ACCEPTED_TCP.
Напишем функцию, которая будет вызываться клиентом до тех пор, пока у него это состояние:
Для этого строки:
изменим на следующие:
А так же добавим следующий код в класс CClient:
Теперь функция AcceptSSL() будет вызываться клиентом до тех пор, пока не произойдет зашифрованное подключение или пока не возникнет ошибка.
1. В случае ошибки функция CClient::AcceptSSL() вернет код RET_ERROR в вызваашую ее функцию CClient::Continue(), которая в этом случае вернет false вызвавшей ее функции CServer::Callback, которая в этом случае удалит клиента из памяти сервера.
2. В случае удачного подключения функция CClient::AcceptSSL() вернет код RET_READY в вызвавшую ее функцию CClient::Continue(), которая в этом случае изменит состояние клиента на S_ACCEPTED_SSL.
Теперь добавим функцию обработки состояния S_ACCEPTED_SSL. Для этого строки
исправим на следующие:
И добавим в CClient функцию:
Эта функция, в отличие от предыдущей, вызовется всего один раз и вернет в CClient::Continue либо RET_ERROR либо RET_READY. Соответственно CClient::Continue вернет либо false, либо изменит состояние клиента на S_READING.
Дальше все аналогично: изменим код
на такой:
И добавляем соответствующие функции обработки состояний:
Наш сервер пока предназначен лишь для того, чтобы показывать клиенту заголовки его http запроса.
После того, как сервер выполнил свое предназначение, он может закрыть соединение и забыть про клиента.
Поэтому в наш код осталось внести последнее небольшое изменение:
нужно исправить на
Вот и все! Теперь у нас есть кроссплатформенный однопоточный 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»
Продолжение
Простейший крос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»
Продолжение