<>第一章 入门

<>1. 同步VS异步

同步编程
中,操作是顺序执行,比如从socket中读取(请求),然后写入(回应)到socket中。每一个操作都是阻塞的。因为操作是阻塞的,所以为了不影响主程序,当在socket上读写时,通常会创建一个或多个线程来处理socket的输入/输出。因此,同步的服务端/客户端通常是多线程的。
异步编程
中,是时间驱动的,虽然启动了一个操作,但是你不知道它何时会结束;它只是提供一个回调给你,当操作结束时,它会调用这个API,并返回操作结果。只需要一个线程。

基础的同步客户端例子:
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
);

解释:程序至少需要一个io_service实例。Boost.Asio使用io_service同操作系统的输入/输出服务进行交互。通常一个io_service就足够了。然后创建你想要链接的地址和端口(tcp或者udp),再建立socket,最后把socket和我们创建的地址和端口连接起来。

基础的同步服务端例子:
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)); } }

解释:首先同样是至少需要一个io_service实例,然后你指定你想要监听的端口,再创建一个接收器,一个用来接收客户端连接的对象。在接下来的循环中,创建一个虚拟的socket来等待客户端的连接,然后当一个连接被建立时,创建一个线程来处理这个连接。再client_session线程中来读取一个客户端的请求,进行解析,然后返回结果。

基础的异步客户端例子:
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_hander); //异步连接 service.run(); void connect_hander(
const boost::system::error_code & ec) { //如果ec返回成功我们就知道连接成功了 }

解释:首先需要创建至少一个io_service的实例,创建它的地址,创建socket,并连接它们俩。当连接完成时,它就异步连接到了指定的地址和端口,也就是说connect_handler被调用了。当connect_handler被调用时,检查错误代码ec,如果成功,就可以向服务端进行异步的写入。

注意:只要还有待处理的异步操作,service.run()循环就会一直运行。在上述例子中,只执行了一个这样的操作,就是socket的async_connect。在这之后,service.run()就推出了。每一个异步操作都有一个完成处理程序----一个操作完成之后被调用的参数。

基础的异步服务端例子:
using boost::asio; typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
//typedef取别名 io_service service; ip::tcp::endpint ep(ip::tcp::v4(), 2001);
//监听端口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))
; //注意这里的handle_accept() } void handle_accept(socket_ptr sock, const boost::
system::error_code & err) { if (err) return; //从这里开始,可以从socket读取或者写入 socket_ptr
sock(new ip::tcp::socket(service)); start_accept(sock); }

解释:在上述代码片段中,首先,你创建一个io_service实例,指定监听端口。然后创建一个接收器acc------一个接收客户端的连接,创建虚拟的socket,异步等待客户端连接的对象。最后,运行异步service.run()循环,当接收到客户端连接时,handle_accept被调用(调用async_accept的完成处理程序)。如果没有错误,这个socket就可以用来做读写操作。在这个socket之后,你创建了一个新的socket,然后再次调用start_accept(),用来创建另外一个“等待客户端连接”的异步操作,从而使service.run()循环一直保持忙碌状态。

<>2. 异步VS错误代码

Boost.Asio允许同时使用异常处理或者错误代码,所有的异步函数都有抛出错误和返回错误码两种方式的重载。当函数抛出错误时,它通常抛出boost::system::system_error的错误。
using boost::asio; ip::tcp::endpoint ep; ip::tcp::socket sock(service); sock.
connect(ep); //第一行 boost::system::error_code err; sock.connect(ep, err); //第二行
解释:在前面的代码中,sock.connect(ep)会抛出错误,sock.connect(ep,err)则会返回一个错误码。

再看下面代码片段:
try { sock.connect(ep); } catch(boost::system::system_error e) { std::cout << e
.code() << std::endl; }
下面的代码片段和前面的时一样的:
boost::system::error_code err; sock.connect(ep, err); if(err) std::cout << err
<< std::endl;
解释:当使用异步函数时候,你可以在你的回调函数里面检查其返回的错误码。异步函数从来不抛出异常,因为这样做毫无意义。那谁会捕获到它呢?

在你的异步函数中,你可以使用异常处理或者错误吗,但要保持一致性。同时使用这两种方式会导致问题,大部分时候是崩溃。如果你的代码很复杂(调用很多socket读写函数),最好选择异常处理的方式,吧你的读写包含在一个函数try()catch块里。
voide client_session(socket_ptr sock) { try{ ... }catch(boost::system::
system_err e){ //错误处理 } }
如果使用错误码,可以使用下面代码片段很好的检测连接是合适关闭的:
char data[512]; boost::system::error_code error; size_t length = sock.read_some
(buffer(data),error); if(error==error::eof) return; //连接关闭
Boost.Asio的所有错误码都包含在命名空间中。如果想要了解更多的细节,请参照boost/asio/error.hpp头文件。

<>3. Boost.Asio中的线程

当说到Boost.Asio的线程时,我们经常讨论:

*
io_service:io_service是线程安全的。几个线程可以同时调用io_service::run()。大多数情况下你可能在一个单线程函数中调用io_service::run(),这个函数必须等待所有异步操作完成之后才能继续执行。然而,事实上你可以在多个线程中调用io_service::run()。这会阻塞所有调用io_service()的线程。只要当中任何一个线程调用了io_service::run(),所有的回调都会同时被调用;这也就意味着,当你一个线程线程中调用io_service::run()时,所有的回调都被调用了。
* socket:socket类不是线程安全的。所以,你要避免在某个线程里读一个socket时,同时在另一个线程里对其进行写入操作。
*
utility:对utility来说,因为它不是线程安全的,所以通常也不提倡在多个线程里面同时使用。里面的方法经常知识在很短的时间里面使用以下,然后就释放了。

除了你自己创建的线程,Boost.Asio本身也包含几个线程。但是可以保证的是那些线程不会调用你的代码。这也意味着,只有调用了io_service::run()方法的线程才会调用回调函数。

<>4.不仅仅是网络通信

除了网络通信,Boost.Asio还包含了其他的I/O功能。
Boost.Asio支持信号量,比如SIGTERM(软件终止)、SIGINT(中断信号)、SIGSEGV(段错误)等等。
你可以创建一个signal_set实例,指定异步等待的信号量,然后当这些信号量产生时,就会调用你的异步处理程序:
void signal_handler(const boost::system::error_code & err, int signal) {
//记录日志,然后退出应用 } boost::asio::signal_set sig(service, SGINT, SIGTERM); sig.
async_wait(signal_handler);
解释:如果SIGINT产生,你就能在你的signal_handler回调中捕获到它。

你可以使用Boost.Asio轻松的连接到一个串口。在Windows上端口名称是COM7,在POSIX平台上是/dev/ttyS0:
io_service service; serial_port sp(service, "COM7");
打开端口后,你就可以使用下面的代码设置一些端口选项,比如端口的波特率、奇偶校验和停止位:
serial_port::baud_rate rate(9600); sp.set_option(rate);

打开端口后,你可以把这个串口看作一个流,然后基于它使用自由函数对串口进行读/写操作。比如async_read(),write,async_write(),就像下面的代码片段:
char data[512]; read(sp, buffer(data,512));
Boost.Asio也可以连接到Windows的文件,然后同样使用自由函数,比如read(), asyn_read()等等,就像下面的代码片段:
HANDLE h = ::OpenFIle(...); windows:;stream_handle sh(service, h); char data[
512]; read(h, buffer(data, 512));
对于POXIS文件描述符,比如管道,标准I/O和各种设备(但不包括普通文件)你也可以这样做,就像下面的代码所做的一样:
posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO)); char data[512];
read(sd_in, buffer(data, 512));
<>5.计时器

一些I/O操作需要一个超时时间。这只能应用在异步操作上(同步意味着阻塞,因此没有超时时间)。例如,下一条信息必须在100ms内从你的同伴那传递给你。
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();

解释:在上述代码片段中,如果你在超时之前读完了数据,read则被设置成true,这样我们的伙伴就及时地通知了我们。否则,当deadline_handler被调用时,read还是false,也就意味着我们的操作超时了。

Boost.Asio也支持同步计时器,但是它们通常和一个简单的sleep操作是一样的。boost::this_thread::sleep(500);这段代码和下面的代码片段完成了同一件事情:
deadline_timer t(service, boost::posix_time::milliseconds(500)); t.wait();
解释:也就是说经常遇到的sleep(500),它的意思是线程在这个时间段内等待连接,如果没有I/O端实例连接进入,则会failed。

<>6.io_service类

你应该已经发现大部分使用Boost.Asio编写的代码都会使用几个ios_service的实例。io_service是这个库里面最重要的类;它负责和操作系统打交道,等待所有异步操作的结束,然后为每一个异步操作调用其完成处理程序。

如果你选择用同步的方式来创建你的应用,则不需要学习下面的内容。在下面的例子中,包括3个异步操作,2个socket连接操作和一个计时器等待操作:

* 有一个io_service实例和一个处理线程的单线程例子: io_service service; service;
//所有的socket操作都由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::posixtime::seconds(5)); t.
async_wait(timeout_handler); service.run();
* 有一个io_service实例和多个处理线程的多线程例子: io_service service; ip::tcp::socket sock1(
service); ip::tcp::socket sock2(service); sock1.asyncconnect( ep,
connect_handler); sock2.async_connect( ep, connect_handler); deadline_timer t(
service, boost::posixtime::seconds(5)); t.async_wait(timeout_handler); for ( int
i= 0; i < 5; ++i) boost::thread( run_service); void run_service() { service.run
(); }
* 有多个io_service实例和多个处理线程的多线程例子: io_service service[2]; ip::tcp::socket sock1(
service[0]); ip::tcp::socket sock2(service[1]); sock1.asyncconnect( ep,
connect_handler); sock2.async_connect( ep, connect_handler); deadline_timer t(
service[0], boost::posixtime::seconds(5)); t.async_wait(timeout_handler); 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(); }
首先,要注意你不能拥有多个io_service实例却只有一个线程。下面的代码片段没有任何意义:
for ( int i = 0; i < 2; ++i) service[i].run();

注意:上面的代码片段没有意义是因为service[1].run()需要service[0].run()先结束。因此,所有由service[1]处理的异步操作都需要等待,这显然不是一个好主意。

在前面的3个方案中,我们在等待3个异步操作结束。为了解释它们之间的不同点,我们假设:过一会操作1完成,然后接着操作2完成。同时我们假设每一个完成处理程序需要1秒钟来完成执行。

在第一个例子中,我们在一个线程中等待三个操作全部完成,第1个操作一完成,我们就调用它的完成处理程序。尽管操作2紧接着完成了,但是操作2的完成处理程序需要在1秒钟后,也就是操作1的完成处理程序完成时才会被调用。

第二个例子,我们在两个线程中等待3个异步操作结束。当操作1完成时,我们在第1个线程中调用它的完成处理程序。当操作2完成时,紧接着,我们就在第2个线程中调用它的完成处理程序(当线程1在忙着响应操作1的处理程序时,线程2空闲着并且可以回应任何新进来的操作)。

在第三个例子中,因为操作1是sock1的connect,操作2是sock2的connect,所以应用程序会表现得像第二个例子一样。线程1会处理sock1
connect操作的完成处理程序,线程2会处理sock2的connect操作的完成处理程序。然而,如果sock1的connect操作是操作1,deadline_timer
t的超时操作是操作2,线程1会结束正在处理的sock1 connect操作的完成处理程序。因而,deadline_timer t的超时操作必须等sock1
connect操作的完成处理程序结束(等待1秒钟),因为线程1要处理sock1的连接处理程序和t的超时处理程序。
简单的说就是时间判断函数是由1线程来统筹的,所以当2线程超时的时候,还是1线程来处理超时这个问题,就导致1线程的操作也中断了。

下面是你需要从前面的例子中学到的:

*

第一种情况是非常基础的应用程序。因为是串行的方式,所以当几个处理程序需要被同时调用时,你通常会遇到瓶颈。如果一个处理程序需要花费很长的时间来执行,所有随后的处理程序都不得不等待。

*

第二种情况是比较适用的应用程序。他是非常强壮的——如果几个处理程序被同时调用了(这是有可能的),它们会在各自的线程里面被调用。唯一的瓶颈就是所有的处理线程都很忙的同时又有新的处理程序被调用。然而,这是有快速的解决方式的,增加处理线程的数目即可。

*

第三种情况是最复杂和最难理解的。你只有在第二种情况不能满足需求时才使用它。这种情况一般就是当你有成千上万实时(socket)连接时。你可以认为每一个处理线程(运行io_service::run()的线程)有它自己的select/epoll循环;它等待任意一个socket连接,然后等待一个读写操作,当它发现这种操作时,就执行。大部分情况下,你不需要担心什么,唯一你需要担心的就是当你监控的socket数目以指数级的方式增长时(超过1000个的socket)。在那种情况下,有多个select/epoll循环会增加应用的响应时间。

如果你觉得你的应用程序可能需要转换到第三种模式,请确保监听操作的这段代码(调用io_service::run()的代码)和应用程序其他部分是隔离的,这样你就可以很轻松地对其进行更改。

最后,需要一直记住的是如果没有其他需要监控的操作,.run()就会结束,就像下面的代码片段:
io_service service; tcp::socket sock(service); sock.async_connect( ep,
connect_handler); service.run();
在上面的例子中,只要sock建立了一个连接,connect_handler就会被调用,然后接着service.run()就会完成执行。

如果你想要service.run()接着执行,你需要分配更多的工作给它。这里有两个方式来完成这个目标。一种方式是在connect_handler中启动另外一个异步操作来分配更多的工作。
另一种方式会模拟一些工作给它,用下面的代码片段:
typedef boost::shared_ptr work_ptr; work_ptr dummpy_work(new io_service::work(
service));
上面的代码可以保证service.run()一直运行直到你调用useservice.stop()或者 dummy_work.reset(0);// 销毁
dummy_work.

总结:

做为一个复杂的库,Boost.Asio让网络编程变得异常简单。构建起来也简单。而且在避免使用宏这一点上也做得很好;它虽然定义了少部分的宏来做选项开关,但是你需要关心的很少。

Boost.Asio支持同步和异步编程。他们有很大不同;你需要在项目早期就选择其中的一种来实现,因为它们之间的转换是非常复杂而且易错的。

如果你选择同步,你可以选择异常处理或者错误码,从异常处理转到错误码;只需要在call函数中增加一个参数即可(错误码)。

Boost.Asio不仅仅可以用来做网络编程。它还有其他更多的特性,这让它显得更有价值,比如信号量,计时器等等。

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信