OpenSSL Sockets in C++ (part 1)

The goal of this tutorial series is to walk through using posix sockets, from the ground up. Projects merely wishing to add networking would probably be best advised to look at already well established abstraction layers like Boost Asio.

To start off, we're going to create a basic http request. To keep things simple, for the first iteration, we're going to use a plain TCP blocking socket. First create a cpp file (mine is named sockets\part1.cpp). First some constants and includes:

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>

namespace
{
    const char HOST[] = "fizz.buzz";
    const size_t BUFFER_SIZE = 1024;
}

int main(int argc, char** argv)
{
    return 0;
}

The first step to most network connections is doing a DNS request to convert a hostname like "fizz.buzz" to an ip address like "208.113.196.82". For illustration purposes you could manually do a DNS request from the shell with the following command:

$ dig +short fizz.buzz
208.113.196.82

To make a DNS request we will be using getaddrinfo(3) which will set the address of an addrinfo pointer passed into it.

struct addrinfo* address_info;
int error = getaddrinfo(HOST, "http", nullptr, &address_info);
if (error != 0)
{
    throw std::string("Error getting address info: ") + std::string(gai_strerror(error));
}

The second parameter to getaddrinfo defines the port. This can be a string for a protocol like "http" or "https", or it can be a numeric string like "80" and "443".

The third parameter to getaddrinfo is a set of "hints" indicating what type of connection we're looking to open. The hints param is optional and could just be a nullptr in this case without issue. In our hints we're setting ai_family to PF_UNSPEC to indicate that we are fine with any protocol. We're also setting ai_socktype to SOCK_STREAM to indicate that we wish to open a TCP byte stream.

The addrinfo struct that address_info now points to looks like this:

addrinfo:
  ai_flags      0
  ai_family     2               # AF_INET
  ai_socktype   1               # SOCK_STREAM
  ai_protocol   6               # IPPROTO_TCP
  ai_addrlen    16              # Length in bytes for the next field (ai_addr)
  ai_addr       sockaddr_in
    sin_family  2               # AF_INET (ipv4)
    sin_port    80              # default http port
    sin_addr    208.113.196.82  # ipv4 address to fizz.buzz
  ai_canonname  <blank>
  ai_next       nullptr         # Forms a linked list

As you can see we have all the details set for a TCP socket on port 80 to the ip address of fizz.buzz. Next we need to open a connection to the server. In the addrinfo struct we just generated there is an ai_next field that forms a singly-linked list, allowing getaddrinfo to return multiple values (for instance in the case where ipv4 and ipv6 would be supported. To handle that we will have to loop over the list trying to connect to each entry until we have a successful connection.

int connection = -1;
std::string error_string = "";
for (struct addrinfo* current_address_info = address_info; current_address_info != nullptr; current_address_info = current_address_info->ai_next)
{
    connection = socket(current_address_info->ai_family, current_address_info->ai_socktype, current_address_info->ai_protocol);
    if (connection < 0)
    {
	error_string = "Unable to open socket";
	continue;
    }

    if (connect(connection, current_address_info->ai_addr, current_address_info->ai_addrlen) < 0)
    {
	error_string = "Unable to connect";
	close(connection); // Cleanup
	connection = -1;
	continue;
    }

    break; // Success
}
if (connection < 0) // If we failed to connect
{
    throw error_string;
}

This loop is walking down the singly linked list trying each entry for a connection. First it attempts to open up a socket and checks to ensure that was successful. The opening of the socket is a local operation that doesn't involve any calls out to fizz.buzz. Next it tries to actually open the connection which is where the fizz.buzz server comes into play for the first time.

Now we're ready to make an HTTP request. We're going to make a very basic request for the home page with no special fields like cookies and user agents. The request string will look like this:

GET / HTTP/1.1
Host: fizz.buzz
<blank line>
std::string http_query = "GET / HTTP/1.1\r\n"       \
    "Host: " + std::string(HOST) + "\r\n\r\n";
send(connection, http_query.c_str(), http_query.size(), 0);

Finally we will need to read the result from the socket. Since we are using blocking sockets, the recv(3) call will wait until either there is data available or the connection has been closed before returning, which keeps this block of code simple.

char buffer[BUFFER_SIZE];

for (ssize_t read_size = recv(connection, buffer, BUFFER_SIZE, 0);
     read_size > 0;
     read_size = recv(connection, buffer, BUFFER_SIZE, 0))
{
    std::cout << std::string(buffer, read_size);
}

Now all we have left to do is cleanup after ourselves

close(connection);
freeaddrinfo(address_info);

Awesome! Lets compile and run the program

$ clang++ -std=c++11 -o sockets_part1 files/sockets_part1.cpp
$ ./sockets_part1
<html source of page should print here>

Lets also check for memory leaks and run some static analysis

$ valgrind --leak-check=full ./sockets_part1
$ scan-build clang++ -std=c++11 -o sockets_part1 files/sockets_part1.cpp

Looks good! In part 2 we will port this code over to non-blocking sockets. The source code for this post is available here under the ISC license.

Comments

Comments powered by Disqus