Pull to refresh

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

Reading time5 min
Views19K
Эта статья является продолжением моей статьи Простейший крос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 в цикле.
Вот и все. Теперь можно запустить наш сервер, а в браузере набрать localhost:1111
В ответ браузер покажет страницу со своим http запросом.

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

ПС: написал продолжение этой статьи
Tags:
Hubs:
Total votes 26: ↑11 and ↓15-4
Comments40

Articles