Cross-platform https server with non-blocking sockets

  • Tutorial
This article is a continuation of my article The simplest cross-platform server with ssl support .
Therefore, in order to read further it is highly advisable to read at least part of the previous article. But if you don’t feel like it, here’s a brief summary: I took the sample file “serv.cpp” from the OpenSSL source code and made it the simplest cross-platform server that can receive one character from the client.
Now I want to go further and force the server:
1. Receive the entire http header from the browser.
2. Send the html page the browser to which the http header will be displayed.
3. In addition, I want the sockets to not block the server process and for this I will transfer them to the so-called "non-blocking mode".

To get started, I will need the serv.cpp file modified in the previous article.
The first thing to do is write cross-platform macros to put sockets in non-blocking mode:

for this line of code
#ifndef WIN32
#define closesocket  close
#endif


change to the following:
#ifdef WIN32
#define SET_NONBLOCK(socket)	\
	if (true)					\
	{							\
		DWORD dw = true;			\
		ioctlsocket(socket, FIONBIO, &dw);	\
	}
#else
#include 
#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


Done! Now, to put a “listening” socket into non-blocking mode, it’s enough right after the line
listen_sd = socket (AF_INET, SOCK_STREAM, 0);	  CHK_ERR(listen_sd, "socket");


insert line:
SET_NONBLOCK(listen_sd);


Now the “listening” socket is non-blocking and the accept function will return control to the program immediately after the call.
Instead of a socket handle, accept will now return the value (-1).
Thus, in non-blocking mode, we need to call the accept function in an infinite loop until it returns a socket handle

  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  
  }

To prevent the program from loading 100% of the processor, I added Sleep (1) in the loop. On Windows, this means a 1 millisecond break. For this to work on Linux, add at the beginning of the file:

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


Theoretically, instead of an infinite loop, you can use the select function and its more powerful analogues to wait until the listen_sd socket becomes readable, and only then call accept once. But personally, I don’t see any special flaws in my cycle method.

So, the program will exit the loop when the client connects. The sd socket in theory should automatically become non-blocking, but practice shows that for reliability it is better to call the macro at the end of the loop
SET_NONBLOCK(sd);


Now that the socket for communication with the client is non-blocking, the function
err = SSL_accept (ssl);

it will not suspend the process, but will return immediately after the call with the value err = SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE in
order to receive the encrypted message, we need another infinite loop:

  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);


Only when the program exits this cycle, you can be sure that the encrypted connection is established and you can start receiving and sending messages.
We will connect to the server using a browser, so client messages consist of an http header and request body.
In this case, the http header should end with the string "\ r \ n \ r \ n".
We will fix our code so that the server reads the entire http header, and not just its first letter.

In order to shorten the code, I suggest using the wonderful STL library:
1. Add three header files:
#include 
#include 
#include 


2. Replace the lines

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


to the following code:

  std::vector 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;
  }

In this cycle, the server reads data from the client until it receives the characters for the end of the http header "\ r \ n \ r \ n", or until the buffer space runs out.
It is convenient for me to allocate the buffer as std :: vector, if only because I do not need a separate variable to remember its length.
After exiting the loop, the entire http header and, possibly, part of the request body should be stored in the buffer.

3. Send the html page to the browser, in which we write the http header of its request.
Replace string
err = SSL_write (ssl, "I hear you.", strlen("I hear you."));  CHK_SSL(err);


to the following code:
  //Преобразуем буфер в строку для удобства
  const std::string strInputString((const char *)&vBuffer[0]);
  //Формируем html страницу с ответом сервера
  const std::string strHTML = 
	  "

Hello! Your HTTP headers is:


" + 
	  strInputString.substr(0, strInputString.find("\r\n\r\n")) + 
	  "
"; //Добавляем в начало ответа 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; }


Since we have non-blocking sockets, there is no guarantee that the answer will be sent completely the first time. Therefore, you need to call SSL_write in a loop.
That's all. Now you can start our server, and type in the browser. localhost:1111
In response, the browser will display a page with its http request.

Project for Visual Studio 2012 in the archive 3_.3s3s.org .
To compile under Linux, copy the files “ca-cert.pem” and “serv.cpp” from the archive into one directory and run the compiler: “g ++ -L / usr / lib -lssl -lcrypto serv.cpp”

PS: wrote a continuation of this articles

Also popular now: