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:
-
domain
: Used to specify the underlying network protocol to be used for communication. We have specifiedAF_INET
which creates a socket that will utilise IPv4 for communication. (AF_INET6
would use IPv6, and other options can be found here). -
type
: Specifies the type of socket, the semantics of what the connection will be used for. A value ofSOCK_STREAM
supports sequenced, reliable, two-way, connection-based byte streams, which is what we want to handle a TCP connection. Again, other possible values are specified in the man page linked above. -
protocol
: The final parameter is used to specify the protocol to use. In most cases, as in ours, there will only be one protocol valid for thedomain
/type
pairing and so we can simply specify a value of 0.
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
}
- 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. - 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.
- Setting the value of
sin_family
sets thedomain
or “family” that we want to accept for inbound connections. Here we use a value ofAF_INET
to accept IPv4 connections, which matches the configuration we used when creating the socket. - Setting
sin_addr.s_addr
specifies the address(es) to which we would like to allow inbound connections. Here we specifyINADDR_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 addresses10.0.0.1
and10.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. - 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 tohtons()
simply converts ashort
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 ofhtons()
will either convert if needed or do nothing, we should always use this call to ensure that the data is correct.) - 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 oursocket()
call, if it was >= 0. -
info
: Asockaddr
struct specifying details about what we want to bind to. -
info length
: The size of thesockaddr
struct.
-
- 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:
-
file descriptor
: The file descriptor of the socket that we want to be put into listening mode. Here we pass the file descriptor that was returned from our call tosocket()
. -
backlog
: This parameter specifies the maximum number of pending connections that should be allowed to wait in the queue. Since we specify a value of10
, if 10 clients were to try to connect and we never accept (we’ll get to accept in a minute) any, when the 11th tries to connect the client will receive a connection error as it cannot be put in to the queue.
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);
}
- We create a
sockaddr_in
and asocklen_t
variable that we can pass by reference to theaccept()
function.accept()
will set these variables to represent the address of the incoming connection. (See below for how we can decipher this data). - 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 successfulaccept()
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 tosocket()
. -
in address
: A reference to asockaddr_in
struct that will be populated with the data about the address of the newly accepted client connection. -
address length
: A reference to asocklen_t
(which is just an int, on my system it is a uint32) which indicates the size of the address length variable.
-
- 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
- We create a char buffer that we will fill with a string form of the clients address.
- 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 thentohs()
which converts ashort
from network byte order to host byte order. -
inet_ntop()
is a function which takes anin_addr
struct and a reference to a character buffer and populates the buffer with a string representation of the address. - 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.