📜 ⬆️ ⬇️

"Boost.Asio C ++ Network Programming". Chapter 6: - other features

Hello!
I continue to translate the book of John Torjo "Boost.Asio C ++ Network Programming".

Content:


In this chapter, we will look at some of the not very well-known features of Boost.Asio. The std streams and streambuf objects are sometimes a bit more difficult to use, but, as you will see for yourself, they have their advantages. Finally, you will see quite a late addition to Boost. Asio is co-routines, which will allow you to have asynchronous code, but easy to read (as if it were synchronous). This is a pretty amazing feature.
')


std streams and std I / O buffers



You should be familiar with objects such as STL streams and STL streambuf in order to understand the things written in this section.
Boost.Asio has two types of I / O buffers:

Throughout the book, you basically saw something like the following:

size_t read_complete(boost::system::error_code, size_t bytes){ ... } char buff[1024]; read(sock, buffer(buff), read_complete); write(sock, buffer("echo\n")); 

Usually this will be enough for you. But, if you want more flexibility, you can use streambuf . Here is the simplest and worst thing you can do with the streambuf object:

 streambuf buf; read(sock, buf); 

This reading will go on until the streambuf object is streambuf , and since the streambuf object can redistribute itself to accommodate more space, it will basically read until the connection is closed. You can use the read_until function to read to the last character:

 streambuf buf; read_until(sock, buf, "\n"); 

Here, the reading will go to the '\ n' character, then what is read and exits the reading function is added to the buffer. To write something to the streambuf object you will do something similar to the following:

 streambuf buf; std::ostream out(&buf); out << "echo" << std::endl; write(sock, buf); 

It's quite simple, you need to create an STL stream, put a streambuf object there during construction, write the message you want to send, and then use the write function to send the contents of the buffer.

Boost.Asio and STL streams



C Boost.Asio did a great job integrating STL streams and networks. Namely, if you already use STL extensively, then you should already have a lot of classes with overloaded operators >> and <<. Reading and writing to the sockets you like more than a walk through the park.
Let's say you have the following code snippet:

 struct person { std::string first_name, last_name; int age; }; std::ostream& operator<<(std::ostream & out, const person & p) { return out << p.first_name << " " << p.last_name << " " << p.age; } std::istream& operator>>(std::istream & in, person & p) { return in >> p.first_name >> p.last_name >> p.age; } 

Sending a person’s data over the network is as easy as below:

 streambuf buf; std::ostream out(&buf); person p; // ... initialize p out << p << std::endl; write(sock, buf); 

The other side can just read it:

 read_until(sock, buf, "\n"); std::istream in(&buf); person p; in >> p; 

The really good side of using streambuf objects and, of course, the corresponding std::ostream for writing or std::istream for reading, is that you end up writing code that will be considered normal:

Finally, a pretty cool trick is known, to reset the contents of the streambuf object in the console, use the following code:

 streambuf buf; ... std::cout << &buf << std::endl; // dumps all content to the console 

Similarly, to convert its contents to a string, use the following code fragment:

 std::string to_string(streambuf &buf) { std::ostringstream out; out << &buf; return out.str(); } 


Class streambuf


As I said before, streambuf derived from std::streambuf. Like std :: streambuf, it does not have a copy constructor.
In addition, it has several additional features, such as:

With the exception of the last two functions, the rest are not so easy to understand. First of all, in most cases, you will send an instance of streambuf as an argument for reading / writing an independent function, as shown below:

 read_until(sock, buf, "\n"); // reads into buf write(sock, buf); // writes from buf 

If you send the entire buffer to an independent function, as shown in the previous snippet, the function will first make sure that it will need to increase the buffer size, look for input and output pointers. In other words, if there is data to read, then you can read it.
For example:

 read_until(sock, buf, '\n'); std::cout << &buf << std::endl; 

In the previous snippet, what you just read from the socket is discarded. The following example will not dump something:

 read(sock, buf.prepare(16), transfer_exactly(16) ); std::cout << &buf << std::endl; 

Bytes are read, but the pointer does not move. You must move it yourself, as shown below:

 read(sock, buf.prepare(16), transfer_exactly(16) ); buf.commit(16); std::cout << &buf << std::endl; 

Similarly, if you want to write a streambuf object and if you use the independent write function, use the following code snippet:

 streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; write(sock, buf); 

The following code will send hi there three times:

 streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; for ( int i = 0; i < 3; ++i) write(sock, buf.data()); 

This happens because the buffer is never destroyed and the data remains there. If you want data to be destroyed, then look at how it is implemented:

 streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; write(sock, buf.data()); buf.consume(9); 

In conclusion, you should prefer to deal with the whole streambuf instance. Use the previous features if you want tweaking.
Even if you can use the same streambuf for reading and writing, I still recommend you two separate instances, one for reading and one for writing. This is perceived easier and clearer, and you will avoid many possible mistakes.

Independent functions that work with streambuf objects


The following list shows the independent functions from Boost.Asio that work with streambuf objects:

Suppose you want to read a vowel:

 streambuf buf; bool is_vowel(char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; } size_t read_complete(boost::system::error_code, size_t bytes) { const char * begin = buffer_cast<const char*>( buf.data()); if ( bytes == 0) return 1; while ( bytes > 0) { if ( is_vowel(*begin++)) return 0; else --bytes; } return 1; } ... read(sock, buf, read_complete); 

If you, for example, want to use regular expressions, then it is very simple:

 read_until(sock, buf, boost::regex("^[aeiou]+") ); 

Or let us modify the example a little, and you can place the match_condition function to work:

 streambuf buf; bool is_vowel(char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; } typedef buffers_iterator<streambuf::const_buffers_type> iterator; std::pair<iterator,bool> match_vowel(iterator b, iterator e) { while ( b != e) { if ( is_vowel(*b++)) return std::make_pair(b, true); } return std::make_pair(e, false); } ... size_t bytes = read_until(sock, buf, match_vowel); 


Coroutines


The authors of Boost.Asio, around 2009-2010, implemented a very cool coroutine idea that will help you create asynchronous applications even easier.
They allow you to facilitate two things, that is, it is easy to write an asynchronous application, and it is as easy to follow the flow of control, almost as if the application was written sequentially.



In the first case, the usual approach is displayed. Using coroutines, you get as close as possible to the second case.
Simply put, coroutine allows you to use multiple entry points to pause and resume execution at specific locations within a function.
If you are going to use coroutines, then you will need to include two header files that you can find only in boost/libs/asio/example/http/server4: yield.hpp and coroutine.hpp . Here two macros and a class are defined in Boost.Asio:

To better understand, consider a few examples. We will re-implement the application of chapter 4, which is a simple client that logs in, pings and can tell you what other clients are logged.
The main code is similar to:

 class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr> , public coroutine, boost::noncopyable { ... void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { for (;;) { yield async_write(sock_, write_buffer_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_, read_buffer_,"\n", MEM_ FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } } } }; 

The first thing that has changed is that a large number of member functions have disappeared, such as connect(), on_connect(), on_read(),do_read(), on_write(), do_write() and so on. Now we have one called function step()
The function body is inside the reenter(this) { for (;;) { }} . You can think of reenter(this) as the code that we executed last, so that we can now call the following code.
Inside the reenter block you can see several ongoing calls. The first time the function is entered, the async_write function is async_write , the second input is the async_read_until function, the third is the service.post function, the fourth is async_write again async_write and so on.
You should never forget the for(;;) {}. instance for(;;) {}. Look at the following code:

 void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { yield async_write(sock_, write_buffer_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_, read_buffer_, "\n",MEM_FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } } 

If we used the previous code snippet for the third time, we would enter the function and service.post . The fourth time we would have passed by service.post and didn’t do anything. The same thing will happen for the fifth time and for all of the following:

 class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr> , public coroutine, boost::noncopyable { talk_to_svr(const std::string & username) : ... {} void start(ip::tcp::endpoint ep) { sock_.async_connect(ep, MEM_FN2(step,_1,0) ); } static ptr start(ip::tcp::endpoint ep, const std::string & username) { ptr new_(new talk_to_svr(username)); new_->start(ep); return new_; } void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { for (;;) { if ( !started_) { started_ = true; std::ostream out(&write_buf_); out << "login " << username_ << "\n"; } yield async_write(sock_, write_buf_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_,read_buf_,"\n", MEM_FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } } } void on_answer_from_server() { std::istream in(&read_buf_); std::string word; in >> word; if ( word == "login") on_login(); else if ( word == "ping") on_ping(); else if ( word == "clients") on_clients(); read_buf_.consume( read_buf_.size()); if (write_buf_.size() > 0) service.post( MEM_FN2(step,error_code(),0)); } ... private: ip::tcp::socket sock_; streambuf read_buf_, write_buf_; bool started_; std::string username_; deadline_timer timer_; }; 

When we start a connection, the start() function is called, which asynchronously connects to the server. When the connection is established, we enter step() for the first time. This is when we send a message with our login.
After this we use async_write , then async_read_until and process the message ( on_answer_from_server ).
In the on_answer_from_server function on_answer_from_server we process incoming messages; we read the first word and send it to the corresponding function, and we ignore the rest of the message (in any case):

 class talk_to_svr : ... { ... void on_login() { do_ask_clients(); } void on_ping() { std::istream in(&read_buf_); std::string answer; in >> answer; if ( answer == "client_list_changed") do_ask_clients(); else postpone_ping(); } void on_clients() { std::ostringstream clients; clients << &read_buf_; std::cout << username_ << ", new client list:" << clients. str(); postpone_ping(); } void do_ping() { std::ostream out(&write_buf_); out << "ping\n"; service.post( MEM_FN2(step,error_code(),0)); } void postpone_ping() { timer_.expires_from_now(boost::posix_time::millisec(rand() % 7000)); timer_.async_wait( MEM_FN(do_ping)); } void do_ask_clients() { std::ostream out(&write_buf_); out << "ask_clients\n"; } }; 

The example is a bit more complicated, since we have to check the connection with the server at a random time. To do this, we postpone the ping operation after successfully requesting a list of clients for the first time. Then we postpone another ping operation for each reply ping from the server.
To run all this, use the following code snippet:

 int main(int argc, char* argv[]) { ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); talk_to_svr::start(ep, "John"); service.run(); } 

Using coroutines, we reduced the code by 15 lines, and it also became much more readable. Here we barely touched on the subject of coroutines. If you want more information on this issue, you can visit this page .

Summary


We have seen how easy Boost.Asio works with STL streams and streambuf objects. We also looked at how coroutines make our code more compact and easier to understand.
In the next chapter, we will look at topics such as Asio vs. Boost.Asio, progressive debugging, SSL, as well as some other platform-specific features.

Resources for this article: link

Thank you all for your attention, until we meet again!

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


All Articles