Warning: include_once(/var/www/web/../var/bootstrap.php.cache): failed to open stream: No such file or directory in /var/www/web/app.php on line 11

Warning: include_once(): Failed opening '/var/www/web/../var/bootstrap.php.cache' for inclusion (include_path='.:') in /var/www/web/app.php on line 11

Warning: session_cache_limiter(): Cannot change cache limiter when headers already sent in /var/www/var/cache/prod/classes.php on line 91

Warning: ini_set(): Headers already sent. You cannot change the session module's ini settings at this time in /var/www/var/cache/prod/classes.php on line 91

Warning: ini_set(): Headers already sent. You cannot change the session module's ini settings at this time in /var/www/var/cache/prod/classes.php on line 203

Warning: ini_set(): Headers already sent. You cannot change the session module's ini settings at this time in /var/www/var/cache/prod/classes.php on line 203

Warning: session_set_save_handler(): Cannot change save handler when headers already sent in /var/www/var/cache/prod/classes.php on line 222
Rogiel.com — Blog — Getting started with ASIO C++: creating a TCP server

ASIO is a really amazing library and now, with C++11 it can be used completely standalone which makes it even more attractive. Using ASIO now is much more expressive: lambdas and type inference can help a lot in making code more readable and simple. I will show you how easy ASIO makes writing server software.

Our idea here is to create a very simple server application using the ASIO C++ library whose purpose is to accept connections, write "Hello World" and close the connection. I have choose to make use of simple organization for the code that you should not replicate on your real application -- specially nesting lambdas which can be very confusion as you code grows.

Let try to be clear one more time: this is not a guide on how you should design your application with ASIO. It's a guide for you to undertand how it works at a very basic level.

About asio and boost::asio

asio and boost::asio are basically the same. They difference is that boost's version will use by default boost constructs and cannot be ran standalone like the former can.

Running standalone requires a macro to be set: ASIO_STANDALONE=1

Let's go quickly through the basics of using ASIO as a server. We first need to create a few setup objects like the io_service, the endpoint and the acceptor:

asio::io_service service;

This creates a new io_service instance that will be used to dispatch all IO async taksks -- think of it like something that looks like a thread.

A little more about asio::io_service

The io_service is responsible for dispatching every async event you submit to the queue. You can think of it like a place where you place your tasks and, eventually they will be run. io_service makes no assumption about multithreading, even though it is allowed to run tasks on multiple threads, you are responsible for syncronizing access to it.

io_service is not only for networking! The io_service::post(CompletionHandler&&) method allows you to schedule any task to be run together with IO tasks.

Having created a io_service we now have to setup our server configuration: what should our server listen to? Any not used port would do, in this example I choose to use 2000. Feel free to use whatever port you want.

asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);

There's nothing fancy here, we just set a asio::ip::tcp::endpoint that will listen to any IPv4 address on port 2000.

A little more about asio::ip::tcp::endpoint

The endpoint class is not used only in server-mode. It also provides the destination address for outgoing connections and you can query the address of clients connected on the server.

Whenever a new client tries to connect to your server you have to accept the connection and this should be done using an asio::ip::tcp::acceptor class. We will start accepting connections in a second, but before that we need to instantiate a new acceptor instance:

asio::ip::tcp::acceptor acceptor(service, endpoint);

Notice that asio::ip::tcp::acceptor constructor takes two arguments: service and endpoint -- this is really simple to explain: tasks are going to be run on the io_service and acceptor should bind to address pointed by the endpoint.

Your code so far

asio::io_service service;
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);
asio::ip::tcp::acceptor acceptor(service, endpoint);

Okay, now we are done with setting up. Every configuration you need is done and now you can to start accepting connections and handling them. Each connection in ASIO is represented by a asio::ip::tcp::socket -- the socket class it where you will be doing most of your work: receiving data and writing data.

// alias to reduce verbosity
using socket_ptr = std::shared_ptr<asio::ip::tcp::socket>;
socket_ptr socket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*socket, [socket](std::error_code error) {
    std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
});

To simplify and reduce the need of typing the socket complete class name we setup an alias using the new C++11's using syntax. Then we are allocating a new std::shared_ptr for the socket. async_acept will place a new task on io_service io queue that will accept the first socket that tried to connect to your server. The second argument -- a lambda -- on async_accept is a handler that will be called once a new connection has been accepted.

Error handling

You should handle errors! Just because I didn't did it here doesn't mean you shouldn't in your real-world code!

Not everything in ASIO must be async

One of the interesting points of ASIO is that it is a ascychronous IO library however, asynchronous IO is not ideal in every case scenario -- some applications might require a certain portion of the IO to be done synchronously and, gladly, ASIO supports this.

Most methods on asio::ip::tcp::acceptor and asio::ip::tcp::socket have both a synchronous version and a asynchronous version that can be accessed by appending async_ to the method name.

If you compile and run your code now you will be surpised that nothing has happened - the program will return immediatly. That's because io_service was never started! To run io_service on the main thread all we need to do is invoke run:

service.run();

Running your program now will accept one connection to the server. After the first connection is accepted it is closed and the program terminates. This is due to an important behaviour on ASIO: io_service::run() will only block if there are pending tasks on the queue, returning once the queue is or becomes empty.

Your code so far

asio::io_service service;
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);
asio::ip::tcp::acceptor acceptor(service, endpoint);

using socket_ptr = std::shared_ptr;
socket_ptr socket = std::make_shared(service);

acceptor.async_accept(*socket, [socket](std::error_code error) {
    std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
});

service.run();

You could also use a raw pointer

In this example I am using a shared_ptr to keep memory management issues outside the scope of this post, however if you wish you can freely use a raw pointer to the socket. Just beware that you only should release it's memory if there's no task scheduled on the socket, otherwise you application will crash. This is even more important when you start dealing with multiple threads which could be schedling tasks you don't know about.

To keep acepting more connection we need to call acceptor::async_accept again inside our handler. To do this we need to change a certain number of things:

  • The socket cannot be passed through the capture group: every connection has to have a new socket
  • The lambda has to be passed to itself through the capture group

A possible implementation to this would be:

using socket_ptr = std::shared_ptr<asio::ip::tcp::socket>;

// the handler lambda takes two arguments: the socket and the error code. handler, service and acceptor are taked by the capture group and are used to schedule the next accept.
std::function<void(socket_ptr, std::error_code)> handler = [&handler, &service, &acceptor](socket_ptr socket, std::error_code error) {
    std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;

    // accepts the next connection -- creates a new socket and gives it to the acceptor
    socket_ptr newSocket = std::make_shared<asio::ip::tcp::socket>(service);
    acceptor.async_accept(*newSocket, std::bind1st(handler, newSocket));
};
socket_ptr socket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*socket, std::bind1st(handler, socket));

We are accepting multiple connections already the only thing that remains on our server is to send our "Hello World" message to the client connecting. Sending data is really easy all you need to do is issue a socket::async_write_some and your done.

Let's go in more details: upon accepting the connection, inside the handler, we should issue a socket::async_write_some call with our hello message and a write completion handler that should close the connection after sending.

std::function<void(socket_ptr, std::error_code)> handler = [&handler, &service, &acceptor](socket_ptr socket, std::error_code error) {
    std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;

    // writes data to the client
    socket->async_write_some(asio::buffer("Hello World\n"), [socket](std::error_code error, size_t bytes) {
        socket->close();
    });

    // accepts the next connection -- creates a new socket and gives it to the acceptor
    socket_ptr newSocket = std::make_shared<asio::ip::tcp::socket>(service);
    acceptor.async_accept(*newSocket, std::bind1st(handler, newSocket));
};

Note the use of the asio::buffer class: it is a small class that will act as a wrapper around a std::string, const char* and many other data types.

async_write_some might not send everything to the client

In order to keep the io_service thread in a non-blocking manner async_write_some might not write all the data you give to the client. This is by design!

As a solution to this issue you can use asio::async_write function that will automatically issue a new socket::async_write_some() until all data is consumed.

If you want to see your server in action you can open a Terminal window (Application > Utilities > Terminal on the Mac or Command Prompt on Windows) and type the following:

$ telnet 127.0.0.1 2000
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    Hello World
    Connection closed by foreign host.

Something that I have not covered here is receiving data from the client which works in a analogous way to writing. I think it is worth mentining that socket::async_read_some suffers from the same issues as socket::async_write_some -- it might not fill buffer and you will have to accumulate the data you are receiving on each event.

And remember: ASIO's documentation is your friend! Good luck!

Complete code with Xcode project is on GitHub.