boost.asio之异步编程
来源:互联网 发布:在哪里可以买淘宝号 编辑:程序博客网 时间:2024/06/06 04:03
异步的需求
同步编程比异步编程简单很多。这是因为,线性的思考是很简单的(调用A,调用A结束,调用B,调用B结束,然后继续,这是以事件处理的方式来思考)。后面你会碰到这种情况,比如:五件事情,你不知道它们执行的顺序,也不知道他们是否会执行!
尽管异步编程更难,但是你会更倾向于选择使用它,比如:写一个需要处理很多并发访问的服务端。并发访问越多,异步编程就比同步编程越简单。
假设:你有一个需要处理1000个并发访问的应用,从客户端发给服务端的每个信息都会再返回给客户端,以‘\n’结尾。
同步方式的代码,1个线程:
using namespace boost::asio;struct client { ip::tcp::socket sock; char buff[1024]; // 每个信息最多这么大 int already_read; // 你已经读了多少};std::vector<client> clients;void handle_clients() { while ( true) for ( int i = 0; i < clients.size(); ++i) if ( clients[i].sock.available() ) on_read(clients[i]);}void on_read(client & c) { int to_read = std::min( 1024 - c.already_read, c.sock.available()); c.sock.read_some( buffer(c.buff + c.already_read, to_read)); c.already_read += to_read; if ( std::find(c.buff, c.buff + c.already_read, '\n') < c.buff + c.already_read) { int pos = std::find(c.buff, c.buff + c.already_read, '\n') - c.buff; std::string msg(c.buff, c.buff + pos); std::copy(c.buff + pos, c.buff + 1024, c.buff); c.already_read -= pos; on_read_msg(c, msg); }}void on_read_msg(client & c, const std::string & msg) { // 分析消息,然后返回 if ( msg == "request_login") c.sock.write( "request_ok\n"); else if ...}
有一种情况是在任何服务端(和任何基于网络的应用)都需要避免的,就是代码无响应的情况。在我们的例子里,我们需要handle_clients()方法尽可能少的阻塞。如果方法在某个点上阻塞,任何进来的信息都需要等待方法解除阻塞才能被处理。
为了保持响应,只在一个套接字有数据的时候我们才读,也就是说,if ( clients[i].sock.available() ) on_read(clients[i])。在on_read时,我们只读当前可用的;调用read_until(c.sock, buffer(…), ‘\n’)会是一个非常糟糕的选择,因为直到我们从一个指定的客户端读取了完整的消息之前,它都是阻塞的(我们永远不知道它什么时候会读取到完整的消息)
这里的瓶颈就是on_read_msg()方法;当它执行时,所有进来的消息都在等待。一个良好的on_read_msg()方法实现会保证这种情况基本不会发生,但是它还是会发生(有时候向一个套接字写入数据,缓冲区满了时,它会被阻塞)
同步方式的代码,10个线程
using namespace boost::asio;struct client { // ... 和之前一样 bool set_reading() { boost::mutex::scoped_lock lk(cs_); if ( is_reading_) return false; // 已经在读取 else { is_reading_ = true; return true; } } void unset_reading() { boost::mutex::scoped_lock lk(cs_); is_reading_ = false; }private: boost::mutex cs_; bool is_reading_;};std::vector<client> clients;void handle_clients() { for ( int i = 0; i < 10; ++i) boost::thread( handle_clients_thread);}void handle_clients_thread() { while ( true) for ( int i = 0; i < clients.size(); ++i) if ( clients[i].sock.available() ) if ( clients[i].set_reading()) { on_read(clients[i]); clients[i].unset_reading(); }}void on_read(client & c) { // 和之前一样}void on_read_msg(client & c, const std::string & msg) { // 和之前一样}
为了使用多线程,我们需要对线程进行同步,这就是set_reading()和set_unreading()所做的。set_reading()方法非常重要,比如你想要一步实现“判断是否在读取然后标记为读取中”。但这是有两步的(“判断是否在读取”和“标记为读取中”),你可能会有两个线程同时为一个客户端判断是否在读取,然后你会有两个线程同时为一个客户端调用on_read,结果就是数据冲突甚至导致应用崩溃。
你会发现代码变得极其复杂。
同步编程有第三个选择,就是为每个连接开辟一个线程。但是当并发的线程增加时,这就成了一种灾难性的情况。
然后,让我们来看异步编程。我们不断地异步读取。当一个客户端请求某些东西时,on_read被调用,然后回应,然后等待下一个请求(然后开始另外一个异步的read操作)。
异步方式的代码,10个线程
using namespace boost::asio;io_service service;struct client { ip::tcp::socket sock; streambuf buff; // 从客户端取回结果}std::vector<client> clients;void handle_clients() { for ( int i = 0; i < clients.size(); ++i) async_read_until(clients[i].sock, clients[i].buff, '\n', boost::bind(on_read, clients[i], _1, _2)); for ( int i = 0; i < 10; ++i) boost::thread(handle_clients_thread);}void handle_clients_thread() { service.run();}void on_read(client & c, const error_code & err, size_t read_bytes) { std::istream in(&c.buff); std::string msg; std::getline(in, msg); if ( msg == "request_login") c.sock.async_write( "request_ok\n", on_write); else if ... ... // 等待同一个客户端下一个读取操作 async_read_until(c.sock, c.buff, '\n', boost::bind(on_read, c, _1, _2));}
发现代码变得有多简单了吧?client结构里面只有两个成员,handle_clients()仅仅调用了async_read_until,然后它创建了10个线程,每个线程都调用service.run()。这些线程会处理所有来自客户端的异步read操作,然后分发所有向客户端的异步write操作。另外需要注意的一件事情是:on_read()一直在为下一次异步read操作做准备(看最后一行代码)。
异步run(), run_one(), poll(), poll_ one()
为了实现监听循环,io_service类提供了4个方法,比如:run(), run_one(), poll()和poll_one()。虽然大多数时候使用service.run()就可以,但是你还是需要在这里学习其他方法实现的功能。
持续运行
再一次说明,如果有等待执行的操作,run()会一直执行,直到你手动调用io_service::stop()。为了保证io_service一直执行,通常你添加一个或者多个异步操作,然后在它们被执行时,你继续一直不停地添加异步操作,比如下面代码:
using namespace boost::asio;io_service service;ip::tcp::socket sock(service);char buff_read[1024], buff_write[1024] = "ok";void on_read(const boost::system::error_code &err, std::size_t bytes);void on_write(const boost::system::error_code &err, std::size_t bytes){ sock.async_read_some(buffer(buff_read), on_read);}void on_read(const boost::system::error_code &err, std::size_t bytes){ // ... 处理读取操作 ... sock.async_write_some(buffer(buff_write,3), on_write);}void on_connect(const boost::system::error_code &err) { sock.async_read_some(buffer(buff_read), on_read);}int main(int argc, char* argv[]) { ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001); sock.async_connect(ep, on_connect); service.run();}
- 当service.run()被调用时,有一个异步操作在等待。
- 当socket连接到服务端时,on_connect被调用了,它会添加一个异步操作。
- 当on_connect结束时,我们会留下一个等待的操作(read)。
- 当on_read被调用时,我们写入一个回应,这又添加了另外一个等待的操作。
- 当on_read结束时,我们会留下一个等待的操作(write)。
- 当on_write操作被调用时,我们从服务端读取另外一个消息,这也添加了另外一个等待的操作。
- 当on_write结束时,我们有一个等待的操作(read)。
- 然后一直继续循环下去,直到我们关闭这个应用。
run_one(), poll(), poll_one() 方法
我在之前说过异步方法的handler是在调用了io_service::run的线程里被调用的。因为在至少90%~95%的时候,这是你唯一要用到的方法,所以我就把它说得简单了。对于调用了run_one(), poll()或者poll_one()的线程这一点也是适用的。
run_one()方法最多执行和分发一个异步操作:
* 如果没有等待的操作,方法立即返回0
* 如果有等待操作,方法在第一个操作执行之前处于阻塞状态,然后返回1
你可以认为下面两段代码是等效的:
io_service service;service.run(); // 或者while ( !service.stopped()) service.run_once();
你可以使用run_once()启动一个异步操作,然后等待它执行完成。
io_service service;bool write_complete = false;void on_write(const boost::system::error_code & err, size_t bytes){ write_complete = true; } …std::string data = "login ok”;write_complete = false;async_write(sock, buffer(data), on_write);do service.run_once() while (!write_complete);
还有一些使用run_one()方法的例子,包含在Boost.Asio诸如blocking_tcp_client.cpp和blocking_udp_client.cpp的文件中。
poll_one方法以非阻塞的方式最多运行一个准备好的等待操作:
* 如果至少有一个等待的操作,而且准备好以非阻塞的方式运行,poll_one方法会运行它并且返回1
* 否则,方法立即返回0
操作正在等待并准备以非阻塞方式运行,通常意味着如下的情况:
* 一个计时器过期了,然后它的async_wait处理方法需要被调用
* 一个I/O操作完成了(比如async_read),然后它的hanlder需要被调用
* 之前被加入io_services实例队列中的自定义handler(这会在之后的章节中详解)
你可以使用poll_one去保证所有I/O操作的handler完成运行,同时做一些其他的工作
io_service service;while ( true) { // 运行所有完成了IO操作的handler while ( service.poll_one()) ; // ... 在这里做其他的事情 …}
poll()方法会以非阻塞的方式运行所有等待的操作。下面两段代码是等效的:
io_service service;service.poll(); // 或者while ( service.poll_one()) ;
所有上述方法都会在失败的时候抛出boost::system::system_error异常。这是我们所不希望发生的事情;这里抛出的异常通常都是致命的,也许是资源耗尽,或者是你handler的其中一个抛出了异常。另外,每个方法都有一个不抛出异常,而是返回一个boost::system::error_code的重载:
io_service service;boost::system::error_code err = 0;service.run(err);if ( err) std::cout << "Error " << err << std::endl;
异步工作
异步工作不仅仅指用异步地方式接受客户端到服务端的连接、异步地从一个socket读取或者写入到socket。它包含了所有可以异步执行的操作。
默认情况下,你是不知道每个异步handler的调用顺序的。除了通常的异步调用(来自异步socket的读取/写入/接收)。你可以使用service.post()来使你的自定义方法被异步地调用。例如:
#include <boost/thread.hpp>#include <boost/bind.hpp>#include <boost/asio.hpp>#include <iostream>using namespace boost::asio;io_service service;void func(int i) { std::cout << "func called, i= " << i << std::endl;}void worker_thread() { service.run();}int main(int argc, char* argv[]) { for ( int i = 0; i < 10; ++i) service.post(boost::bind(func, i)); boost::thread_group threads; for ( int i = 0; i < 3; ++i) threads.create_thread(worker_thread); // 等待所有线程被创建完 boost::this_thread::sleep( boost::posix_time::millisec(500)); threads.join_all();}
在上面的例子中,service.post(some_function)添加了一个异步方法调用。
这个方法在某一个调用了service.run()的线程中请求io_service实例,然后调用给定的some_funtion之后立即返回。在我们的例子中,这个线程是我们之前创建的三个线程中的一个。你不能确定异步方法调用的顺序。你不要期待它们会以我们调用post()方法的顺序来调用。下面是运行之前代码可能得到的结果:
func called, i= 0func called, i= 2func called, i= 1func called, i= 4func called, i= 3func called, i= 6func called, i= 7func called, i= 8func called, i= 5func called, i= 9
有时候你会想让一些异步处理方法顺序执行。比如,你去一个餐馆(go_to_restaurant),下单(order),然后吃(eat)。你需要先去餐馆,然后下单,最后吃。这样的话,你需要用到io_service::strand,这个方法会让你的异步方法被顺序调用。看下面的例子:
using namespace boost::asio;io_service service;void func(int i) { std::cout << "func called, i= " << i << "/" << boost::this_thread::get_id() << std::endl;}void worker_thread() { service.run();}int main(int argc, char* argv[]){ io_service::strand strand_one(service), strand_two(service); for ( int i = 0; i < 5; ++i) service.post( strand_one.wrap( boost::bind(func, i))); for ( int i = 5; i < 10; ++i) service.post( strand_two.wrap( boost::bind(func, i))); boost::thread_group threads; for ( int i = 0; i < 3; ++i) threads.create_thread(worker_thread); // 等待所有线程被创建完 boost::this_thread::sleep( boost::posix_time::millisec(500)); threads.join_all();}
在上述代码中,我们保证前面的5个线程和后面的5个线程是顺序执行的。func called, i = 0在func called, i = 1之前被调用,然后调用func called, i = 2……同样func called, i = 5在func called, i = 6之前,func called, i = 6在func called, i = 7被调用……你需要注意的是尽管方法是顺序调用的,但是不意味着它们都在同一个线程执行。运行这个程序可能得到的一个结果如下:
func called, i= 0/002A60C8func called, i= 5/002A6138func called, i= 6/002A6530func called, i= 1/002A6138func called, i= 7/002A6530func called, i= 2/002A6138func called, i= 8/002A6530func called, i= 3/002A6138func called, i= 9/002A6530func called, i= 4/002A6138
异步post() VS dispatch() VS wrap()
Boost.Asio提供了三种让你把处理方法添加为异步调用的方式:
* service.post(handler):这个方法能确保其在请求io_service实例,然后调用指定的处理方法之后立即返回。handler稍后会在某个调用了service.run()的线程中被调用。
* service.dispatch(handler):这个方法请求io_service实例去调用给定的处理方法,但是另外一点,如果当前的线程调用了service.run(),它可以在方法中直接调用handler。
* service.wrap(handler):这个方法创建了一个封装方法,当被调用时它会调用service.dispatch(handler),这个会让人有点困惑,接下来我会简单地解释它是什么意思。
在之前的章节中你会看到关于service.post()的一个例子,以及运行这个例子可能得到的一种结果。我们对它做一些修改,然后看看service.dispatch()是怎么影响输出的结果的:
using namespace boost::asio;io_service service;void func(int i) { std::cout << "func called, i= " << i << std::endl;}void run_dispatch_and_post() { for ( int i = 0; i < 10; i += 2) { service.dispatch(boost::bind(func, i)); service.post(boost::bind(func, i + 1)); }}int main(int argc, char* argv[]) { service.post(run_dispatch_and_post); service.run();}
在解释发生了什么之前,我们先运行程序,观察结果:
func called, i= 0func called, i= 2func called, i= 4func called, i= 6func called, i= 8func called, i= 1func called, i= 3func called, i= 5func called, i= 7func called, i= 9
偶数先输出,然后是奇数。这是因为我用dispatch()输出偶数,然后用post()输出奇数。dispatch()会在返回之前调用hanlder,因为当前的线程调用了service.run(),而post()每次都立即返回了。
现在,让我们讲讲service.wrap(handler)。wrap()返回了一个仿函数,它可以用来做另外一个方法的参数:
using namespace boost::asio;io_service service;void dispatched_func_1() { std::cout << "dispatched 1" << std::endl;}void dispatched_func_2() { std::cout << "dispatched 2" << std::endl;}void test(boost::function<void()> func) { std::cout << "test" << std::endl; service.dispatch(dispatched_func_1); func();}void service_run() { service.run();}int main(int argc, char* argv[]) { test( service.wrap(dispatched_func_2)); boost::thread th(service_run); boost::this_thread::sleep( boost::posix_time::millisec(500)); th.join();}
test(service.wrap(dispatched_func_2));会把dispatched_ func_2包装起来创建一个仿函数,然后传递给test当作一个参数。当test()被调用时,它会分发调用方法1,然后调用func()。这时,你会发现调用func()和service.dispatch(dispatched_func_2)是等价的,因为它们是连续调用的。程序的输出证明了这一点:
testdispatched 1dispatched 2
io_service::strand 类(用来序列化异步调用)也包含了poll(), dispatch()和 wrap()等成员函数。它们的作用和io_service的poll(), dispatch()和wrap()是一样的。然而,大多数情况下你只需要把io_service::strand::wrap()方法做为io_service::poll()或者io_service::dispatch()方法的参数即可。
保持活动
假设你需要做下面的操作:
io_service service;ip::tcp::socket sock(service);char buff[512];...read(sock, buffer(buff));
在这个例子中,sock和buff的存在时间都必须比read()调用的时间要长。也就是说,在调用read()返回之前,它们都必须有效。这就是你所期望的;你传给一个方法的所有参数在方法内部都必须有效。当我们采用异步方式时,事情会变得比较复杂。
io_service service;ip::tcp::socket sock(service);char buff[512];void on_read(const boost::system::error_code &, size_t) {}...async_read(sock, buffer(buff), on_read);
在这个例子中,sock和buff的存在时间都必须比read()操作本身时间要长,但是read操作持续的时间我们是不知道的,因为它是异步的。
当使用socket缓冲区的时候,你会有一个buffer实例在异步调用时一直存在(使用boost::shared_array<>)。在这里,我们可以使用同样的方式,通过创建一个类并在其内部管理socket和它的读写缓冲区。然后,对于所有的异步操作,传递一个包含智能指针的boost::bind仿函数给它:
using namespace boost::asio;io_service service;struct connection : boost::enable_shared_from_this<connection> { typedef boost::system::error_code error_code; typedef boost::shared_ptr<connection> ptr; connection() : sock_(service), started_(true) {} void start(ip::tcp::endpoint ep) { sock_.async_connect(ep, boost::bind(&connection::on_connect, shared_from_this(), _1)); } void stop() { if ( !started_) return; started_ = false; sock_.close(); } bool started() { return started_; }private: void on_connect(const error_code & err) { // 这里你决定用这个连接做什么: 读取或者写入 if ( !err) do_read(); else stop(); } void on_read(const error_code & err, size_t bytes) { if ( !started() ) return; std::string msg(read_buffer_, bytes); if ( msg == "can_login") do_write("access_data"); else if ( msg.find("data ") == 0) process_data(msg); else if ( msg == "login_fail") stop(); } void on_write(const error_code & err, size_t bytes) { do_read(); } void do_read() { sock_.async_read_some(buffer(read_buffer_), boost::bind(&connection::on_read, shared_from_this(), _1, _2)); } void do_write(const std::string & msg) { if ( !started() ) return; // 注意: 因为在做另外一个async_read操作之前你想要发送多个消息, // 所以你需要多个写入buffer std::copy(msg.begin(), msg.end(), write_buffer_); sock_.async_write_some(buffer(write_buffer_, msg.size()), boost::bind(&connection::on_write, shared_from_this(), _1, _2)); } void process_data(const std::string & msg) { // 处理服务端来的内容,然后启动另外一个写入操作 }private: ip::tcp::socket sock_; enum { max_msg = 1024 }; char read_buffer_[max_msg]; char write_buffer_[max_msg]; bool started_;};int main(int argc, char* argv[]) { ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); connection::ptr(new connection)->start(ep);}
在所有异步调用中,我们传递一个boost::bind仿函数当作参数。这个仿函数内部包含了一个智能指针,指向connection实例。只要有一个异步操作等待时,Boost.Asio就会保存boost::bind仿函数的拷贝,这个拷贝保存了指向连接实例的一个智能指针,从而保证connection实例保持活动。问题解决!
当然,connection类仅仅是一个框架类;你需要根据你的需求对它进行调整(它看起来会和当前服务端例子的情况相当不同)。
你需要注意的是创建一个新的连接是相当简单的:connection::ptr(new connection)- >start(ep)。这个方法启动了到服务端的(异步)连接。当你需要关闭这个连接时,调用stop()。
当实例被启动时(start()),它会等待客户端的连接。当连接发生时。on_connect()被调用。如果没有错误发生,它启动一个read操作(do_read())。当read操作结束时,你就可以解析这个消息;当然你应用的on_read()看起来会各种各样。而当你写回一个消息时,你需要把它拷贝到缓冲区,然后像我在do_write()方法中所做的一样将其发送出去,因为这个缓冲区同样需要在这个异步写操作中一直存活。最后需要注意的一点——当写回时,你需要指定写入的数量,否则,整个缓冲区都会被发送出去。
总结
网络api实际上要繁杂得多,这个章节只是做为一个参考,当你在实现自己的网络应用时可以回过头来看看。
Boost.Asio实现了端点的概念,你可以认为是IP和端口。如果你不知道准确的IP,你可以使用resolver对象将主机名,例如www.yahoo.com转换为一个或多个IP地址。
我们也可以看到API的核心——socket类。Boost.Asio提供了TCP、UDP和 ICMP的实现。而且你还可以用你自己的协议来对它进行扩展;当然,这个工作不适合缺乏勇气的人。
异步编程是刚需。你应该已经明白为什么有时候需要用到它,尤其在写服务端的时候。调用service.run()来实现异步循环就已经可以让你很满足,但是有时候你需要更进一步,尝试使用run_one()、poll()或者poll_one()。
当实现异步时,你可以异步执行你自己的方法;使用service.post()或者service.dispatch()。
最后,为了使socket和缓冲区(read或者write)在整个异步操作的生命周期中一直活动,我们需要采取特殊的防护措施。你的连接类需要继承自enabled_shared_from_this,然后在内部保存它需要的缓冲区,而且每次异步调用都要传递一个智能指针给this操作。
参考资料: https://github.com/okingniko/boost-asio-cpp-network-programming-chinese
- boost.asio之异步编程
- boost::asio编程-异步TCP
- Boost asio之网络编程
- boost之asio异步io使用实例
- boost asio异步通信
- boost asio 异步
- 异步工作 boost asio
- 异步客户端 boost asio
- 异步服务端 boost asio
- 异步客户端 boost asio
- 异步服务端 boost asio
- boost asio 异步io
- boost::asio的同步方式和异步方式网络编程
- Boost.Asio基础(五) 异步编程初探
- 整理socket编程<二>:boost::asio实现异步服务器
- 【boost学习】之boost::asio(3)——socket编程
- boost::asio学习之[三 .2]异步tcp service
- boost::asio学习之[四]udp client server 异步server
- QtWidgets继承关系树
- Ubuntu 14.04安装dede织梦管理系统
- mvn plugin checkstyle/findbugs/pmd在多module项目中的使用
- TCP连接TIME_WAIT和CLOSE_WAIT状态
- LeetCode题目难度分布(含面试频率及使用的数据结构与算法)
- boost.asio之异步编程
- 27. Remove Element
- 谷歌汽车出误判曝光 6大奇葩科技更牛
- iOS开发之内存管理:借助工具解决内存问题
- android点点滴滴
- java后台框架 springmvc整合mybatis框架源码 bootstrap html5 mysql oracle
- COCOS学习笔记--schedule定时器
- iOS Error Domain=com.alamofire.error.serialization.response Code=-1016
- EasyUI基础知识