ASIO TCP echo server

My first ASIO TCP server application was flawed in many ways, and its most substantial flaw was that it managed just one connection, shutting down after aknowledging it.

Here we rewrite it, solving this limit. The idea is that it sits waiting for a connection, once it receive a request from a client a new thread is started to manage that session, and a new socket is created to wait for a new connection.

But before caring of the code, a short list of project properties that have to be set (if you are working with Visual C++ 2010) for using the Boost-ASIO library.

A fast way to access the project properties is right clicking on the project name in the Solution Explorer window and then choose the last item in the menu (Properties).

Then set these three values:

C/C++, General
- Additional Include Directories: your local Boost root folder
- Preprocessor Definition: _WIN32_WINNT=0x0501
Linker, General
- Additional Library Directories: your local Boost lib folder

Once done that I wrote a main function that could call a couple of function, server() or client(), accordingly to the argument passed from the shell to the application. It would be better to let the user decide for port number (and the server IP address, for the client run) but I made it simpler assuming both client and server run on localhost and using a (carefully chosen) fixed port number.

All functions (main, client, and server) would use these basic information stored in a common include file, echo.h:
#pragma once // 1.

static const int ECHO_PORT = 50013;
static const char* ECHO_PORT_S = "50013";
static const char* ECHO_HOST = "localhost";

const int MSG_LEN = 8; // 2.

extern void server();
extern void client();

1. The pragma once makes this code stricty bounded to the MS compiler, feel free to use the more standard include guard mechanism instead.
2. This constant should be set to a more sensible value, like 80, representing the normally expected length of a message exchanged between client and server. Here is kept so low for testing purposes.

The server.cpp file keeps all the server related code in it. It starts with a bunch of includes and a typedef:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/smart_ptr.hpp>

#include "echo.h"

typedef boost::shared_ptr<boost::asio::ip::tcp::socket> SmartSocket;

The typedef is aimed to reduce typing and making code a bit clearer. SmartSocket is a smart pointer (a sharable one) based on the Boost ASIO TCP/IP socket class. It is used as parameter passed to a local function that we are going to see in details in a while, for the moment we just have a glimpse at its declaration:
namespace // 1.
{
void session(SmartSocket sock) // 2.
{
// ...
}
}

1. The function is in an anonymous namespace, that means it is visible only locallly.
2. It accepts in input a parameter, sock, that is a smart pointer to a socket.

But let see now the definition of the server() function:
void server()
{
boost::asio::io_service ios; // 1.

boost::asio::ip::tcp::acceptor acp(ios,
boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), ECHO_PORT)); // 2.

while(true) // 3.
{
SmartSocket sock(new boost::asio::ip::tcp::socket(ios)); // 4.
std::cout << "Waiting for a new connection" << std::endl;
acp.accept(*sock); // 5.
std::cout << "Connection accepted" << std::endl;
boost::thread t(session, sock); // 6.
}
}

1. This is the ASIO io_service object we are going to use to create all the relevant ASIO object in this example.
2. To accept a socket connection we need an acceptor, created using the local io_service instance and an endpoint specifying the protocol and the used port.
3. We are looping forever - this is not actually beautiful, but we are going to live with it, for the moment. When we are done with the server we kill it sending it an interrupt.
4. A smart pointer to a TCP/IP socket is created.
5. The application sits on the acceptor waiting for a client that would be connected to the socket we just created.
6. Here is the magic. We create a new thread on the session() function passing to it the smart pointer to the socket and then we start a new iteration of the while loop, creating a new socket smart pointer instance.

So, any time a client connect to the server, a new socket is created an make available for a new connection.

As promised, let's see now what happens in the session() function. Its basic idea is that it gets characters from the client, in bunches of MSG_LEN or less, and it sends them back to the client. As usual, we have to decide how the client would say to the server that it is done, and it does not want to send it anything anymore. I decided to use the NUL character, '\0', as terminator. When it is seen at the end of a chunk of data, the server knows the client considers terminated the communication:
void session(SmartSocket sock)
{
try
{
bool pending = true;
while(pending)
{
boost::array<char, MSG_LEN> data;

boost::system::error_code error;
size_t length = sock->read_some(boost::asio::buffer(data), error);
if (error == boost::asio::error::eof)
break; // Connection closed cleanly by peer.
else if (error)
throw boost::system::system_error(error); // Some other error.

if(data[length-1] == '\0') // 1.
{
std::cout << "Client sent a terminator" << std::endl;
--length;
pending = false;
}

if(length) // 2.
{
std::cout << "echoing " << length << " characters" << std::endl;
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}
}
catch (std::exception& e)
{
std::cerr << "Exception in thread: " << e.what() << std::endl;
}
}

1. We check if the last character in the current chunk of data is a NUL, if this the case the loop should be interrupted.
2. If there is something (besides the terminator) in the buffer read from the client, we send it back to it.

This post is based on the official documentation Boost ASIO Echo examples, here is the link to the original code for the TCP/IP blocking Echo server example.

3 comments:

  1. Nice tutorial! Thanks for makining it. I have one question: The code above makest two threads per connection? Is this correct? If i connect a simple client to the server then it prints the waiting for new connection accepted line out two times. In the debugger is see two iterations in the while loop with two threads.

    ReplyDelete
    Replies
    1. Found the problem! I was a bug in my client code. Didn't connect to the server socket on the right way. Thanks again for the tutorial.

      Delete
    2. Happy you found it useful! Thank you for your feedback.

      Delete