Поэтому для того, чтобы читать дальше очень желательно прочитать хотя бы часть предыдущей статьи. Но если не хочется, то вот краткое содержание: я взял из исходников 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/
Добавить комментарий