Эта статья является продолжением моей статьи Простейший крос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=%in", 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 заголовок должен заканчиваться строкой "rnrn".
Исправим наш код так, чтобы сервер читал весь 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("rnrn") != -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 заголовка "rnrn", либо пока место в буфере не кончится.
Буфер мне удобно выделять как 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("rnrn")) +
"</pre></body></html>";
//Добавляем в начало ответа http заголовок
std::ostringstream strStream;
strStream <<
"HTTP/1.1 200 OKrn"
<< "Content-Type: text/html; charset=utf-8rn"
<< "Content-Length: " << strHTML.length() << "rn" <<
"rn" <<
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»
Автор: 3s3s