Asio: A Brief Introduction for the Windows Programmer

Really! Yet another tutorial on Asio?
Why?” The average C++ programmer has much more experience than say the average Java/Javscript programmer – not necessarily a good thing. (C++ is becoming like assembler.) As such a C++ developer can understand how to use Asio[1] if the design is understood. Most authors tend to view it in rather abstract terms without exploiting prior knowledge of multi-threading and sockets programming. As Spolsky[2] pointed in his “The Law of Leaky Abstractions,” at some point all abstraction fails. To understand Asio, we will describe how it could be implemented in Windows even if the actual implementation diverges a little from our description. Let’s get started. Fasten your seat belts

Socket Programming.
Asio is a C++ library for asynchronous socket IO. It consists of two layers: the socket layer and an asynchronous layer on top of it. There really is no reason why the design can’t be extended to File IO at least on Windows because it is implemented using IO Completion ports.
Let us focus on the sockets part first. There are a number of books devoted to network programming. Here I will paraphrase Merck’s description[3] as the basis for our discussion:

A TCP/UDP connection is uniquely identified by a tuple of five values:

{<protocol>, <source address>, <source port number>, <destination address>, <destination port number>}

No two connections can have the same five values. The protocol of a socket is set when a socket is created with the socket() function. We shall restrict our attention to TCP and UDP protocols. The source address and port are set with the bind() function. The destination address and port are set with the connect() function. UDP is a connectionless protocol in that the connection need persist only as long as the read or write operation is in progress. Hence UDP sockets can be used without explicitly connecting them. A TCP socket on the other hand need not be explicitly bound before connecting because an unbound TCP socket is automatically bound when it is be connected. In other words the client application has to specify the destination while connecting. The “system” takes care of binding it to an appropriate source address and port.

Asio views the above 5-tuple as a pair
<source_endpoint,destination_endpoint>
where an endpoint is a pair <address,port number>. The protocol is identified by the type of endpoint as the following example of a synchronous TCP client for an echo server illustrates.

    void main()
    {
        asio::io_service service; 
        asio::ip::tcp::endpoint ep( asio::ip::address::from_string("127.0.0.1"), 8001);
        asio::ip::tcp::socket sock(service);
        sock.connect(ep);
        string msg= "Hello World\n";
        sock.write_some(asio::buffer(msg));
        char buf[1024];
        int bytes = read(sock, asio::buffer(buf));
        sock.close();
    }

A socket in Asio is linked to an object of type io_service  or io_context for reasons we’ll ignore for now.  The destination endpoint is specified while connecting.  The source endpoint is generated by the operating system while connecting, as described earlier, although Asio chooses to ignore it. So far no surprises.

Consider a UDP client:

    void main()
    {
        io_service service;
        ip::udp::socket sock(service, ip::udp::endpoint(ip::udp::v4(), 0) );
        ip::udp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001);
        std::string msg("hello World!"); 
        sock.send_to(buffer(msg), ep);
        char buff[1024];
        ip::udp::endpoint sender_ep;
        int bytes = sock.receive_from(buffer(buff), sender_ep);
        std::string copy(buff, bytes);
        std::cout << "server echoed our " << msg << ": "
                << (copy == msg ? "OK" : "FAIL") << std::endl;
        sock.close();
    }

A UDP destination endpoint is created and a message is sent to it. The Asio library then has to keep track of the source endpoint as it is implicitly used when doing a receive_from. Things get more interesting doing asynchronous sockets.

Asynchronous TCP client

It is easier to understand Asio if some attention is paid to how it is implemented. Here I’ll focus on how it is implemented in Windows
As described in a previous post:

The message pump [in Windows] is based on a queue where messages are posted and the messages are then handed to [the call back function for ] the window for which it was meant. Hence we can have multiple window handles on a single thread and … the call back … will be executed in the thread that created the window.

An IO completion port is also a handle to a message queue except that multiple threads can wait on the same queue and the Windows API GetQueuedCompletionStatus returns a task to any [one] thread waiting on the completion port. … You can post a task using PostQueuedCompletionStatus.

In other words if we view it as a producer-consumer problem, then GetQueuedCompletionStatus is on the consumer side and PostQueuedCompletionStatus is on the producer side. One advantage of completion ports is that if a socket or file handle is associated with a completion port then the system posts the completion event to the completion port, which can then be handled in the consumer.

Let us go through an asynchronous version of the TCP echo client as listed below:


#include <iostream>
#include <memory>
#include <utility>
#include "asio.hpp"
void main()
{
    using namespace std;
    using namespace asio;
    io_service service; 
    ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 8081);
    ip::tcp::socket sock(service);
    std::string msg= "Hello World\n";
    char read_buf[1024];
    int bytes_read=0;
    sock.async_connect(ep, [&](const error_code & err)
    { //on connect do write
        sock.async_write_some(asio::buffer(msg), 
          [&] (const error_code & err, int bytecount)
          {  // on write do read
             async_read(sock,asio::buffer(read_buf,bytecount),
                    [&] (const error_code & err, int bytecount) 
                    {// on read
                        bytes_read = bytecount;
                        sock.close();
                    }
                    );
        }
        );
    });
    service.run();
    cout <<"Sent string:"  << msg << endl;
    cout <<"Recv string:"  << string(read_buf,bytes_read-1) << endl;
}

Through the use of lambda functions the completion call back routines are put inline. This code should be easy to read if you ignore the asynchronous part. Lets go through the code. asio::io_service creates an IO completion port. The socket constructor associates the socket with the completion port.

The first connect call is an asynchronous request. No connection request is made when the function returns. A message is posted to the IO completion port with the appropriate function object. Let’s assume there is a thread that waits on the IO completion port using GetQueuedCompletionStatus. When this request is picked up, an actual asynchronous connect request is made. The connect function could return before the connection is made. When the connection is completed the IO completion port is notified. That thread then makes the call to the call back routine provided with the connect function. In much the same vein, this call back makes an other asynchronous call, to send a message. On completion of the send, a receive request is executed.

Now where is GetQueuedCompletionStatusactually called? Notice the Line

service.run().

This method performs the message pump. Thus all that asyn_connect did was to post one message to the IO completion port. All the action takes place only when service.run is called.

service.run() terminates when there is no pending IO request. Hence the method has to be called after an asynchronous method is created. Otherwise the method will see no pending request and return without doing anything.

For an application with many IO requests it would be possible to create multiple threads all executing just service.run(). Of course multiple threads involves other concurrency issues. I addressed some of those in a previous blog.

Windows Specific
random_access_handle is a class for using random access files. Like a socket it is associated with an io_service in its constructor. Later a file handle can be assigned to it. The following methods can be used for read and write operations:

  • read_some_at: read starting from a location
  • write_some_at: write starting from a location
  • async_read_some_at: asynchronous counterpart to read
  • async_write_some_at: asynchronous counterpart to write

However if you want to call DeviceIOControl there is nothing out of the box that does that although there is a transmit file example[4] that shows how to mix native windows methods with asio::io_service.

A Windows specific asynchronous file copy application using Asio is available[5]

Conclusion
Asio is a well designed portable library and it is possible that parts of it if not most of it will be accepted to future C++ standards. The library has been in development for over ten years. But the advent of lambda functions in C++ has made it easy to use it.

References:
[1] http://think-async.com/
[2] http://www.joelonsoftware.com/articles/LeakyAbstractions.html
[3] http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t
[4] http://think-async.com/Asio/asio-1.10.6/src/examples/cpp03/windows/transmit_file.cpp
[5] https://github.com/theSundayProgrammer/AsioFileCopy

Advertisements

About The Sunday Programmer

Joe is an experienced C++/C# developer on Windows. Currently looking out for an opening in C/C++ on Windows or Linux.
This entry was posted in C++, Concurrent Programming, Software Engineering and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s