boost.asio
boost.asio为异步IO提供了一份标准的C++的跨平台实现,特别针对网络IO提供了良好的支持,使之成为C++网络编程利器。关于如何使用asio,boost文档中已经有了详尽说明,而且附带的例子也很直观,我们不必再造轮子;本文则结合asio的基本应用,侧重于源代码的分析,特别是针对平台上的实现进行分析。
纵观asio源码,在统一的接口层之下,asio提供了大量的类来支持不同的平台(、Unix...)、不同的IO类型(同步、异步)及IO模型(IOCP、、Poll)及网络协议(TCP,UDP,ICMP)。归纳起来,这一大堆类可以分为三层,分别是:
本系列文章从类型入手,开启代码分析行程,采用自底向上的方法,逐层推导,最后得到asio的体系结构;如果想先了解asio的整体结构,可以先跳到asio体系结构部分。
(理论上来说,行文伊始还是该先举个简单的例子说一下asio如何使用,不过这样的例子少说也要六七十行代码,为节约篇幅就不贴了;真想先看一个完整例子的,烦请移步下面链接瞧下那只麻雀:)
·类
从第一个boost.asio的教程开始,boost文档就一直在告诉我们:使用boost.asio第一步就是要创建一个对象。那么是个什么东西呢?
boost.asio文档说,为下面的这些异步IO对象提供最核心的IO功能:
接着,文档就会说,像下面这样,就可以简简单单声明一个对象了:
int main()
boost::asio:: io;
上面的一行代码,表明无限地风光与潇洒,看起来简简单单一行,就几乎有了异步IO、网络操作的框架,那么他到底代表了什么,在后面,又有什么事情发生呢?又是如何支撑起这些功能的呢?带着这些问题,我们开始分析吧……
从C++的角度看,上面的这一行代码,无非就是定义了一个的实例,而C++的底层机制所隐藏起来的东西,无非就是初始化该对象的所有数据成员。再来看的声明,我们知道,该类除了一堆成员函数之外,事实上只有三个成员(暗想:这几个成员肯定很是神奇无匹了):
#if () || ()
:: init_;
#elif (__sun) || () || () || (_AIX) \
|| ()
:: init_;
#endif
// The .
boost::asio::::* ;
// The .
& impl_;
暂时抛开那几个成员,再来看一下比较重要的一个函数:run() 的实现,发现该函数也就是将真正的功能委托给成员impl_去做事儿了:
std:: ::run()
boost:::: ec;
std:: s = impl_.run(ec);
boost::asio::::(ec);
s;
种种迹象表明,impl_是个巨牛的东西了。统揽的实现,我们不难发现,该类的所有功能,几乎都是委托给了impl_成员去干了——典型的“有事秘书干”。不过想想也挺容易理解的,提供了一个上层的接口,充当抛头露面的BOSS,真正的工作则委托给下一层的员工去实现——哪家公司不是这样呢?
所以,要想了解的玄机,就需要弄清楚这三个数据成员到底是什么来历,特别是impl_的来历才行。
的初始化
成员init_ 在的各个函数中,并没有显式用到。其存在的价值,就在于该类的构造函数里面,调用了初始化相关的代码,具体到平台,就是的初始化,也就是调用 ::() 这一编程所必须调用的第一个函数;而其析构函数则进行清理工作,同样交由函数 ::() 完成。
也就是说,通过init_数据成员的创建与销毁,自动完成了相关的初始化及清理工作。
的实现委托
前面说过,impl_ 带着无限的神秘默默地完成了::run()的功能。至于他到底是什么style,现在来揭开盖头吧。
从直接声明来看,impl_ 具有 & 类型。用不难发现该类型只不过是一个类型别名:
:: ;
一波未平一波又起,这儿又冒出个。继续刨根问底,找到:
#if ()
{ ; }
#else
{ ; }
#endif
终于知道,在某些情况下,它是 ,在某些情况下,是task_ 。事实上,在Win NT 环境下,如果没有禁用IOCP,也就是没有声明OCP 这个预处理器指令,那么asio就采用 来做那些脏活累活;在剩下的其他平台,或者Win上禁用了IOCP,则使用 来做事儿了。
先稍微提一下, 是对环境下的IOCP(完成端口IO)模型的封装,该类作为boost.asio在下的核心,我们在后面详细分析其实现。
的服务管理
的另外一个数据成员,又具备什么样的身份和功能呢?我们来看看 的构造函数:
::()
:
new boost::asio::::(
*this,
(0),
(std::::max)()
),
impl_(->())
构造函数为了初始化,动态分配了一个boost::asio:::: 类对象。为了构造该对象,提供了三个参数:
再来观察的实现,发现其实际就是一个链表,管理所容纳的所有对象(就是一种)。每种都有一个id,链表以此id作为标志,在客户通过来请求一种服务时,例如调用 (&) 时,会查找链表,如果有对应类型的服务,就返回该类型服务实例的指针;否则就创建一个新的对象,并加入到链表末端,再返回此新创建的实例;通过这种形式,确保每种类型的服务都只有一个实例存在。
asio提供了这样几个函数,来进行的管理和使用——这也为扩展asio提供了可能,例如可以自己定义一种服务,使用加入进行管理。
& (& ios);
void (& ios, * svc);
bool (& ios);
::类型及跨平台策略
boost::asio::
+ run()
+ poll()
+ void ()
+ void stop()
- :: init_
- ::*
- & impl_
class boost::asio::::::id
class boost::asio::::work
enum boost::asio::::
class boost::asio::::
class boost::asio::::
- key key_
- boost::asio::& ;
- * next_;
内部定义了这样一些类型,来为其服务:
针对类型,asio从其派生出了数十个类分别完成不同的功能,例如在Win上充当的类,以及为各种IO 类型提供服务的类,如对应于TCP的e,对应于UDP的 。下图显示了asio常用到的服务类,及其针对不同平台的适配类之间的关系。
图中左边的类,会在我们的应用程序中直接用到(由于asio又对这些类提供了一层动态组装,所以代码中不会去直接声明这些类型的实例,但是剥掉动态组装的外衣,我们声明的仍然是这些类的实例,具体在下面一部分说明),作为应用层的类;而右边部分,则是针对不同平台所提供的不同实现,作为平台适配层。
应用层类在编译时,根据所在平台(其实是喂给编译器的各种预处理宏,如 ),选择对应的类进行编译。例如用于TCP的服务类e是这样进行选择的:
class e
:
// The type of the - .
#if ()
::ice
;
#else
::ice
;
#endif
// The - .
;
…
};
其他需要进行平台决策的类型,都是采用这种技术,来选择不同的实现的。
从::派生的完整的类列表如下:
还有几个非常重要的类,他们作为劳苦大众在金字塔底层默默提供功能,但却没有从::派生;他们是上述那些服务类在各个平台的具体实现,为金字塔中间层的服务类提供再服务的(不难想象,提供服务的方式,又是那种“有事儿秘书干”的方式):
·io
asio的文档,告诉我们在声明一个对象之后,就可以创建io对象去干活了,例如:
int main(int argc, char* argv[])
boost::asio:: ;
tcp:: ();
tcp::::query query("", "80");
tcp:::: = .(query);
上图中main()的第二行代码声明了一个tcp::对象,后续进行地址解析的逻辑,都是围绕此对象展开的。那么这个是一个什么样的类型呢?阅读源代码我们在boost::asio::ip::tcp类内部看到了这样的类型别名定义:
;
是否似曾相识呢?是的,这和我们STL中的, 等一样,使用的都是某个模板类的一个实例。这里的是在tcp模版参数下的实例,是在char模版参数下的实例——boost库和STL库统一风格的一个体现。
asio大量采用这种技术,所有和一样提供io功能的类,都是某个 模版类的实例化。下面我们来研究一下asio的io 逻辑。
io 类关系网
继续向上追溯,知道该类从派生,主要负责地址解析相关的操作,提供的主要接口有(), ()等。
查看整个asio的源代码,我们发现从派生的类不少,他们分别负责一些具体的事务,例如r可以作为一个服务器进行侦听,提供了诸如bind(), ()等接口;再如类是对 IO 操作的封装,提供了(), (), (), (), (), ()等接口。
整个asio中的io 关系网,我们用下图来显示:
这些io 的功能,简述如下:
另外一点,所有这些io 的构造函数,都要求有一个& 作为参数,使用这一参数,这些对象知道了自己的归属,之后自己所要派发出去的同步、异步操作请求,都将通过自己所在的这个对象来完成。这也就说明了,为什么创建对象是整个asio程序的第一步。
io 服务委托
上述的这些io 类,提供了应用开发中常用的各种“实际”功能,例如地址解析,以及读写等,那么这些io 类和第一部分中的类之间存在着什么样的关系呢?会不会是这些io 只是一个应用的接口,而具体的功能则委托给类来完成呢?如果是这样的,那么又是如何实现的呢?
带着这些问题,我们继续研究类的源代码,看看他所提供的功能,到底是如何实现的。
是如何委托的
我们知道,类是在boost::asio::ip::tcp类中的一个类型别名:
;
那么,类所提供的()接口,就来自于类。再来看看::()的实现——该函数有两个重载,我们以其中一个为例:
void (& q, () )
// If you get an error on the line it means that your does
// not meet the type for a .
(
, , ) ;
->.(this->, q,
()());
可见,()果然是委托给了某个类来做事的——再一次,典型的有事儿秘书干。那么,这儿的“this->”又是什么呢?
通过跟踪代码的执行,知道该其实是第一部分曾经提到过的boost::asio::ip::,而它又委托给了boost::asio::::——这家伙再无其他可以委托的对象了,只有苦逼的自己做事儿了——其基本思路就是先创建一个用以地址解析的,用这个op来代表本次异步操作,之后启动op去做事情。至于op又是什么东西,稍后在部分做介绍;先贴出这段苦主:
// boost::asio::::
(& impl,
& query, )
// and an to wrap the .
op;
::ptr p = { boost::(),
::(
(op), ), 0 };
p.p = new (p.v) op(impl, query, , );
((p.p, "", &impl, ""));
(p.p);
p.v = p.p = 0;
PS:
- 做过开发的对这种形式似曾相识,这多少和 异曲同工
- 那几行创建op的代码还不是很明白
OK,至此,整个()从上到下就拉通了。概括起来,就是io 将具体功能委托给服务类,服务类又委托给和平台实现相关的服务类来完成最后的功能。
的服务创建
通过的执行,我们知道了其层层委托关系,那么所委托的this->又是怎么来的呢?下面部分,我们来分析所做的服务管理工作。
首先来看该的具体类型。
要想知道是如何创建的,我们就要追根朔源找到的具体类型声明,看看到底是如何将拉上贼船的。好吧,我们再次从头开始(别嫌啰嗦):
;
针对这个模版实例,将展开:
= >
class :
à
= >
class : < >
再对进行展开:
class
à
< >
class
在内部展开:
;
à
;
于是,可以确定::的类型为&;从而将io 和下层的关联起来。
再来看::的初始化。我们来看其构造函数:
(boost::asio::& )
: (boost::asio::())
.();
很明显,在构造过程中,使用的返回值来初始化该成员;我们知道,会在所维护的链表中查找该类型,如果没有,就创建一个新的实例;在此,就可以确保的实例已经被创建出来了,该服务就可以工作了(当然,不要忘记,该事实上又委托了一层,而这个最底层的实例,在此的构建过程中,内部再次调用()创建出来了。过程如下:
除此之外,在构造函数体中,调用了的函数,做进一步的初始化。(PS:是否有中的二段构造的影子呢?)
asio 的io逻辑总结
前面部分以为例,分析了的功能和对应之间的关系,纵观所有的io ,都是采用了这种模式,总结起来有如下几点:
· asio提供了多个 模版类。
· 应用层使用对应的 模版类的的具体类,对外提供服务接口。
· io 内部,将操作委托给底层服务类。
· 底层服务类再次将操作委托给平台实现层,完成实际的工作。
·asio的体系结构
三层类关系图
根据前面的分析,我们知道asio有着这样的逻辑:
鉴于此,我们将asio体系划分为三层:io 层, 模版类层,服务层。
基本的体系结构关系如下图所示。注意:图中并非全部asio中的类。
动态组装
前面已经提过,和STL中的一样,都是使用了模板类的一个具体实例。这种在编译时动态地选择对应的组件进行编译,我们姑且称为动态组装。这种技术使得我们可以对asio的进行扩展。例如自己实现一个的类,然后告诉,你要使用自己的类,而非默认的那个……
不过话说回来,自己对boost或者STL进行扩展,需要点实力的
·asio的实现
还记得前面我们在分析的实现的时候,挖了一个关于的坑?为了不让自己陷进去,现在来填吧;接下来我们就来看看asio中的各种。
和前面提到过的的类似,这里的也分为两大系:IOCP 和系列。这里我们重点关注下图中红色部分表示的IOCP 系列。
基类
从上图可以看到,所有IOCP 的,其基类都是 结构,该结构是Win32进行交叠IO一个非常重要的结构,用以异步执行过程中的参数传递。所有的直接从该结构继承的结果,就是所有对象,可以直接作为结构在异步函数中进行传递。
例如在中,为了启动一个的异步操作, 函数就直接把传递进来的指针作为结构传递给::函数,从而发起一个异步服务请求。
void ::(
::type& impl,
* , std:: ,
:: flags, bool noop, * op)
(impl);
.();
if (noop)
.(op);
else if (!(impl))
.(op, boost::asio::error::);
else
DWORD = 0;
DWORD = flags;
int = ::(impl., , (),
&, &, op, 0);
DWORD = ::();
if ( == D)
= ;
else if ( == LE)
= ;
if ( != 0 && != )
.(op, , );
else
.(op);
执行流程
关于对象的创建、传递,以及完成的执行序列等,使用下图可以清晰的描述。
下表反映了环境下,部分的异步请求所对应的服务、win函数、等信息:
异步请求
服务
start op
Win32函数
对应
ip::tcp::::
ice
()
::
ip::tcp::::
()
::
_op
ip::tcp::::
()
::
_op
ip::tcp::::
()
::
_op
ip::tcp::::
()
::
_op
ip::tcp::::
()
::
pt_op
ip::tcp::::
()
::
静态的
不知你是否注意到,在的类图中,所有从继承的子类,都定义了一个()函数,然而该函数声明为,这又是为何呢?
我们以_op为例来进行说明。该类中的是这样声明的:
void (* owner,
* base,
const boost::::& ,
std:: )
该类的构造函数,又把此函数地址传递给父类去初始化父类成员,这两个类的构造函数分别如下,请注意加粗代码:
_op ::
_op(:: state,
::pe ,
const e& , & )
: (&_op::),
(state),
(),
(),
(()())
::( func)
: next_(0),
func_(func)
reset();
至此,我们明白,将声明为,可以方便获取函数指针,并在父类中进行回调。那么,不仅要问,既然两个类存在继承关系,那么为何不将声明为虚函数呢?
再回头看看这些类的最顶层基类,就会明白的。最顶层的基类,使得将对象作为对象在异步函数中进行传递成为可能;如果将声明为虚函数,则多数编译器会在对象起始位置放置vptr,这样就改变了内存布局,从而不能再把对象直接作为对象进行传递了。
当然,一定要用虚函数的话,也不是不可能,只是在传递对象的时候,就需要考虑到vptr的存在,这会有两个方面的问题:一是进行多态类型转换时,效率上的损失;二是各家编译器对vtpr的实现各不相同,跨平台的asio就需要进行多种适配,这无疑又过于烦躁了。于是作者就采取了最为简单有效的方式——用函数来进行回调——简单,就美。
的实现
在 NT环境下(IOCP ),代表着,是整个asio的运转核心。本节开始来分析该类的实现。
从类的命名也可以看出,IOCP是该实现的核心。IOCP(IO Port, IOCP)在上,可以说是效率最高的异步IO模型了,他使用有限的线程,处理尽可能多的并发IO请求。该模型虽说可以应用于各种IO处理,但目前应用较多的还是网络IO方面。
我们都知道,在是环境下使用IOCP,基本上需要这样几个步骤:
使用Win函数rt()创建一个完成端口对象; 创建一个IO对象,如用于的对象; 再次调用rt()函数,分别在参数中指定第二步创建的IO对象和第一步创建的完成端口对象。由于指定了这两个参数,这一次的调用,只是告诉系统,后续该IO对象上所有的完成事件,都投递到绑定的完成端口上。 创建一个线程或者线程池,用以服务完成端口事件;
所有这些线程调用tatus()函数等待一个完成端口事件的到来;
进行异步调用,例如()等操作。 在系统执行完异步操作并把事件投递到端口上,或者客户自己调用了()函数,使得在完成端口上等待的一个线程苏醒,执行后续的服务操作。
那么,这些步骤,是如何分散到asio中的呢?来吧,先从完成端口创建开始。
完成端口的创建
如上所述,完成端口的创建,需要调用rt()函数,在的构造函数中,就有这样的操作:
::(
boost::asio::& , )
: boost::asio::::(),
iocp_(),
(0),
(0),
(0),
(0),
(0)
;
iocp_. = ::rt(, 0, 0,
((std::min)(, DWORD(~0))));
if (!iocp_.)
DWORD = ::();
boost:::: ec(,
boost::asio::error::());
boost::asio::::(ec, "iocp");
的构造函数,负责创建一个完成端口,并把此完成端口对象的句柄交给一个进行管理——的唯一用途,就是在对象析构时,调用::()把句柄资源关闭,从而保证不会资源泄露。
我们在环境下,声明一个boost::asio::对象,其内部就创建了一个的实例;因此,一个对象就对应着一个完成端口对象——这也就可以解释,为什么所有的IO 都需要一个参数了——这样,大家就好公用外面定义好的完成端口对象。
除了对象会创建一个完成端口对象,事实上,在asio中,另外一个也会创建一个,这就是boost::asio::ip::。该类对应的实现boost::asio::::中,有一个数据成员是: ,这样就同样创建了一个完成端口对象:
boost {
asio {
{
class e
...
:
// used for host .
;
...
至于该完成端口的用途如何,我们在后续部分再来说明——搽,又开始挖坑了。
完成端口的绑定
在创建了io对象后,例如,就需要将此对象和完成端口对象绑定起来,以指示操作系统将该io对象上后续所有的完成事件发送到某个完成端口上,该操作同样是由rt()函数完成,只是所使用的参数不同。
在中,这个操作由下面的代码完成——请注意参数的差别:
boost:::: ::(
, boost::::& ec)
if (::rt(, iocp_., 0, 0) == 0)
DWORD = ::();
ec = boost::::(,
boost::asio::error::());
else
ec = boost::::();
ec;
通过代码搜索,我们发现函数::()内部调用了();该函数的作用是打开一个(其中调用了函数()去创建一个),也就是说,在打开一个后,就把该绑定到指定的完成端口上,这样,后续的事件就会发送到完成端口了。
此外还有另外的和相关的两个函数也调用了(),不再贴出其代码了。
线程函数
IOCP要求至少有一个线程进行服务,也可以有一堆线程;早就为这些线程准备好了服务例程,即::run()函数。
void ::run()
// a pool of to run all of the .
std::::> > ;
for (std:: i = 0; i < ; ++i)
boost::
(
new boost::(
boost::bind(&boost::asio::::run, &)
);
.();
// Wait for all in the pool to exit.
for (std:: i = 0; i < .size(); ++i)
[i]->join();
由于::run()又是委托::run()来实现的,我们来看看后者的实现:
::run(boost::::& ec)
if (::dd(&, 0) == 0)
stop();
ec = boost::::();
0;
;
:: ctx(this, );
n = 0;
while ((true, ec))
if (n != (std::::max)())
++n;
n;
run()首先检查是否有需要处理的操作,如果没有,函数退出;使用 来记录当前需要处理的任务数。如果该数值不为0,则委托函数继续处理——asio中,所有的脏活累活都在这里处理了。
::函数较长,我们只贴出核心代码
::(bool block, boost::::& ec)
for (;;)
// Try to for and ops.
if (::(&, 0, 1) == 1) ? #1
mutex:: lock();
// and .
ops;
ops.push();
.(ops);
tions(ops); ? #2
();
// Get the next from the queue.
DWORD = 0;
= 0;
= 0;
::(0);
BOOL ok = ::tatus(iocp_., &,
&, &, block ? : 0); ? #3
DWORD = ::();
if ()
* op = (); ? #4
boost:::: (,
boost::asio::error::());
// We may have been the and in the
// .
if ( == )
= boost::::((op->),
*(op->));
= op->;
// any has been saved into the
// .
else
op-> =
另外还有一些提供的操作,例如请求执行代为执行指定的操作:
所有这些需要自己投递完成端口数据包的操作,基本上都是这样一个投递流程:
OK,至此,基本分析完了的投递,总数填了一个前面挖下的坑。
自己的IOCP
前面说过,自己会创建一个IOCP,为什么会这样呢?由于Win32下面没有提供对应于地址解析的版本的函数,为了实现操作,作者自己实现了这样一个异步服务。在内部,有一个数据成员,该数据成员创建了一个IOCP;除此之外,该内部还启动一个工作线程来执行::run(),使用此线程来模拟异步服务。
使用进行的详细过程如下:
Main (IOCP#1)
(IOCP #2)
1. 构建主 对象, IOCP#1 被创建
2. 构建 对象, IOCP#2 被创建,同时该持有主的引用
3. 发起异步调用:.()
4. 被创建
5. 线程启动,主线程开始等待
6. 开始运行,激活等待事件,并在 IOCP#2上开始等待
7. 线程恢复执行;将op投递到 IOCP#2
8. 执行op->() 操作,地址解析完成后,将op再回投给IOCP#1
9. () 得到从线程投递回来的op,开始执行op->() 操作,此时回调所设置的
10. 结束
请注意step8 和 step9,执行同样一个op->()函数,为什么操作不一样呢?看其实现就知道,该函数内部,会判断执行此函数时的owner,如果owner是主对象,则说明是在主线程中执行,此时进行的回调;否则就说明在工作线程中,就去执行真正的地址解析工作;
任务的取消
针对上提交的异步请求,可以使用()函数来取消掉该上所有没执行的异步请求。
使用该函数,在 Vista(不含)以前的版本,有几点需要注意:
针对这些问题,另外的替代方案是:
在 vista及后续版本中,()函数在内部调用Win32函数 (),该函数可以取消来自任何线程的异步操作请求,不存在上述问题。
需要注意的是,即使异步请求被取消了,所指定的也会被执行,只是传递的error code 为:boost::asio::error::。
ice实现
该提供了下所有相关的功能,是asio在环境中一个非常重要的角色,他所提供的函数主要分下面两类:
不过关于该类的实现前面已经做了较多的涉及,不再单独详述了。
前摄器模式
现在我们已经把环境下所涉及到的关键部件都涉及到了,此刻我们再回过头来,从高层俯瞰一下asio的架构,看看是否会有不一样的感受呢?事实上,asio的文档用下面的图来说明asio的高层架构——前摄器模式,我们也从这个图开始:
呵呵,其实这张图,从一开始就是为了表达(前摄器)模式的,基本上它和asio没半毛钱关系,只不过asio既支持同步IO,又支持异步IO,在异步IO部分,是参照模式来实现的。下面我们来分别说说asio的前摄器模式中的各个组件:
仅仅从asio使用者的角度看,高层的e类就是一个这样的处理器,因为从tcp::发送的异步操作都是由其完成处理的。但是从真正实现的角度看,这样的异步操作在上,大部分由操作系统负责完成的,另外一部分由asio自己负责处理,如,因此操作系统和asio一起组成了异步操作处理器。
在平台上,类通过类的()函数把每个异步操作所设定的 调用起来。
在上,asio的完成事件队列由操作系统负责管理;只不过该队列中的数据有两个来源,一个是内部,另外一个是asio中自己()所提交的事件。
在上,这一功能也是由操作系统完成的,具体来说,我认为是由tatus完成的,而该函数时由()调用的,因此,从高层的角度来看,这个分离器,也是由负责的。
基于上述信息,我们重绘模式架构图如下:
·其他
VS. VS.
是一个全局函数;后面两个则于ip::tcp::的成员个函数;都可以用来异步读取操作,他们有什么样的差别呢。先来看和,他们的文档说明如下:
从文档来看,只有一个单词的差别,一个是read,一个是;反正都是从中获取数据,这两个词有什么差别呢?我是看不出他们的差别,那就看代码吧:
void (const e& ,
() )
// If you get an error on the line it means that your does
// not meet the type for a .
(, ) ;
this->().(this->(),
, 0, ()());
void (const e& ,
() )
// If you get an error on the line it means that your does
// not meet the type for a .
(, ) ;
this->().(this->(),
, 0, ()());
好了,有了源代码,就无从狡辩了吧。他们都是使用底层服务的来读取数据,因此他们的功能是一样的,都是从获取一定的数据,但是该函数并不保证能够从获得指定长度的数据——也即不保证填满缓冲区;
如果想要保证异步操作完成时获取到指定数量(缓冲区的长度)的数据,那么使用全局函数。该函数内部启动一个 ,该op内部,会多次调用.,直到缓冲区填满,或者读操作结束;
这既是这三个函数的异同之所在。
妖怪
前面一节提到过,全局函数 会启动并在该op内部反腐调用async ()来读取数据直到缓冲区慢,或者EOF出现;如果要来看一下的(),你多半会感觉看到妖怪了。乖,别怕,来看看吧:
void ()(const boost::::& ec,
std:: , int start = 0)
std:: n = 0;
(start)
case 1:
n = this->(ec, );
for (;;)
.(
boost::asio::( + , n),
()(*this));
; :
+= ;
if ((!ec && == 0)
|| (n = this->(ec, )) == 0
|| == boost::asio::())
break;
(ec, ());
不知道你是否会和我一样感觉毛骨悚然,惊叫-case的邪乎。是的,说他妖怪,就妖怪在的那两个case标签,第一个“case 1” 还算人性,第二个标签就完全是妖怪了,它放置在for循环的内部——是的,编译器是允许的,假设程序流程首先跳转到标签,在执行完分支后,他会像正常的for循环流程一样,继续for loop的,这就是这个妖怪。
自己写的一个简单的测试及输出:
void ( int state )
int i = 0;
( state )
case 1:
cout