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 the last post. If you missed it now would be a good time to head back and check it out.

Client side #

The client side is a little simpler than the server, all we need to do is create the socket and instead of binding to a port, we need to connect to the server.

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

Connecting to the server #

Now that we have created a socket and have a valid file descriptor for it we can attempt to connect to the server. This process is not dissimilar to the “bind” section in the server-side setup:

struct sockaddr_in address; // 1
bzero(& address, sizeof(address)); // 2
address.sin_family = AF_INET; // 3
address.sin_port = htons(7777); // 4
inet_pton(AF_INET, "127.0.0.1", &address.sin_addr) // 5
if (connect(fd, (struct sockaddr *)&address, sizeof(address)) < 0) { // 6
    exit(0);
}
  1. We simply declare a sockaddr_in variable to be used for connecting. We created one very similar for the server to define what it would bind to, and here we will use it to define what to connect to.
  2. Zero the memory in the struct, and therefore all of it’s values.
  3. Setting the value of sin_family sets the domain or “family” that we want to use for our connections. Here we use a value of AF_INET to use a IPv4 connection, which matches the configuration we used when creating the socket.
  4. The sin_port allows us to specify the port which we want to connect to. Here we have chosen to use the same port as we used in the server tutorial. Feel free to change this value, but make sure you change it on the client and server or they won’t be able to connect. (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.)
  5. Setting the address to connect to is a little bit more complicated than it was for the server. Here we need to specify the address to connect to, so we need to convert our string (or printable) version of the address into a network byte order version to be used by the socket. We saw init_ntop() in the server post where we were trying to see the address of the connected client, this function is just the reverse, and sets the value in the address that we pass it. A positive value will be returned if this succeeds, it can of course fail if the address we supply in the string is not a valid IPv4 address, but we know we have supplied a valid address so do not need to check here.
  6. Now we actually try to connect. As we would expect the connect() function will return a negative error value on failure and 0 or positive on success, and takes 3 parameters:
    • file descriptor: The file descriptor of the socket that we want to connect, here we pass the file descriptor that was returned from our socket() call.
    • address: A sockaddr_in struct representing the server that we want to connect to.
    • address length: The size of the address struct being passed in.

Data transfer #

So now we have a server up and running waiting for a connection, and a client that is able to connect to the server. The next thing we want to be able to do is send and receive data across our connection. We’ll split this into the two parts, sending, and receiving and see how they both work.

Sending data #

In order to send data across the socket connection we have to “write” a data buffer to the socket. This shouldn’t be too tricky:

char buffer[13]; // 1
strcpy(buffer, "Hello world."); // 2
write(fd, buffer, strlen(buffer)); // 3
  1. We declare a buffer variable that we will use to store the data that we want to write to the socket.
  2. We copy the string literal Hello World. into the buffer.
  3. We call the write() function to finally write the data to the socket. This function blocks until the data has been written and on success returns the number of bytes that were successfully written to the socket. On error a negative error value will be returned. write() takes 3 parameters:
    • file descriptor: The file descriptor of the socket that we want to write data to. In our case, the one we created earlier by calling socket().
    • buffer: A const void * pointer to a buffer of data to be written to the socket.
    • buffer length: The amount of data from the buffer to be written to the socket.

And that’s that! We can now write data to our socket, of course that’s not a huge amount of use unless we can read that data out at the other end.

Receiving data #

Right, let’s have a look at how we can access the data that is sent to us across a socket. This will be somewhat symmetrical to the sending data we just saw, for that we created a buffer of the data we wanted to send and the wrote it to the socket, now we will create an empty buffer and read from the socket into it:

char buffer[256]; // 1
ssize_t read = recv(fd, buffer, strlen(buffer), 0); // 2
  1. We declare a pointer to a buffer of 256 characters which we can use to read data into.
  2. We call the recv() function to read data from the socket. recv() is a blocking call and will not return until some data has been read on the socket and on completion returns either the number of bytes read as a ssize_t (on my system a long) or a negative error value. The call takes 4 parameters:
    • file descriptor: The file descriptor of the socket from which we wish to read.
    • buffer: A pointer to a buffer into which data should be read.
    • buffer size: The size of the buffer, or the maximum number of bytes that should be read into the buffer.
    • flags: There are some optional configuration flags that can be passed here, for example passing MSG_DONTWAIT will cause the call to return immediately if there is no data to be read from the socket when the call is made.

You may notice that in this example we are limited to reading a defined amount (256 characters) of data from the socket, which of course is impractical. The next post will look deeper into how we can better handle the read operation.

Closing the connection #

On both the client and server end it is important that we remember to close any sockets that we have created. This allows the system to free up resources that are being used by the sockets. Also, when you are playing around you will undoubtedly at some point cause your server to crash before the socket is closed. In this case you may notice that if you run the server again soon after, it will error when binding to the port as it is still being reserved for the unclosed socket from the last process. If this happens, just wait a few minutes and try again. So how do we close the socket:

close(fd); // 1
  1. Call the close() function and pass in the file descriptor of the socket that you want to close.

Yup, it’s really that simple, so no excuses for forgetting to do it!

Boom! Now we can setup a server and a client and send and receive data between the two. Everything we have seen so far is just the very basics and while we could wire it up to send data between two applications or computers, it would not create a particularly robust or efficient system. However, it all server a purpose to get a handle on the building blocks required to build a socket based communication system.

Next time #

The next post in this series will continue on and look into some of the things that we will need to do to make our socket code viable for a functioning server. We’ll look at creating a new thread to handle the incoming connections, how the server can handle processing data that comes in from multiple sockets and how to read all of the data that is waiting for us on a socket, rather than just filling a 256 character buffer and hoping that that is all that has been sent.

Please do get in touch if you feel anything here does not make sense or could do with clarification.

 
57
Kudos
 
57
Kudos

Now read this

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”... Continue →