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

от автора

Эта статья является продолжением моей статьи Простейший кросcплатформенный сервер с поддержкой ssl.
Поэтому для того, чтобы читать дальше очень желательно прочитать хотя бы часть предыдущей статьи. Но если не хочется, то вот краткое содержание: я взял из исходников OpenSSL файл-пример «serv.cpp» и сделал из него простейший кроссплатформенный сервер, который умеет принимать от клиента один символ.
Теперь я хочу пойти дальше и заставить сервер:
1. Принять от браузера весь http заголовок.
2. Отправить браузеру html страницу на которую будет выведен http заголовок.
3. Кроме этого, я хочу чтобы сокеты не блокировали процесс сервера и для этого я переведу их в так называемый «неблокирующий режим».

Для начала мне понадобится модифицированный в предыдущей статье файл serv.cpp.
Первое, что нужно сделать — написать кроссплатформенные макросы для перевода сокетов в неблокирующий режим:

для этого строки кода

#ifndef WIN32 #define closesocket  close #endif 

меняем на следующие:

#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) #endif 

Готово! Теперь, чтобы перевести «слушающий» сокет в неблокирующий режим, достаточно сразу после строки

listen_sd = socket (AF_INET, SOCK_STREAM, 0);	  CHK_ERR(listen_sd, "socket"); 

вставить строку:

SET_NONBLOCK(listen_sd); 

Tеперь «слушающий» сокет неблокирующий и функция accept вернет управление программе сразу же после вызова.
Вместо дескриптора сокета accept теперь вернет значение (-1).
Таким образом, в неблокирующем режиме нам нужно вызывать функцию accept в бесконечном цикле, пока она не вернет дескриптор сокета

  int sd = -1;   while(sd  == -1)   { 	  Sleep(1); #ifdef WIN32 	sd = accept (listen_sd, (struct sockaddr*) &sa_cli, (int *)&client_len); #else 	sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len); #endif     } 

Чтобы программа не грузила на 100% процессор, я добавил в цикле Sleep(1). В Windows это означает перерыв на 1 миллисекунду. Чтобы это работало в Linux, добавьте в начале файла:

#ifndef WIN32 #define Sleep(a) usleep(a*1000) #endif 

Теоретически, вместо бесконечного цикла, можно с помощью функции select и ее более мощных аналогов, ждать пока сокет listen_sd станет доступен для чтения, а лишь потом один раз вызвать accept. Но лично я не вижу в моем способе с циклом никаких особых недостатков.

Итак, программа выйдет из цикла когда клиент подключится. Сокет sd в теории должен автоматически стать неблокирующим, но практика показывает, что для надежности лучше в конце цикла все-таки вызвать макрос

SET_NONBLOCK(sd); 

Теперь, когда сокет для общения с клиентом неблокирующий, функция

err = SSL_accept (ssl); 

не будет подвешивать процесс, а вернется сразу после вызова с значением err = SSL_ERROR_WANT_READ или SSL_ERROR_WANT_WRITE
чтобы принять зашифрованное сообщение, нам понадобится еще один бесконечный цикл:

  while(1)   { 	Sleep(1); 	err = SSL_accept (ssl);   	const int nCode = SSL_get_error(ssl, err); 	if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) 	    break;   }   CHK_SSL(err); 

Лишь когда программа выйдет из этого цикла, можно быть уверенными, что зашифрованное соединение установлено и можно начинать прием и отправку сообщений.
Мы будем подключаться к серверу с помощью браузера, поэтому сообщения клиента состоят из http заголовка и тела запроса.
При этом http заголовок должен заканчиваться строкой "\r\n\r\n".
Исправим наш код так, чтобы сервер читал весь http заголовок, а не только его первую букву.

Для того, чтобы сократить код, я предлагаю воспользоваться замечательной библиотекой STL:
1. Добавим три заголовочных файла:

#include <vector> #include <string> #include <sstream> 

2. Заменим строки

  err = SSL_read (ssl, buf, sizeof(buf) - 1);                   CHK_SSL(err);   buf[err] = '\0';   printf ("Got %d chars:'%s'\n", err, buf); 

на следующий код:

  std::vector<unsigned char> vBuffer(4096); //выделяем буфер для входных данных   memset(&vBuffer[0], 0, vBuffer.size()); //заполняем буфер нулями    size_t nCurrentPos = 0;   while (nCurrentPos < vBuffer.size()-1)   { 	  err = SSL_read (ssl, &vBuffer[nCurrentPos], vBuffer.size() - nCurrentPos - 1); //читаем в цикле данные от клиента в буфер 	  if (err > 0) 	  { 		  nCurrentPos += err; 		  		  const std::string strInputString((const char *)&vBuffer[0]); 		  if (strInputString.find("\r\n\r\n") != -1) //Если найден конец http заголовка, то выходим из цикла 			  break;  		  continue; 	  } 	  	  const int nCode = SSL_get_error(ssl, err); 	  if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) 		  break;   } 

В этом цикле сервер читает данные от клиента до тех пор, пока не получит символы конца http заголовка "\r\n\r\n", либо пока место в буфере не кончится.
Буфер мне удобно выделять как std::vector хотя бы потому, что не нужно отдельной переменной для запоминания его длины.
После выхода из цикла в буфере должен храниться весь http заголовок и, возможно, часть тела запроса.

3. Отправим браузеру html страницу, в которую напишем http заголовок его запроса.
Заменим строку

err = SSL_write (ssl, "I hear you.", strlen("I hear you."));  CHK_SSL(err); 

на следующий код:

  //Преобразуем буфер в строку для удобства   const std::string strInputString((const char *)&vBuffer[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();  	//Цикл для отправки ответа клиенту. 	nCurrentPos = 0; 	while(nCurrentPos < strStream.str().length()) 	{ 		err = SSL_write (ssl, strStream.str().c_str(), strStream.str().length()); 		if (err > 0) 		{ 			nCurrentPos += err; 			continue; 		} 	  		const int nCode = SSL_get_error(ssl, err); 		if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) 			break; 	}  

Поскольку сокеты у нас неблокирующие то нет гарантии, что ответ отправится полностью с первого раза. Поэтому нужно вызывать SSL_write в цикле.
Вот и все. Теперь можно запустить наш сервер, а в браузере набрать https://localhost:1111
В ответ браузер покажет страницу со своим http запросом.

Проект для Visual Studio 2012 в архиве 3_.3s3s.org.
Чтобы скомпилировать под Linux, скопируйте из архива файлы «ca-cert.pem» и «serv.cpp» в один каталог и запустите компилятор: «g++ -L/usr/lib -lssl -lcrypto serv.cpp»

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


Комментарии

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

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