Mastering C++ Sockets (Server)

Intro #

In this series of posts we’ll look at how we can setup a socket connection between two C++ applications. We’ll look individually at the two halves, the “server” end which will bind and then wait for connections, and the “client” end which will attempt to connect to the server. We’ll then go on to look at how we can push data across the socket in both directions, how to select on a set of socket file descriptors and how to use threads to setup a reliable and efficient system for handling persistent sockets.

Note: While I’ll endeavour to explain things as best I can, bear in mind that sockets are not a basic topic and if you do not already have a solid grasp of C++ and what sockets are, this post may not be for you.

Server side #

There are 4 steps that we need to take to setup the server-side of the socket connection. We need to create a socket, bind the socket to a port, put the socket into listening mode and then start accepting client connections on the socket.

Before we get started, you’ll need to include the following in your C++ file for the rest of this tutorial to work:

#include <netinet/in.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

Creating a socket #

To initially create the socket and get a valid file descriptor (an id by which we will reference the socket in the future) we call the socket() function. To get started we want to do something like this:

int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
    exit(0);
}

As you can see the socket function takes 3 parameters:

If successful, the call to the socket() function returns a file descriptor which we must keep hold of as we will need it in future calls to socket related functions, to indicate which socket we are using. In the event that this call fails, a negative error value will be returned and we should not continue the process of setting up the socket (the negative value will of course not be a valid file descriptor).

Binding to a port #

Now that we have created the socket and have a file descriptor to identify it, we need to bind the socket to a port so that it knows where to send data and so that it can give us access to the relevant input data as and when it arrives. The code to do this may seem unnecessarily complicated, but it’s actually not too bad, let’s have a look:

struct sockaddr_in address; // 1
bzero(& address, sizeof(address)); // 2
address.sin_family = AF_INET; // 3
address.sin_addr.s_addr = INADDR_ANY; // 4
address.sin_port = htons(7777); // 5
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) < 0) { //6
    exit(0); // 7
}
  1. First we have to create a sockaddr_in struct that we will configure with the relevant information to indicate what type of connections we want to allow.
  2. We simply make sure that all the memory for the struct is set to 0, therefore setting all of the values in the struct to 0.
  3. Setting the value of sin_family sets the domain or “family” that we want to accept for inbound connections. Here we use a value of AF_INET to accept IPv4 connections, which matches the configuration we used when creating the socket.
  4. Setting sin_addr.s_addr specifies the address(es) to which we would like to allow inbound connections. Here we specify INADDR_ANY to specify that we will allow connections to any inbound address on this machine. Bear in mind that this is not the address of the client, it refers the the address that the client is connecting to (the address of an interface on the server). E.g. Suppose addresses 10.0.0.1 and 10.0.0.2 both point to different interfaces on your server. If you want you can specify one of them here and any requests made to the other address will not get routed to your application. INADDR_ANY binds to all available interfaces.
  5. The sin_port allows us to specify the port on which we wish to accept connections, or the port which we want to bind to.We typically go for high numbered ports as ports less that 1024 are usually reserved by the system and will cause an error if we try to bind to them. (The call to htons() simply converts a short from host to network byte order. Since the network is big-endian and our system will often be little-endian this step is necessary. Since the system specific implementation of htons() will either convert if needed or do nothing, we should always use this call to ensure that the data is correct.)
  6. Finally we bind to the port. The bind() function takes 3 parameters:
    • file descriptor: The file descriptor of the socket that we want to bind, here we should provide the value returned from our socket() call, if it was >= 0.
    • info: A sockaddr struct specifying details about what we want to bind to.
    • info length: The size of the sockaddr struct.
  7. The bind() call will return a negative error value if it fails and a 0 or positive value on success. Here we simply check for an error and exit if one occurred.

Listen for new connections #

Now that we have successfully created a socket and bound to a port so that we can accept incoming traffic, we need to put our socket into the listen state so that it knows to listen for new connections. Turns out this one is quite simple:

if (listen(fd, 10) < 0) {
    exit(0);
}

All we have to do is call to the listen() function and check for an error. listen() takes two parameters:

As with bind(), the listen() function returns an negative error value if an error occurs and a 0 or positive value on success.

Accepting connections #

The final stage of setting up the server side of our socket connection is to be able to accept incoming connections on the socket. A simple version of this is very easy to do, we’ll look at the simplest example here and then see why in reality we would need to implement something a bit more complex:

struct sockaddr_in clientAddress; // 1
socklen_t addresssLength = sizeof(clientAddress); // 1
int newFd = accept(fd, (struct sockaddr *)& clientAddress, &addresssLength); // 2
if (newFd < 0) { // 3
    exit(0);
}
  1. We create a sockaddr_in and a socklen_t variable that we can pass by reference to the accept() function. accept() will set these variables to represent the address of the incoming connection. (See below for how we can decipher this data).
  2. We call the accept() function to accept a new connection on the socket. It is important to note that this is a blocking call, that is, when we make the call, the function will not return until a client has connected. If successful accept() will return a new file descriptor which can be used to identify the new client socket in the future for sending and receiving data. accept() takes 3 parameters:
    • file descriptor: The file descriptor of the listening socket which we want to accept a connection on. Here we pass the file descriptor that we received from the earlier call to socket().
    • in address: A reference to a sockaddr_in struct that will be populated with the data about the address of the newly accepted client connection.
    • address length: A reference to a socklen_t (which is just an int, on my system it is a uint32) which indicates the size of the address length variable.
  3. On error a negative error value will be returned and for now we simply exit if this happens.

Concurrency #

Due to the blocking nature of the accept call, in order to create a functional server-side implementation we much introduce a concurrent system in order to be able to accept and act on multiple sockets at a time if we want the sockets to be persistent. In a very inefficient http server we could get away without it as we could accept a connection, respond, close the socket and then loop back round to accept a new connection. If, however, we wanted to implement some sort of persistent socket connection where we may continue to send data down a socket for some time, we will likely want to create a new thread to accept connections and one to handle input data from the connections. We’ll look in detail at how to achieve this in a later post, this one aims only to discuss the technical side of setting up a very simple server-side socket.

Bonus - Parsing the in address #

In the “Accepting connections” section we saw that we can get a sockaddr_in variable filled in with the details of the incoming client’s address. However, we did not look at how we can access that information afterwards, suppose we want to know the ip address of the client that is connecting? We can get that like this:

char addressString[INET_ADDRSTRLEN]; // 1
int port = ntohs(clientAddress.sin_port); // 2
inet_ntop(AF_INET,  &clientAddress.sin_addr, ipstr, sizeof(addressString)); // 3
printf("Client connected: %s", addressString); // 4
  1. We create a char buffer that we will fill with a string form of the clients address.
  2. We extract the port number (for us this is not necessary but is here for the sake of completeness). Note that when we bound the socket we used htons() to convert our local byte order to the network order, here we use the ntohs() which converts a short from network byte order to host byte order.
  3. inet_ntop() is a function which takes an in_addr struct and a reference to a character buffer and populates the buffer with a string representation of the address.
  4. Finally we just print out the address to the console.

Conclusion #

At this point we have successfully created, bound and configured our server-side socket. Of course this is not much use to us until we can also create a client to connect to it, our call to accept() will never return as there are no clients attempting a connection. The next post will go over creating the client side of the socket (it’ll be much simpler than the server side) and then move on to cover how to send and receive data across our socket connection.

 
16
Kudos
 
16
Kudos

Now read this

Mastering C++ Sockets (Client)

Intro # Incase you missed it, in the previous post we covered how to setup a very basic server-side socket. This post will continue the series and cover how to setup a client application that can connect to the server that we created in... Continue →