Last Updated:

C++ and Sockets Web Server

Let's create an HTTP server that processes browser requests and returns a response as an HTML page.
  • Introduction to HTTP
  • What will the server do?
  • About sockets
  • Creating a Socket
  • Binding a socket to an address (bind)
  • Preparing the socket to accept incoming connections (listen)
  • Waiting for an incoming connection (accept)
  • Receive a request and send a response
  • Sequential processing of requests

Introduction to HTTP

First, let's see what HTTP is. It is a text-based protocol for exchanging data between a browser and a web server.

An example of an HTTP request:

GET /page.html HTTP/1.1
Host: site.com

The first line passes the request method, resource identifier (URI), and HTTP protocol version. It then lists the request headers in which the browser passes the hostname, supported encodings, cookies, and other utility parameters. Each header is followed by a line break.\r\n

Some queries have a body. When a form is submitted by the POST method, the field values of the form are passed in the body of the request.

POST /submit HTTP/1.1
Host site.com
Content-Type: application/x-www-form-urlencoded

name=Sergey&last_name=Ivanov&birthday=1990-10-05

The body of the request is separated from the headers by a single empty string. The "Content-Type" header tells the server in which format the request body is encoded. By default, in an HTML form, data is encoded by the "application/x-www-form-urlencoded" method.

Sometimes it is necessary to transfer data in a different format. For example, when uploading files to a server, binary data is encoded by the "multipart/form-data" method.

The server processes the client request and returns a response.

Example of a server response:

HTTP/1.1 200 OK
Host: site.com
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 21

<h1>Test page...</h1>

The first line of the response transmits the protocol version and the status of the response. For successful requests, the status "200 OK" is usually used. If the resource is not found on the server, "404 Not Found" is returned.

The body of the response, just like that of the request, is separated from the headers by a single empty string.

The full HTTP protocol specification is described in the rfc-2068 standard. For obvious reasons, we will not implement all the features of the protocol within this material. It is enough to implement support for working with request and response headers, obtaining the request method, protocol version and URL.

What will the server do?

 

The server will accept client requests, parse the headers and request body, and return a test HTML page that displays the client's request data (requested URL, request method, cookies, and other headers).

Test Page server

About sockets

To work with a network at a low level, sockets are traditionally used. A socket is an abstraction that allows you to work with network resources as files. We can write and read data from a socket in much the same way as from a regular file.

In this article, we will work with the Windows socket implementation, which is located in the header file. In Unix-like operating systems, the principle of working with sockets is the same, only the API is different. You can read more about Berkeley sockets, which are used in GNU/Linux.<WinSock2.h>

Creating a Socket

Let's create a socket using the function that is located in the header file. To work with IP addresses, we'll need a header file.socket<WinSock2.h><WS2tcpip.h>

#include <iostream>
#include <string>
#include <string>

// For freeaddrinfo to work correctly in MinGW
// Read more: http://stackoverflow.com/a/20306451
#define _WIN32_WINNT 0x501

#include <WinSock2.h>
#include <WS2tcpip.h>

// It is necessary that the linking happens with the DLL-library
// To work with sockets
#pragma comment(lib, "Ws2_32.lib")

using std::cerr;

int main()
{
    // service structure for storing information
    // about the implementation of Windows Sockets
    WSADATA wsaData;

    // start using the socket library by the process
    // (Ws2_32.dll is loaded)
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);

    // If there was an error loading the library
    if (result != 0) {
        cerr << "WSAStartup failed: " << result << "\n";
        return result;
    }

    struct addrinfo* addr = NULL; // structure storing information
    // about the IP address of the listening socket

    // Template for initializing the address structure
    struct addrinfo hints;
    ZeroMemory(&hints, sizeof(hints));

    // AF_INET determines that the network is used to work with the socket
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; // Set the socket's stream type
    hints.ai_protocol = IPPROTO_TCP; // Use the TCP protocol
    // The socket is bound to an address to accept incoming connections
    hints.ai_flags = AI_PASSIVE;

    // Initialize the structure that stores the socket address - addr.
    // The HTTP server will hang on the 8000th port of the localhost
    result = getaddrinfo("127.0.0.1", "8000", &hints, &addr);

    // If address structure initialization fails,
    // display a message about this and end the program execution
    if (result != 0) {
        cerr << "getaddrinfo failed: " << result << "\n";
        WSACleanup(); // unload the Ws2_32.dll library
        return 1;
    }

    // Create a socket
    int listen_socket = socket(addr->ai_family, addr->ai_socktype,
        addr->ai_protocol);
    // If the socket creation failed, print a message,
    // free the memory allocated for the addr structure,
    // unload the dll-library and close the program
    if (listen_socket == INVALID_SOCKET) {
        cerr << "Error at socket: " << WSAGetLastError() << "\n";
        freeaddrinfo(addr);
        WSACleanup();
        return 1;
    }

    // ...

We have prepared all the data that is needed to create the socket and created the socket itself. The function returns the integer value of the file descriptor that is allocated by the operating system for the socket.socket

Binding a socket to an address (bind)

 

The next step, we need to bind the IP address to the socket so that it can accept incoming connections. To bind a specific address to a socket, use the function . It accepts the integer identifier of the socket file descriptor, the address (a field from the structure), and the size of the address in bytes (used to support IPv6).bindai_addraddrinfo

// Bind the socket to an IP address
result = bind(listen_socket, addr->ai_addr, (int)addr->ai_addrlen);

// If it was not possible to bind the address to the socket, then display a message
// about an error, free the memory allocated for the addr structure.
// and close the open socket.
// Unload the DLL from memory and close the program.
if (result == SOCKET_ERROR) {
     cerr << "bind failed with error: " << WSAGetLastError() << "\n";
     freeaddrinfo(addr);
     closesocket(listen_socket);
     WSACleanup();
     return 1;
}

Preparing the socket to accept incoming connections (listen)

Prepare the socket to accept incoming connections from clients. This is done using the . It accepts a handle to the listening socket and the maximum number of concurrent connections.listen

In the event of an error, the function returns the value of the constant . If successful, it will return 0.listenSOCKET_ERROR

// Initialize the listening socket
if (listen(listen_socket, SOMAXCONN) == SOCKET_ERROR) {
     cerr << "listen failed with error: " << WSAGetLastError() << "\n";
     closesocket(listen_socket);
     WSACleanup();
     return 1;
}

A constant stores the maximum possible number of concurrent TCP connections. This limitation works at the kernel level.SOMAXCONN

Waiting for an incoming connection (accept)

The function waits for a TCP connection request from the remote host. A handle to the listening socket is passed to it as an argument.accept

When a TCP connection is successfully established, a new socket is created for it. The function returns a handle to this socket. If a connection error occurs, it returns .

acceptINVALID_SOCKET

// Accept incoming connections
int client_socket = accept(listen_socket, NULL, NULL);
if (client_socket == INVALID_SOCKET) {
     cerr << "accept failed: " << WSAGetLastError() << "\n";
     closesocket(listen_socket);
     WSACleanup();
     return 1;
}

Receive a request and send a response

After establishing a connection to the server, the browser sends an HTTP request. We receive the content of the request through the . It takes a handle to the TCP connection (in our case, this is a pointer to the buffer to save the received data, the size of the buffer in bytes, and additional flags (which we are not interested in now).

recvclient_socket

If successful, the function will return the size of the retrieved data. In the event of an error, the value of . If the connection was closed by the client, 0 is returned.

recvSOCKET_ERROR

We'll create a 1024-byte buffer to save the HTTP request.

const int max_client_buffer_size = 1024;
char buf[max_client_buffer_size];

result = recv(client_socket, buf, max_client_buffer_size, 0);

std::stringstream response; // the response to the client will be written here
std::stringstream response_body; // response body

if (result == SOCKET_ERROR) {
    // error getting data
    cerr << "recv failed: " << result << "\n";
    closesocket(client_socket);
} else if (result == 0) {
    // connection closed by client
    cerr << "connection closed...\n";
} else if (result > 0) {
    // We know the actual size of the received data, so we put an end-of-line mark
    // In the request buffer.
    buf[result] = '\0';

    // Data received successfully
    // form the response body (HTML)
    response_body << "<title>Test C++ HTTP Server</title>\n"
        << "<h1>Test page</h1>\n"
        << "<p>This is the body of the test page...</p>\n"
        << "<h2>Request headers</h2>\n"
        << "<pre>" << buf << "</pre>\n"
        << "<em><small>Test C++ Http Server</small></em>\n";

    // We form the entire response along with headers
    response << "HTTP/1.1 200 OK\r\n"
        << "Version: HTTP/1.1\r\n"
        << "Content-Type: text/html; charset=utf-8\r\n"
        << "Content-Length: " << response_body.str().length()
        << "\r\n\r\n"
        << response_body.str();

    // Send a response to the client using the send function
    result = send(client_socket, response.str().c_str(),
        response.str().length(), 0);

    if (result == SOCKET_ERROR) {
        // an error occurred while sending data
        cerr << "send failed: " << WSAGetLastError() << "\n";
    }
    // Close the connection to the client
    closesocket(client_socket);
}

Upon receipt of the request, we immediately sent a response to the client using the . It accepts a socket handle, a string of response data, and the size of the response in bytes.send

In the event of an error, the function returns . If successful, the number of bytes transferred.

SOCKET_ERROR

Let's try to compile the program, not forgetting to pre-complete the function .main

   // Clean up after ourselves
     closesocket(listen_socket);
     freeaddrinfo(addr);
     WSACleanup();
     return 0;
}

The entire source code of the example.

If you compile and run the program, the console window will "hang" while waiting for a request to establish a TCP connection. Open the http://127.0.0.1:8000/ address in your browser. The server will return a response as in the figure below and shut down.

Test Page

Sequential processing of requests

 

To prevent the server from shutting down after processing the first request, but continuing to process new connections, you need to loop the part of the code that accepts the connection request and returns the response.

 

const int max_client_buffer_size = 1024;
char buf[max_client_buffer_size];
int client_socket = INVALID_SOCKET;

for(;;) {
    // Accept incoming connections
    client_socket = accept(listen_socket, NULL, NULL);
    if (client_socket == INVALID_SOCKET) {
        cerr << "accept failed: " << WSAGetLastError() << "\n";
        closesocket(listen_socket);
        WSACleanup();
        return 1;
    }

    result = recv(client_socket, buf, max_client_buffer_size, 0);

    std::stringstream response; // the response to the client will be written here
    std::stringstream response_body; // response body

    if (result == SOCKET_ERROR) {
        // error getting data
        cerr << "recv failed: " << result << "\n";
        closesocket(client_socket);
    } else if (result == 0) {
        // connection closed by client
        cerr << "connection closed...\n";
    } else if (result > 0) {
        // We know the size of the received data, so we put an end-of-line mark
        // In the request buffer.
        buf[result] = '\0';

        // Data received successfully
        // form the response body (HTML)
        response_body << "<title>Test C++ HTTP Server</title>\n"
            << "<h1>Test page</h1>\n"
            << "<p>This is the body of the test page...</p>\n"
            << "<h2>Request headers</h2>\n"
            << "<pre>" << buf << "</pre>\n"
            << "<em><small>Test C++ Http Server</small></em>\n";

        // We form the entire response along with headers
        response << "HTTP/1.1 200 OK\r\n"
            << "Version: HTTP/1.1\r\n"
            << "Content-Type: text/html; charset=utf-8\r\n"
            << "Content-Length: " << response_body.str().length()
            << "\r\n\r\n"
            << response_body.str();

        // Send a response to the client using the send function
        result = send(client_socket, response.str().c_str(),
            response.str().length(), 0);

        if (result == SOCKET_ERROR) {
            // an error occurred while sending data
            cerr << "send failed: " << WSAGetLastError() << "\n";
        }
        // Close the connection to the client
        closesocket(client_socket);
    }
}

When the server finishes processing a request from one client, it will close the connection to it and wait for a new request.

The source code for the final version of the server.

In the second part of this article series, we'll write an HTTP header parser and create a normal API for managing HTTP requests and responses.

Note: if you use MinGW in Windows, then the Ws2_32.lib library must be manually registered in the linker settings.