Boost.Asio C++ Network Programming

4 (1 reviews total)
By John Torjo
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies

About this book

Network programming is not new and it’s no secret that it’s not an easy task. Boost.Asio provides an excellent level of abstraction, making sure that with a minimal amount of coding you can create beautiful client/server applications, and have fun in the process!

'Boost.Asio C++ Network Programming" shows how to build client/server applications using a library that is part of the popular peer-reviewed Boost C++ Libraries. It analyzes whether you should go synchronous or asynchronous and the role that threading plays, whilst providing examples you can run and extend for yourself.

After covering the fundamentals of Boost.Asio you’ll discover how you can build synchronous and asynchronous clients and servers. You will also see how you can have your own asynchronous functions play nice with Boost.Asio. As a bonus, we will introduce co-routines, which make asynchronous programming a piece of cake. Nowadays, network programming is a must, no matter what type of programmer you are. "Boost.Asio C++ Network Programming" shows just how simple networking can be, if you’re using the right tools.

Publication date:
February 2013
Publisher
Packt
Pages
156
ISBN
9781782163268

 

Chapter 1. Getting Started with Boost.Asio

First, lets delve into what Boost.Asio is, how to build it, and a few examples along the way. Boost.Asio is more than a networking library as you're about to find out. You'll also discover the most important class that sits at the heart of Boost.Asio – io_service.

 

What is Boost.Asio?


In short, Boost.Asio is a cross-platform C++ library mainly for networking and some other low-level input/output programming.

There have been many implementations that have tackled networking, but Boost.Asio has by far surpassed them all; it was admitted into Boost in 2005, and has since been tested extensively by Boost users and used in many projects, such as Remobo (http://www.remobo.com) that allows you to create your own Instant Private Network (IPN), libtorrent (http://www.rasterbar.com/products/libtorrent), which is a library that implements a Bittorrent client, and PokerTH (http://www.pokerth.net), which is a poker game that supports LAN and Internet games.

Boost.Asio has successfully abstracted the concepts of input and output that work not just for networking but for COM serial ports, files, and so on. On top of these, you can do input or output programming synchronously or asynchronously:

read(stream, buffer [, extra options]) 
async_read(stream, buffer [, extra options], handler)
write(stream, buffer [, extra options])
async_write(stream, buffer [, extra options], handler)

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

As you can see in the preceding code snippet, the functions take a stream instance, which can be anything (not just a socket, we can read or write to it).

The library is portable and works across most operating systems, and scales well over thousands of concurrent connections. The networking part was inspired by Berkeley Software Distribution (BSD) sockets. It provides an API that deals with Transmission Control Protocol (TCP) sockets, User Datagram Protocol (UDP) sockets, Internet Control Message Protocol (IMCP) sockets, and is extensible as you can adapt it to your own protocol if you wish.

History

Boost.Asio was accepted into Boost 1.35 in December 2005, after being developed in 2003. The original author is Christopher M. Kohlhoff, and he can be reached at .

The library has been tested on the following platforms and compilers:

  • 32-bit and 64-bit Windows, using Visual C++ 7.1 and above

  • Windows using MinGW

  • Windows using Cygwin (make sure to define __USE_232_SOCKETS)

  • Linux based on 2.4 and 2.6 kernels, using g++ 3.3 and above

  • Solaris, using g++ 3.3 and above

  • MAC OS X 10.4+, using g++ 3.3 and above

It may also work on the platforms, such as AIX 5.3, HP-UX 11i v3, QNX Neutrino 6.3, Solaris using Sun Studio 11+, True64 v5.1, Windows using Borland C++ 5.9.2+ (consult at www.boost.org for moredetails).

Dependencies

Boost.Asio depends on the following ibraries:

  • Boost.System : This library provides operating system support for Boost libraries (http://www.boost.org/doc/libs/1_51_0/doc/html/boost_system/index.html)

  • Boost.Regex : This library (optional) is used in case you're using the read_until() or async_read_until() overloads that take a boost::regex parameter

  • Boost.DateTime: This library(optional) is used if you use Boost.Asio timers

  • OpenSSL : This library (optional) is used if you decide to use the SSL support provided by Boost.Asio

Building Boost.Asio

Boost.Asio is a header-only library. However, depending on your compiler and the size of your program, you can choose to build in Boost.Asio as a source file. You may want to do this to decrease the compilation times. This can be done in the following ways:

  • In only one of your files, by using #include <boost/asio/impl/src.hpp> (if you're using SSL, #include <boost/asio/ssl/impl/src.hpp> as well)

  • By using #define BOOST_ASIO_SEPARATE_COMPILATION in all your source files

Do note that Boost.Asio depends on Boost.System and optionally Boost.Regex, so you'll need to at least build boost libraries, using the following code:

bjam –with-system –with-regex stage

If you want to build the tests as well, you should use the following code:

bjam –with-system –with-thread –with-date_time –with-regex –with-serialization stage

The library comes with lots of examples, which you can check out, along with the examples that come with this book.

Important macros

Use BOOST_ASIO_DISABLE_THREADS if set; it disables threading support in Boost.Asio, regardless of whether Boost was compiled with threading support.

 

Synchronous versus asynchronous


First off, asynchronous programming is extremely different than synchronous programming. In synchronous programming, you do the operations in sequential order, such as read (request) from socket S, then write (answer) to socket. Each operation is blocking. Since operations are blocking, in order not to interrupt the main program while you're reading from or writing to a socket, you'll usually create one or more threads that deal with socket's input/output. Thus, synchronous servers/clients are usually multi-threaded.

In contrast, asynchronous programming is event-driven. You start an operation, but you don't know when it will end; you supply a callback, which the API will call when the operation ends, together with the operation result. To programmers that have extensive experience with QT, Nokia's cross-platform library for creating graphical user interface applications, this is second nature. Thus, in asynchronous programming, you don't necessary need more than one thread.

You should decide early on in your project (preferably at the start) whether you go synchronous or asynchronous with networking, as switching midway will be very difficult and error-prone; not only will the API differ substantially, the semantic of your program will change completely (asynchronous networking is usually harder to test and debug than synchronous networking). You'll want to think of either going for blocking calls and multi-threading (synchronous, usually simpler) or less-threads and events (asynchronous, usually more complex).

Here's a basic example of a synchronous client:

using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.connect(ep);

First, your program needs at least an io_service instance. Boost.Asio uses io_service to talk to the operating system's input/output services. Usually one instance of an io_service will be enough. Next, create the address and port you want to connect to. Create the socket. Connect the socket to your address and port:

Here is a simple synchronous server:using boost::asio;
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001
ip::tcp::acceptor acc(service, ep);
while ( true) {
    socket_ptr sock(new ip::tcp::socket(service));
    acc.accept(*sock);
    boost::thread( boost::bind(client_session, sock));
}

void client_session(socket_ptr sock) {
    while ( true) {
        char data[512];
        size_t len = sock->read_some(buffer(data));
        if ( len > 0) 
            write(*sock, buffer("ok", 2));
    }
}

Again, first your program needs at least one io_service instance. You then specify the port you're listening to, and create an acceptor, one object that accepts client connections.

In the following loop, you create a dummy socket and wait for a client to connect. Once a connection has been established, you create a thread that will deal with that connection.

In the client_session thread, read a client's request, interpret it, and answer back.

To create a basic asynchronous client, you'll do something similar to the following:

using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.async_connect(ep, connect_handler);
service.run();

void connect_handler(const boost::system::error_code & ec) {
  // here we know we connected successfully
  // if ec indicates success  
}

Your program needs at least one io_service instance. You specify where you connect to and create the socket.

You then connect asynchronously to the address and port once the connection is complete (its completion handler), that is, connect_handler is called.

When connect_handler is called, check for the error code (ec), and if successful, you can write asynchronously to the server.

Note that the service.run() loop will run as long as there are asynchronous operations pending. In the preceding example, there's only one such operation, that is, the socket async_connect. After that, service.run() exits.

Each asynchronous operation has a completion handler, a function that is called when the operation has completed.

The following code is of a basic asynchronous server:

using boost::asio;
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001
ip::tcp::acceptor acc(service, ep);
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
service.run();

void start_accept(socket_ptr sock) {
    acc.async_accept(*sock,  boost::bind( handle_accept, sock, _1) );
}

void handle_accept(socket_ptr sock, const boost::system::error_code & err) {
    if ( err) return;
    // at this point, you can read/write to the socket

    socket_ptr sock(new ip::tcp::socket(service));
    start_accept(sock);
}

In the preceding code snippet, first, you create an io_service instance. You then specify what port you're listening to. Then, you create the acc acceptor, an object to accept client connections and also, create a dummy socket, and asynchronously wait for a client to connect.

Finally, run the asynchronous service.run() loop. When a client connects, handle_accept is called (the completion handler for the async_accept call). If there's no error, you can use this socket for read/write operations.

After using the socket, you create a new socket, and call start_accept() again, which appends another "wait for client to connect" asynchronous operation, keeping the ser ice.run() loop busy.

 

Exceptions versus error codes


Boost.Asio allows for both exceptions or error codes. All the synchronous functions have overloads that either throw in case of error or can return an error code. In case the function throws, it will always throw a boost::system::system_error error.

using boost::asio;
ip::tcp::endpoint ep;
ip::tcp::socket sock(service);
sock.connect(ep); // Line 1
boost::system::error_code err;
sock.connect(ep, err); // Line 2

In the preceding code, sock.connect(ep) will throw in case of an error, and sock.connect(ep, err) will return an error code.

Take a look at the following code snippet:

try {
    sock.connect(ep);
} catch(boost::system::system_error e) {
    std::cout << e.code() << std::endl;
}

The following code snippet is similar to the preceding one:

boost::system::error_code err;
sock.connect(ep, err);
if ( err)
    std::cout << err << std::endl;

In case you're using asynchronous functions, they all return an error code, which you can examine in your callback. Asynchronous functions never throw an exception, as it would make no sense to do so. And who would catch it?

In your synchronous functions, you can use exceptions or error codes (whatever you wish), but do it consistently. Mixing them up can cause problems and most of the time crashes (when you forget to handle a thrown exception, by mistake). If your code is complex (socket read/write function calls), you should probably prefer exceptions and embody your reads/writes in the try {} catch block of a function.

void client_session(socket_ptr sock) {
    try {
        ...
    } catch ( boost::system::system_error e) {
        // handle the error 
    }
}

If using error codes, you can very nicely see when the connection is closed, as shown in the following code snippet:

char data[512];
boost::system::error_code error;
size_t length = sock.read_some(buffer(data), error);
if (error == error::eof)
    return; // Connection closed

All Boost.Asio error codes are in namespace boost::asio::error (in case you want to create a big switch to check out the cause of the error). Just check out the boost/asio/error.hpp header for more details.

 

Threading in Boost.Asio


When it comes to threading in Boost.Asio, we will talk about:

  • io_service: The io_service class is thread-safe. Several threads can call io_service::run(). Most of the time you'll probably call io_service::run() from a single thread that function is blocking until all asynchronous operations complete. However, you can call io_service::run() from several threads. This will block all threads that have called io_service::run(). All callbacks will be called in the context of any of the threads that called io_service::run(); this also means that if you call io_service::run() in only one thread, all callbacks are called in the context of that thread.

  • socket: The socket classes are not thread-safe. Thus, you should avoid doing such as reading from a socket in one thread and write to it in a different thread (this isn't recommended in general, let alone with Boost.Asio).

  • utility: For the utility classes, it usually does not make sense to be used in several threads, nor are they thread-safe. Most of them are meant to just be used for a short time, then go out of scope.

The Boost.Asio library itself can use several threads besides your own, but it guarantees that from those threads, it will not call any of your code. This in turn means that callbacks are called only from the threads that have called io_service::run().

 

Not just networking


Boost.Asio, in addition to networking, provides other input/output facilities.

Boost.Asio allows waiting for signals, such as SIGTERM (software terminate), SIGINT (signal interrupt), SIGSEGV (segment violation), and so on.

You create a signal_set instance, and specify what signals to asynchronously wait for, and when any of them happen, your asynchronous handler is called:

void signal_handler(const boost::system::error_code & err, int signal) {
    // log this, and terminate application
}

boost::asio::signal_set sig(service, SIGINT, SIGTERM);
sig.async_wait(signal_handler);

If SIGINT is generated, you'll catch it in your signal_handler callback.

Using Boost.Asio, you can easily connect to a serial port. The port name is COM7 on Windows, or /dev/ttyS0 on POSIX platforms:

io_service service;
serial_port sp(service, "COM7"); 

Once opened, you can set some options, such as port's baud rate, parity, stop bits, as set in the following code snippet:

serial_port::baud_rate rate(9600);
sp.set_option(rate);

Once the port is open, you can treat the serial port as a stream, and on top of that, use the free functions to read from and/or write to the serial port, such as, read(), async_read(), write, async_write(), as used in the following code snippet:

char data[512];
read(sp, buffer(data, 512));

Boost.Asio also allows you to connect to Windows files, and again use the free functions, such as read(), asyn_read(), and so on, as used in the following code snippet:

HANDLE h = ::OpenFile(...);
windows::stream_handle sh(service, h);
char data[512];
read(h, buffer(data, 512));

You can do the same with POSIX file descriptors, such as pipes, standard I/O, various devices (but not with regular files), as done in the following code snippet:

posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO));
char data[512];
read(sd_in, buffer(data, 512));
 

Timers


Some I/O operations can have a deadline to complete. You can apply this only to asynchronous operations (synchronous means blocking, thus, no deadline). For instance, the next message from your partner needs to reach you in 100 milliseconds:

bool read = false;
void deadline_handler(const boost::system::error_code &) {
    std::cout << (read ? "read successfully" : "read failed") << std::endl;
}
void read_handler(const boost::system::error_code &) {
    read = true;
}

ip::tcp::socket sock(service);
…
read = false;
char data[512];
sock.async_read_some(buffer(data, 512));
deadline_timer t(service, boost::posix_time::milliseconds(100));
t.async_wait(&deadline_handler);
service.run();

In the preceding code snippet, if we read our data before the deadline, read is set to true, thus our partner reached us in time. Otherwise, when deadline_handler is called, read is still set to false, which means we did not meet our deadline.

Boost.Asio allows for synchronous timers as well, but they are usually equivalent to a simple sleep operation. The boost::this_thread::sleep(500); code and the following snippet of code accomplish the same thing:

deadline_timer t(service, boost::posix_time::milliseconds(500));
t.wait();
 

The io_service class


You've already seen that most code that uses Boost.Asio will use some instance of io_service. The io_service is the most important class in the library; it deals with the operating system, waiting for all asynchronous operations to end, and then calling the completion handler for each such operation.

If you choose to create your application synchronously, you won't need to worry about what I'm about to show you in this section.

You can use io_service instances in several ways. In the following examples, we have three asynchronous operations, two socket connections and a timer wait:

  • Single-thread with one io_service and one handler thread:

    io_service service_;
    // all the socket operations are handled by service_
    ip::tcp::socket sock1(service_);
    // all the socket operations are handled by service_
    ip::tcp::socket sock2(service_);
    sock1.async_connect( ep, connect_handler);
    sock2.async_connect( ep, connect_handler);
    deadline_timer  t(service_, boost::posix_time::seconds(5));
    t.async_wait(timeout_handler);
    service_.run();
  • Multi-threaded with a single io_service instance and several handler threads:

    io_service service_;
    ip::tcp::socket sock1(service_);
    ip::tcp::socket sock2(service_);
    sock1.async_connect( ep, connect_handler);
    sock2.async_connect( ep, connect_handler);
    deadline_timer  t(service_, boost::posix_time::seconds(5));
    t.async_wait(timeout_handler);
     
    for ( int i = 0; i < 5; ++i)
        boost::thread( run_service);
     
    void run_service() {
      service_.run();
    }
  • Multi-threaded with several io_service instances and several threads:

    io_service service_[2];
    ip::tcp::socket sock1(service_[0]);
    ip::tcp::socket sock2(service_[1]);
    sock1.async_connect( ep, connect_handler);
    sock2.async_connect( ep, connect_handler);
    deadline_timer t(service_[0], boost::posix_time::seconds(5));
    t.async_wait(timeout_handler); 
    
    for ( int i = 0; i < 2; ++i)
        boost::thread( boost::bind(run_service, i));
     
    void run_service(int idx) {
        service_[idx].run();
    }

First off, notice you can't have several io_service instances and one thread. It would make no sense to have the following code snippet:

for ( int i = 0; i < 2; ++i) 
    service_[i].run();

The preceding code snippet makes no sense, because service_[1].run() would need service_[0].run() to complete first. Thus, all asynchronous operations handled by service_[1] would have to wait, which is not a good idea.

In all the three preceding scenarios, we're waiting for three asynchronous operations to complete. To explain the differences, we'll assume that, after a while, operation 1 completes, and just after that, operation 2 completes. We'll also assume that each completion handler takes a second to complete.

In the first case, we're waiting for all three operations to complete in one thread. Once operation 1 completes, we call its completion handler. Even though operation 2 completes just after, the completion handler for operation 2 will be called one second after the operation 1's handler completes.

In the second case, we're waiting for the three operations to complete in two threads. Once operation 1 completes, we call its completion handler in the first thread. Once operation 2 completes, just after, we'll call its completion handler instantly, in the second thread (while thread 1 is busy responding to operation 1 handler's, thread 2 is free to answer any incoming new operation).

In the third case, in case operation 1 is connect of sock1, and operation 2 is connect of sock2, the application will behave like in the second case. Thread 1 will handle connect of sock1 completion handler, and thread 2 will handle connect of sock2 completion handler. However, if connect of sock1 is operation 1, and timeout of deadline_timer t is operation 2, thread 1 will end up handling connect of sock1 completion handler. Therefore, timeout of deadline_timer t completion handler will have to wait until connect of sock1 completion handler ends (it will wait one second), since thread 1 handles both connection handler sock1 and timeout handler of t.

Here's what you should have learnt from the previous examples:

  • Situation 1 is for very basic applications. You will always run into bottleneck problem if several handlers need to be called at the same time, as they will be called in a serial manner. If one handler takes too long to complete, all subsequent handlers will have to wait.

  • Situation 2 is for most applications. It is very robust – if several handlers are to be called at the same time (this is possible) they will each be called in their own thread. The only bottleneck you can have is if all handler threads are busy and new handlers are to be called at that time. However, as a quick solution, just increase the number of handler threads.

  • Situation 3 is the most complex and most flexible. You should use this only when situation 2 is not enough. That will probably be when you have thousands of concurrent (socket) connections. You can consider that each handler thread (thread running io_service::run()) has its own select/epoll loop; it waits for any socket, it monitors to have a read/write operation, and then once it finds such an operation, it executes it. In most cases, you don't need to worry about this, as you'll only need to worry if the number of sockets you're monitoring grows exponentially (greater than 1,000 sockets). In that case, having several select/epoll loops can increase response times.

If, in your application, you think, you'll ever need to switch to situation 3, make sure that the monitor for operations code (the code that calls io_service::run()) is insulated from the rest of the application, so you can easily change it.

Finally, always remember that .run() will always end if there are no more operations to monitor, as given in the following code snippet:

io_service service_;
tcp::socket sock(service_);
sock.async_connect( ep, connect_handler);
service_.run();

In the previous case, once sock has established a connection, connect_handler will be called, and afterwards, service.run() will complete.

If you want to make sure service_.run() continues to run, you have to assign more work to it. There are two ways of accomplishing this. One way is to assign more work inside connect_handler by starting another asynchronous operation.

The other way is to simulate some work for it, by using the following code snippet:

typedef boost::shared_ptr<io_service::work> work_ptr;
work_ptr dummy_work(new io_service::work(service_));

The preceding code will make sure that service_.run()never stops unless you either useservice_.stop() or dummy_work.reset(0); // destroy dummy_work.

 

Summary


Boost.Asio is a complex library, making networking quite simple. Building it is easy. It's done quite a good job at avoiding use of macros; it's got a few macros to turn options on/off, but there's only quite a few you need to worry about.

Boost.Asio allows for both synchronous and asynchronous programming. They are very different; you should choose one way or the other as early as possible, since switching is quite complicated and prone to error.

If you go synchronous, you can choose between exceptions and error codes, going from exceptions to error codes is simple; just add one more argument to the function call (the error code).

Boost.Asio is not just for networking. It's got a few more features, making it even more valuable, such as signals, timers, and so on.

In the next chapter, we'll delve into the multitude of functions and classes Boost.Asio provides for networking. Also, we'll learn a few tricks about asynchronous programming.

About the Author

  • John Torjo

    John Torjo is a renown C++ expert. He has been programming for over 15 years, most of which were spent doing C++. Sometimes, he also codes C# or Java. He's also enjoyed writing articles about programming in C++ Users Journal (currently, Dr. Dobbs) and other magazines. In his spare time, he likes playing poker and driving fast cars. One of his freelance projects lets him combine two of his passions, programming and poker. You can reach him at [email protected].

    Browse publications by this author

Latest Reviews

(1 reviews total)
Good
Book Title
Access this book, plus 7,500 other titles for FREE
Access now