📜 ⬆️ ⬇️

"Boost.Asio C ++ Network Programming". Chapter 1: Getting Started with Boost.Asio

Hi habraedi!
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:


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. As you 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:

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, runs 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. The original author is Christopher M. Kohlhoff (Christopher M. Kohlhoff), you can contact him at chris@kohlhoff.com.
The library has been tested on the following platforms and compilers:

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:


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:

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 many examples that you can check, along with the examples that are provided in this book.

Important Macros


Use BOOST_ASIO_DISABLE_THREADS , if 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.
Asynchronous programs, on the other hand, are event driven. You start the operation, but you do not know when it will end; You provide a callback 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 an instance of io_service . Boost.Asio uses io_service to communicate with the operating system I / O service. Usually one instance of 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)); } 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, your first program must have at least one instance of 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 thread that will deal with this connection.
In the stream, in the client_session 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(); void connect_handler(const boost::system::error_code & ec) { // here we know we connected successfully // if ec indicates success } 

Your program must have at least one io_service instance. You specify where the socket is connected and created. Then, as soon as the connection is established, you connect asynchronously to the address and port (this is the completion of the handler), that is, connect_handler is called.
After calling connect_handler check the error code ( ec ), and if successful, you can write to the server asynchronously.
Note that the service.run() loop will be executed as long as there are incomplete asynchronous operations. In the previous example, there is only one such operation, this is the socket async_connect . After this, service.run() terminates.
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(); 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 previous code snippet, first, you create an instance of 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, run the asynchronous service.run() loop. When the client connects, handle_accept is handle_accept (the final handler for calling 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 start_accept() again, which adds a similar asynchronous operation “waiting for client connection”, leaving the service.run() loop busy.

Exceptions against error codes


Boost.Asio allows you to use both exceptions and error codes. All synchronous functions have overloading throwing exceptions due to an error or return an error code. If the function fails, then it throws an error boost::system::system_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 previous code, sock.connect(ep) threw an exception in case of an error and sock.connect(ep, err) would 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 piece of 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 (the read / write functions in the socket are called), then you probably prefer to use exceptions and perform read and write functions in the try {} catch block.

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

If you use error codes, you may well 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 the namespace boost::asio::error (in case you want to do a full brute force search for a fault). You can also check boost/asio/error.hpp for more details.

Streams in Boost.Asio


When it comes to threads in Boost.Asio, we need to talk about the following:

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 io_service::run() called from.

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 (signal interruption), SIGSEGV (segment violation) and others.
You create an instance of signal_set and specify which signals to wait asynchronously and when one of them happens, your asynchronous handler will be 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 generated, you will be 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, and it is recommended to use free functions for reading and / or writing to the serial port, for example, read() , async_read() , write() , async_write() , as shown in 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 ordinary 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; 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 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 deadline_handler is called, read is still set to false , which means that we have not been contacted until the end of the allotted time.
Boost.Asio allows you to use synchronous timers, but, usually, they are equivalent to a simple sleep operation. boost::this_thread::sleep(500); and the following 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 instance of io_service . io_service is the most important class in the library, it deals with the operating system, waits for the end of all asynchronous operations, and then at the end 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 io_service instance io_service several ways. In the following examples, we have three asynchronous operations, two connected sockets, and a wait timer:

First of all, note that you cannot have multiple instances of 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 code doesn’t make any sense, because service_[1].run() will require service_[0].run() when you close the first one. So all asynchronous service_[1] 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 one, we will have to wait a second to call its handler after the first operation completes.
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. 2, ( , , ).
, 1 connect sock1 2 connect sock2 , . connect sock1 , – connect sock2 . , sock1 1 - deadline_timer t 2, connect sock1 . , - deadline_timer t connect sock1 ( ), sock1 , - t .
:

, - 3, , (, io_service::run() ) , .
, .run() , , :

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

connect_handler service.run() .
, service.run() , . . connect_handler , .
, :

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

The above code will provide a permanent job service_.run()until you call useservice_.stop()or dummy_work.reset(0); // destroy dummy_work.

Summary.


Boost.Asio , . . , ; /, , .
Boost.Asio . - , .
, , , ( ).
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!

Source: https://habr.com/ru/post/192284/


All Articles