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

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

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

 

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

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB