"Boost.Asio C ++ Network Programming". Chapter 1: Getting Started with Boost.Asio
- From the sandbox
- Tutorial
This is my first post, so do not judge strictly. I want to start a free translation of the book John Torjo "Boost.Asio C ++ Network Programming" is a link to it.
Content:
- Chapter 1: Getting Started with Boost.Asio
- Chapter 2: Boost.Asio Basics
- Chapter 3: Echo Server / Client
- Chapter 4: Client and Server
- Chapter 5: Synchronous vs. Asynchronous
- Chapter 6: Boost.Asio - Other Features
- Chapter 7: Boost.Asio - additional topics
First, let's look at what Boost.Asio is, how to build it, as well as a few examples. You will find out that Boost.Asio is more than a network library. Also, you will learn about the most important class, which is located in the heart of Boost.Asio -
io_service.What is Boost.Asio?
In short, Boost.Asio is, for the most part, a cross-platform C ++ library for programming networks and some other low-level I / O programs.
There are many implementations for solving network problems, but Boost.Asio outdid them all; It was adopted in Boost in 2005 and has since been tested by a large number of Boost users. It is used in many projects, such as:
- Remobo , allows you to create your own IPN
- libtorrent , is a library that implements a bittorrent client
- PokerTH , is a poker game with LAN and Internet support.
Boost.Asio successfully abstracts the concepts of input and output, which work not only for networking, but also for serial COM ports, files, and so on. In addition, you can make input or output programming synchronous or asynchronous:
read(stream, buffer [, extra options])
async_read(stream, buffer [, extra options], handler)
write(stream, buffer [, extra options])
async_write(stream, buffer [, extra options], handler)
As you have noticed in the previous code snippet, functions accept a stream instance, which can be anything (not just a socket, we can read and write to it).
The library is portable, works on most operating systems and scales well with more than a thousand simultaneous connections. The network part was a follower of BSD (Berkeley Software Distribution) sockets. An API is provided for working with TCP (Transmission Control Protocol) sockets, UDP (User Datagram Protocol) sockets, IMCP (Internet Control Message Protocol) sockets, the library is also extensible, so if you want, you can adapt it to your own protocol .
Story
Boost.Asio was adopted in Boost 1.35 in December 2005, after development began in 2003. Initially the author is Christopher M. Kohlhoff (Christopher M. Kohlhoff), you can contact him at [email protected].
The library has been tested on the following platforms and compilers:
- 32-bit and 64-bit Windows using Visual C ++ 7.1 and higher
- Windows using MinGW
- Windows using Cygwin (make sure it is defined
__USE_232_SOCKETS) - Linux based on 2.4 and 2.6 kernels using g ++ 3.3 and higher
- Solaris using g ++ 3.3 and up
- MAC OS X 10.4+ using g ++ 3.3 and higher
It can also work on 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 www .boost.org for details).
Dependencies
Boost.Asio depends on the following libraries:
- Boost.System: This library provides operating system support for the Boost library (http://www.boost.org/doc/libs/1_51_0/doc/html/boost_system/index.html)
- Boost.Regex: this library (option) is used in case you use
read_until()orasync_read_until()which take aboost::regexparameter - Boost.DateTime: this library (option) is used if you use Boost.Asio timers
- OpenSSL: this library (option) is used if you decide to use SSL support provided by Boost.Asio
Build Boost.Asio
Boost.Asio is a purely header library. However, depending on the compiler and the size of your program, you can choose to create Boost.Asio as the source file. You can do this to reduce compile time. This can be done in the following ways:
- Only in one of your files using
#include <boost/asio/impl/src.hpp>(if you use SSL then#include <boost/asio/ssl/impl/src.hpp>) - Using
#define BOOST_ASIO_SEPARATE_COMPILATIONin all your source files
Note that Boost.Asio depends on Boost.System and not necessarily on Boost.Regex, so you need to at least build a boost library using the following code:
bjam –with-system –with-regex stage
If you also want to build tests, you should use the following code:
bjam –with-system –with-thread –with-date_time –with-regex –withserialization stage
The library comes with a large number of examples that you can check, along with the examples in this book.
Important Macros
Use
BOOST_ASIO_DISABLE_THREADSif installed; it disables support for threads in Boost. Asio, regardless of whether Boost was compiled with support for threads.Synchronous vs. Asynchronous
First, asynchronous programming is extremely different from synchronous programming. In synchronous programming, all operations you do in sequential order, such as reading (query) from socket S, and then writing (response) to the socket. Each of the operations is blocking. Since the operations are blocking, in order not to interrupt the main program while you are reading or writing to the socket, you usually create one or more threads that deal with I / O sockets. Thus, synchronous server / clients are usually multi-threaded.
On the other hand, asynchronous programs are event driven. You start the operation, but you do not know when it will end; you provide
callback the function that will be called by the API with the result of the operation when the operation is completed. For programmers who 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 do not need to have more than one stream.You must decide at an early stage of your project (preferably at the beginning) which approach you will use: synchronous or asynchronous, since switching halfway will be difficult and error prone; not only the API is significantly different, the semantics of your program will be greatly changed (asynchronous networks are usually harder to test and debug than synchronous ones). Think before you want to use either blocking calls and many threads (synchronous, as a rule, easier) or few threads and events (asynchronous, as a rule, more complex).
Here is a simple 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 must have a copy
io_service. Boost.Asio uses io_service to communicate with the operating system I / O service. Usually one instance io_service is enough. Next, create the address and port to which you want to connect. Create a 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));
}
voidclient_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, your first program must have at least one copy
io_service. Then you specify the listening port and create an acceptor (receiver) —one object that accepts client connections. In the next cycle, you create a dummy socket and wait for the client to connect. After the connection is established, you create a stream that will deal with this connection.
In a stream, in a function,
client_session you listen to client requests, interpret them, and respond. To create a simple asynchronous client, you will 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();
voidconnect_handler(const boost::system::error_code & ec){
// here we know we connected successfully// if ec indicates success
}
Your program must have at least one copy
io_service. You specify where the socket is connected and created. Then, as soon as the connection is established, you are asynchronously connected to the address and port (this is the completion of the handler), that is, it is called connect_handler. After the call,
connect_handler check the code for errors ( ec), and if successful, you can write to the server asynchronously. Note that the loop
service.run()will be executed as long as there are incomplete asynchronous operations. In the previous example, there is only one such operation, this is a socket async_connect. After this service.run()completes. Each asynchronous operation has a terminating handler, a function that will be called when the operation is completed.
The following code is a simple 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();
voidstart_accept(socket_ptr sock){
acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) );
}
voidhandle_accept(socket_ptr sock, const boost::system::error_code & err){
if ( err) return;
// at this point, you can read/write to the socketsocket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
}
In the previous code snippet, first, you create an instance
io_service. Then you specify the port to listen on. Then you create an acceptor — an object for accepting client connections, as well as creating a dummy socket and asynchronously waiting for a client connection. Finally, start the asynchronous loop
service.run(). When the client connects, it is invoked handle_accept (the final handler for the call async_accept). If there are no errors, then you can use this socket for read / write operations. After using the socket, you create a new socket and call it again
start_accept(), which adds a similar asynchronous operation “wait for client connection”, leaving the loop service.run()busy.Exceptions against error codes
Boost.Asio allows you to use both exceptions and error codes. All synchronous functions have overloading throwing exceptions as a result of an error or return an error code. If the function fails, then it throws an
boost::system::system_errorerror.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 2In the previous code,
sock.connect(ep)throws an exception in case of an error and sock.connect(ep, err)returns 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 is similar to the previous one:
boost::system::error_code err;
sock.connect(ep, err);
if ( err)
std::cout << err << std::endl;
In case you use asynchronous functions, they all return an error code, which you can check in your callback function. Asynchronous functions never throw exceptions and it makes no sense to do this. And who will catch him?
In your synchronous functions, you can use both exceptions and error codes (which you want more), but use something else. Mixing them up can lead to problems or even a fall (when you forget to handle an exception by mistake). If your code is complex (called read / write functions in the socket), then you probably prefer to use exceptions and perform read and write functions in a
try {} catchblock.voidclient_session(socket_ptr sock){
try
{
...
}
catch ( boost::system::system_error e)
{
// handle the error
}
}
If you use error codes, you can see well 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 closedAll Boost.Asio error codes are in the namespace
boost::asio::error(in case you want to do a full search to find the fault). You can also look boost/asio/error.hppfor more details.Streams in Boost.Asio
When it comes to threads in Boost.Asio, we need to talk about the following:
io_service: classio_serviceis thread safe. Multiple threads can triggerio_service::run(). Most often, you probably callio_service::run()from one thread, so the function waits until all blocking asynchronous functions are executed. However, you can callio_service::run()from multiple threads. This blocks all threads that will callio_service::run(). All callback functions will be called in the context of all threads that have calledio_service::run(); it also means that if you calledio_service::run(), only in one thread, then all callback functions will be called in the context of that thread.socket: socket classes are not thread safe. Thus, you should avoid such situations as reading from a socket in one thread, and writing to it in another (this is recommended in general, not to mention Boost.Asio).utility: Classesutilitydo not usually make sense to use in multiple threads, they are not thread safe. Most of them are used for a short time and then go out of scope.
The Boost.Asio library itself can use several threads besides yours, but it is guaranteed that your code will not be called from these threads. This in turn means that the callback functions will be called only in those threads where it is called from
io_service::run().Not only networks
Boost.Asio provides other I / O objects in addition to networks.
Boost.Asio allows you to use such signals as
SIGTERM(end the program), SIGINT(interrupt the signal), SIGSEGV (segment violation) and others. You create an instance
signal_setand specify which signals to wait asynchronously and when one of them happens, your asynchronous handler will be called:voidsignal_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 generated
SIGINT, you will be taken to a handler signal_handler. Using Boost.Asio, you can easily connect to the serial port. COM7 port name on Windows or / dev / ttyS0 on POSIX platforms:
io_service service;
serial_port sp(service, "COM7");
After opening, you can set some parameters, such as port data rate, parity, stop bits, as indicated in the following code snippet:
serial_port::baud_rate rate(9600);
sp.set_option(rate);
If the port is open, you can process it in the stream, also recommended to use the available functions for reading and / or writing to the serial port, such as,
read(), async_read(), write(), async_write(), as shown in the following example:char data[512];
read(sp, buffer(data, 512));
Boost.Asio also allows you to connect to Windows files and, again, use free functions, such as
read(), asyn_read()and others, as shown below: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 regular files), as is done in the following snippet:
posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO));
char data[512];
read(sd_in, buffer(data, 512));
Timers
Some I / O operations may have time constraints to complete. You can only apply this to asynchronous operations (since synchronous locking tools have no time limit). For example, the following message from your partner should come to you in 100 milliseconds:
bool read = false;
voiddeadline_handler(const boost::system::error_code &){
std::cout << (read ? "read successfully" : "read failed") << std::endl;
}
voidread_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 previous code snippet, if we read our data before the end of time,
read set to true, then our partner got to us in time. Otherwise, when called deadline_handler, it read is still set to false, which means that we are not contacted until the end of the allotted time. Boost.Asio allows you to use synchronous timers, but, usually, they are equivalent to a simple operation
sleep. The line boost::this_thread::sleep(500);and the next fragment will do the same:deadline_timer t(service, boost::posix_time::milliseconds(500));
t.wait();
Class io_service
You have already seen that most of the code that uses Boost.Asio will use some kind of instance
io_service. io_service - the most important class in the library, it deals with the operating system, waits for the end of all asynchronous operations, and then when it completes it calls the handler for each such operation. If you decide to create your application synchronous, then you do not need to worry about what I am going to show in this section.
You can use an instance in
io_service several ways. In the following examples, we have three asynchronous operations, two connected sockets, and a wait timer:- One thread with one instance
io_serviceand one handler: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(); - Many threads with one instance
io_serviceand several handlers: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); voidrun_service(){ service_.run(); } - Many threads with multiple instances
io_serviceand multiple handlers: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)); voidrun_service(int idx){ service_[idx].run(); }
First of all, note that you cannot have multiple instances
io_service in the same thread. It makes no sense to write the following code:for ( int i = 0; i < 2; ++i)
service_[i].run();
The previous section of the code does not make any sense, because it
service_[1].run()will require service_[0].run()when you close the first. So all asynchronous operations service_[1]will have to wait for processing, which is not a good idea. In all three previous examples, we waited for three asynchronous operations to complete. To explain the differences, we will assume that, some time later, operation 1 will end and operation 2 will be completed immediately after that. We also assume that each handler will need a second to complete.
In the first case, we are waiting for the completion of all three operations in one thread. After the first operation is completed, we call its handler. Even if operation 2 completes immediately after the first, we will have to wait a second to call its handler after completing the first operation.
In the second case, we are waiting for the completion of three operations in two threads. After the completion of the first operation, we call its handler in the first thread. As soon as operation 2 is completed, we immediately call its handler in the second thread (while the first thread is busy waiting for the handler of the first operation to complete, the second thread can respond to the completion of any other operation).
In the latter case, if operation 1 is
connect k sock1 and operation 2 connect k sock2, then the application will behave as in the second case. The first thread will process the handler connect for sock1, and the second thread will process the handler connect for sock2. However, if of sock1 is operation 1 and timeout deadline_timer tis operation 2, then the first thread will process the handler forconnect out sock1. Thus, the timeout handler deadline_timer twill have to wait until the end of the work the handler connect from sock1 (it will wait for one second) in the first stream is treated as a connection to the handler sock1, and timeout handler t. Here is what you should learn from the previous examples:
- Situation 1 for basic applications. You will always encounter problems if several handlers must be called at the same time or if they must be called sequentially. If one handler takes a long time to finish, then the other handlers will have to wait.
- Situation 2 for most cases. This is very good if several handlers must be called at the same time and each of them is called in a separate thread. The only bottleneck can occur if all processing threads are occupied and at the same time new handlers must be called. However, as a simple solution, you can simply increase the number of handler threads.
- Situation 3 is the most complex and more flexible. You should use it only when situation 2 is not enough. This will probably be possible when you have over a thousand simultaneous connections (sockets). You can assume that each thread handler (the thread that started
io_service::run()) has its own loopselect/epoll; it waits for all sockets, controls read / write operations and, having found at least one such operation, starts processing it. In most cases, you have nothing to worry about, you can only worry when the number of sockets grows exponentially (more than 1000 sockets). In this case, the presence of several cyclesselect/epollcan increase the response time.
If you think that your application will ever go to situation 3, then make sure that the code segment (the code that calls
io_service::run()) is isolated from the rest of the code so that it can be easily changed. Finally, always remember that
.run()it will always be completed if there are no more operations to control, as shown in the example below:io_service service_;
tcp::socket sock(service_);
sock.async_connect( ep, connect_handler);
service_.run();
In this case, as soon as the socket has established the connection will be called
connect_handler and service.run()terminated. If you want to
service.run()continue working, you must give him more work. There are two ways to solve this problem. One way is to increase the load by connect_handlerrunning another asynchronous operation. The second way is to simulate some of his work, using the following code:
typedef boost::shared_ptr<io_service::work> work_ptr;
work_ptr dummy_work(new io_service::work(service_));
The above code will keep you running
service_.run()until you call useservice_.stop()or dummy_work.reset(0); // destroy dummy_work.Summary.
Boost.Asio is a complex library that makes programming networks quite simple. It is easy to assemble. It works quite well, avoiding the use of macros; Several macros are provided for enabling on / off options, but there are a few things that should not be forgotten.
Boost.Asio supports both synchronous and asynchronous programming. These two approaches are very different and you should choose one in some way as early as possible, since the switching is rather complex and error prone.
If you choose the synchronous approach, then you can choose between exceptions and error codes, the transition from exceptions to error codes is quite simple, you need to add another argument to the function call (error code).
Boost.Asio is not only for programming networks. This library has several features that make it more valuable, such as signals, timers, and so on.
In the next chapter, we will delve into the many functions and classes Boost.Asio provides networks. In addition, we learn a few tricks about asynchronous programming.
Well, that's all for today, if you liked it, then I will gladly continue the translation. Write about all comments in the comments.
Good luck to all!