A guide to getting started with boost::asio
8. Networking basics: binary protocol sending and receiving (TCP)
To really get anything useful done with our sockets, we must be able to read and write to them. There are many different ways to go about this. Some protocols rely on text (ascii/unicode) while others require on binary. This chapter will just focus on smaller binary examples. For a more extensive set of examples, the boost::asio example page has a lot of useful reference material.
There are many types of functions for sending and receiving. Depending on our protocol, we will want to choose between them based on what best suites our application. Here is a list of relevant functions we need to be aware of:
boost::asio Free Functions
Wow! That is a lot of functions. We should remember that boost::asio is more than just networking functions, so that is why there are so many of them. As we spend more time using the boost::asio library, we will be able to readily know which functions our application should be using. For this guide, we will make use of async_write and async_read_some. The reason we will use async_write is because the function will write all of the data for us, so we do not have to worry about partial sends. Likewise, we are using async_read_some as a generic function to read some data since we do not have a specific protocol we are using for receiving. We will go over this concept a bit later.
For now, let us take a look at a complete example of using our IO functions. We will be using a modified server example from Example 7C.
In this example, we add a ClientContext class that houses all of the context specific stuff for our incoming connections. This class wraps up the necessary IO functionality for sending and receiving. In this example, the server will send a message to the incoming connection in a format of (message size)[xx xx] (message payload)[ xx ... xx]. However, we do not have a client yet so to test we can simply telnet into the server to receive the data. Any data we send to the server will be dumped out on the console.
There are a couple of things we need to take away from the previous example. First, each connection we wish to service needs its own context. This context should contain the socket as well as the send/recv buffers as well as any other user data. Next, the example is not particularly thread safe per se, so we only limit it to one worker thread. We will cover this problem later. For now though, the concepts of reading and writing to the socket should be made clear. We simply choose the right API functions for our particular task and make sure to properly use them.
In order to properly use them, we must make sure the context and the buffers remain valid the entire duration of the function. In this case, we use a list of vectors for the sends and one vector to act as a common receive buffer. Depending on the protocol we have to implement, we might want to change a few things here and there. For example, let us say we want to process packets in our stream. In that case, we would want to use async_read with the size of the header followed by another call to async_read with the exact size of the payload. That way, we do not have to keep track of the stream position when using async_read_some.
The disadvantage of processing packets one at a time in the stream is that it is inefficient in larger scale programs. Imagine we had 100 3 byte messages in the stream. We would have to execute our "read one packet" logic 100 times! Compare that to if we used async_read_some, we would most likely get all of the data at once and the logically parse it. That is the preferred method, but it is also more complex. Depending on our application needs, we must decide which route we go. For some cases, we could not use either or though. For example, if we have a stream protocol, such as HTTP, then we cannot simply wait for an exact amount of bytes at a time but instead just receive as much as we can and then process it.
The code for a client would look similar as well. The only difference is that the connection context would connect to a remote host rather than be accepted. As a result, we can reuse a lot of the code. However, we will not dive into modifying the previous example to a client. The reason is because at this point of the guide, we have been exposed to all of the concepts we need to be aware of for using the boost::asio library for TCP programming. This means we can take a look at a full network wrapper and we should be able to understand all of the concepts that are used.
The reason we want to make a transition into the network wrapper is because we need reusable code to work from rather than recoding the same logic over and over as we have been. Another benefit of the wrapper is that it has been tested and should be relatively bug free. Having code that works to reference in the future can be a great help as we go on to write our own code. With that said, we will take a look at the network wrapper next.