Boost.Asio技术⽂档汇总
Christopher Kohlhoff
Copyright © 2003-2012 Christopher M. Kohlhoff
以Boost1.0的软件授权进⾏发布(见附带的LICENSE_⽂件或从)
Boost.Asio是⽤于⽹络和低层IO编程的跨平台C++库,为开发者提供了C++环境下稳定的异步模型.
综述
基本原理
应⽤程序与外界交互的⽅式有很多,可通过⽂件,⽹络,串⼝或控制台.例如在⽹络通信中,完成独⽴的IO操作需要很长时间.对应⽤程序开发者提出了⼀个挑战.
Boost.Asio 提供了管理需长时间运⾏操作的⼯具,但不必涉及到线程的并发模式和显式锁定.
Boost.Asio 库使⽤C++来实现,提供如⽹络编程等常⽤的操作系统接⼝. Boost.Asio实现了如下⽬标:
· 可移植性Portability.库⽀持⼀系列的常⽤系统操作,具有稳定的跨平台特性.
· 可扩展性Scalability.库可以帮助开发者构建数千并发连接的⽹络应⽤程序.asio库实现的每个系统操作都使⽤了最具扩展性的机制.
· 效率Efficiency.库⽀持发散聚合IO(scatter-gather I/O)等技术,使应⽤程序尽量少的拷贝数据.
· 可以像BSD Sockets⼀样通过API函数建⽴模型概念. BSD Socket API应⽤⼴泛,有很多相关的程序库.其他编程语⾔通常也使⽤其简单的⽹络API接⼝.出于这些原因, Boost.Asio同样使⽤这种已存在的技术进⾏构建.
· 易于使⽤Ea of u.库提供了⼯具箱⽽不是框架来降低新⼿⼊门门槛.使⽤最少时间投⼊来学习基本的规则和⽅针.⽽后,库的使⽤者只需要理解⽤到的特定函数即可.
· 基于更多的抽象.Basis for further abstraction.asio库提供了⾼层次的抽象允许其他开发者对库进⾏扩展.例如,实现常⽤的HTTP协议.
虽然Boost.Asio⼀开始定位于⽹络通信,但其异步IO的概念已经扩展到了如串⼝通信,⽂件描述符等其他操作系统的IO操作⽅⾯.
核⼼概念和功能
解析Boost.Asio
Boost.Asio 可⽤于如socket等IO对象的同步或异步操作.在使⽤Boost.Asio前⾸先了解⼀下Boost.Asio概念图, 以及与应⽤程序的相互集成⽅式.
第⼀个范例,看看处理socket连接的情况.⾸先从同步操作开始.
应⽤程序必须有⼀个io_rvice对象. io_rvice对象负责连接应⽤程序与操作系统的IO服务.
boost::asio::io_rvice io_rvice;
要执⾏IO操作应⽤程序需要⼀个像TCP Socket的IO对象:
boost::asio::ip::tcp::socket socket(io_rvice);
⽽后执⾏同步连接操作,发送如下事件:
1. 应⽤程序调⽤IO对象的初始化连接操作:
中专毕业证书t(rver_endpoint);
2. IO对象向io_rvice 提出请求.
3. io_rvice 调⽤操作系统的功能执⾏连接操作.
4. 操作系统向io_rvice 返回执⾏结果.
5. io_rvice将错误的操作结果翻译为boost::system::error_code类型. error_code可与特定值进⾏⽐较,或作为boolean值检测(fal表⽰⽆错误).结果再传递给IO对象.
6. 如果操作失败,IO对象抛出boost::system::system_error类型的异常.开始操作的代码如下所⽰:
boost::system::error_code ec;
⽽后error_code类型的变量ec被赋予操作的结果值,但不会抛出异常.
对于异步操作,事件顺序有所不同.
1. 应⽤程序调⽤IO对象进⾏连接操作:
socket.async_connect(rver_endpoint, your_completion_handler);
your_completion_handler函数的签名为:
void your_completion_handler(const boost::system::error_code& ec);
执⾏的异步操作需要严格的函数签名.每种操作的合法形式可见参考⽂档.
2. IO对象请求io_rvice的服务.
3. io_rvice 通知操作系统其需要开始⼀个异步连接.
时序过程:(在同步情况下等待包括连接操作时间.)
4. 操作系统指⽰连接操作完成, io_rvice从队列中获取操作结果.
5. 应⽤程序必须调⽤io_rvice::run()(或io_rvice相似的成员函数)以便于接收结果.调⽤io_rvice::run()会阻塞应⽤程序等待未完成的异步操作,因此可在启动第⼀个异步操作后调⽤这个函数.
6. 调⽤io_rvice::run()后,io_rvice返回⼀个操作结果,并将其翻译为error_code,传递到事件处理器中.
这是Boost.Asio的简单图形.更多特性可从⽂档中获取,如使⽤Boost.Asio执⾏其他类型的异步操作.
Proactor设计模式:⽆线程并发
Boost.Asio库同时⽀持同步和异步操作.异步⽀持基于Proactor设计模式.下⾯讨论这种⽅式与同步操作及Reactor⽅式相⽐的优缺点. Proactor和Boost.Asio
现在讨论Proactor不依赖于平台细节的特性.
Proactor 设计模式
— 异步操作:定义⼀个异步执⾏操作,如Socket异步读写.
— 异步操作处理器:执⾏异步操作,并在操作完成后执⾏完成事件队列中的队列事件.从更⾼层次上说,服务端的stream_socket_rvice 就是⼀个异步操作处理器.
— 完成事件队列:存放完成事件,这些事件会被异步事件信号分离器移出队列.
好用的润唇膏— 完成句柄:异步操作完成后执⾏的函数.这是⼀个函数对象,通常使⽤boost::bind创建.
— 异步事件信号分离器:在完成事件队列中阻塞等待事件,受信后向调⽤者返回完成事件.
— Proactor :调⽤异步事件信号分离器将相应的处理事件移出队列,并为这个事件分配⼀个完成句柄(如调⽤函数对象).这个功能封装在
io_rvice类中.
— 初始化器: 执⾏特定程序代码启动异步操作.初始化器通过如basic_stream_socket等⾼层次接⼝与异步操作处理器交互,并返回
stream_socket_rvice等类型的服务端代理.
使⽤Reactor的实现
在很多平台上Boost.Asio按照Reactor机制实现Proactor设计模式,如lect,epoll或kqueue等.相对于Proactor,其实现⽅式如下:
— 异步操作处理器:Reactor使⽤lect,epoll或kqueue机制实现.如果reactor指⽰运⾏操作的资源已经就位,处理器执⾏异步操作并在完成事件队列中插⼊相应的完成句柄.
— 完成事件队列:存放完成句柄的链表(如函数对象).
— 异步事件信号分离器:在事件或条件变量上等待,直到完成事件队列中的完成句柄可⽤.
实现Windows的重叠IO
在Windows NT,2000,和XP系统中, Boost.Asio使⽤重叠IO⾼效实现了Proactor设计模式:
— 异步操作处理器:由操作系统实现.调⽤如AcceptEx等重叠函数进⾏初始化.
— 完成事件队列:由操作系统实现,与IO的完成端⼝相关联.每个io_rvice实例对应⼀个IO完成端⼝.
— 异步事件信号分离器:由Boost.Asio从队列中移除事件及其相关联的完成句柄.
优点
— 可移植性Portability:很多操作系统都提供原⽣的异步操作IO API(如Windows中的重叠IO),是开发者开发⾼性能⽹络应⽤程序的最佳选择.ASIO库尽可能使⽤原⽣的异步IO.如果原⽣⽀持不可⽤,ASIO库可使⽤同步事件信号分离器实现典型的Reactor模式,如POSIX的lect().
— 去除多线程并发的耦合
应⽤程序使⽤异步⽅式调⽤长时间运⾏的操作.但应⽤程序不必为增加的并发⽽显式的创建⼤量线程.
每个连接⼀个线程的实现策略(仅需要同步的⽅式)--太多的线程会产⽣⼤量的CPU上下⽂切换,同步和数据移动,导致系统效率降低.异步操作创建最⼩数量的操作系统线程,避免线程上下⽂切换的代价--CPU是典型的有限资源,这时仅由激活的逻辑线程进⾏事件处理.
— 简单的应⽤程序同步.
异步操作完成句柄可以在已存在的单线程环境中进⾏设置,此时开发出来的应⽤程序逻辑清晰,不必涉及同步问题.
— 功能组合.
功能组合是指实现⾼层次的操作功能,如按特定格式发送消息.每个功能的实现都是依赖于多次调⽤底层的读或写操作.
例如, ⼀个包含固定长度包头和变长包体的协议,包体长度在包头中指定.假设read_message操作由两个底层的读来实现,第⼀次接收包头,得到长度,第⼆次接收包体.
如果⽤异步⽅式来组合这些功能,可将这些异步调⽤连接到⼀起.前⼀个操作的完成句柄初始化下⼀个操作.使⽤时只需要调⽤封装链中的第⼀个操作,调⽤者不会意识到这个⾼层次的操作是按异步操作链的⽅式实现的.
按此⽅式可以很简单的向⽹路库中添加新的操作,开发出更⾼层次的抽象,如实现特定协议的功能和⽀持.
缺点
— 抽象复杂.
由于操作初始化和完成在时间和空间上是分离的,增加了使⽤异步机制开发程序的难度.⽽且控制流颠倒使应⽤程序很难调试.
— 内存占⽤.
香菊胶囊说明书在读和写过程中必须独占缓冲区空间,⽽后缓冲区⼜变为不确定的,每个并发操作都需要⼀个独⽴的缓冲区.换句话说,在Reactor模式中,直到socket准备读或写时才需要缓冲区空间.
线程和Boost.Asio
剑客古诗
线程安全
通常在并发操作中使⽤相互独⽴的局部对象是安全的,但并发操作中使⽤同⼀个全局对象就不安全了.然⽽如io_rvice 等类型可以保证并发操作中使⽤同⼀个对象也是安全的.
线程池
在多个线程中调⽤io_rvice::run()函数,就可以创建⼀个包含这些线程的线程池,其中的线程在异步
操作完成后调⽤完成句柄.也可通过调⽤io_rvice::post()实现横跨整个线程池的任意计算任务来达到相同的⽬的.
一级棒客源
注意所有加⼊io_rvice池的线程都是平等的,io_rvice可以按任意的⽅式向其分配⼯作.
内部线程
这个库的特定平台实现会创建⼏个内部线程来模拟异步操作.同时,这些线程对库⽤户是不可见的.这些线程特性:
不会直接调⽤⽤户的代码
阻塞所有信号
使⽤如下担保来实现:
异步完成句柄只会由调⽤io_rvice::run()的线程来调⽤.
因此,由库的使⽤者来建⽴和管理投递通知的线程.
原因:
在单线程中调⽤io_rvice::run(),⽤户代码避免了复杂的线程同步控制.例如,ASIO库⽤户可以使⽤单线程实现伸缩性良好的服务端(特定⽤户观点).
线程启动后在其他应⽤程序代码执⾏前,ASIO库⽤户需要在线程中执⾏⼀些简短的初始化操作.例如在线程中调⽤微软的COM操作之前必须调⽤CoInitializeEx.
将ASIO库接⼝与线程的创建和管理进⾏了解耦,确保可在不⽀持线程的平台中进⾏调⽤.
Strands:⽆显式锁定的线程
Strand被定义为严格按顺序调⽤(如⽆并发调⽤)事件句柄的机制.使⽤Strand可以在多线程程序中同步执⾏代码⽽⽆需显式地加锁(如互斥量等).
Strand可以按如下两种⽅式来隐式或显式的应⽤:
· 仅在⼀个线程中调⽤io_rvice::run()意味着所有的事件句柄都执⾏在⼀个隐式的Strand下,因为io_rvice保证所有的句柄都在run()中被调⽤.
· 链接中只有⼀个相关的异步操作链(如半双⼯的HTTP),句柄是不可能并发执⾏的.也是隐式Strand的情况.
· 显式Strand需要创建⼀个io_rvice::strand实例.所有的事件句柄函数对象都需要使⽤io_rvice::strand::wrap()进⾏包装,或使⽤
io_rvice::strand进⾏投递或分发.
在组合的异步操作中,如async_read() 或 async_read_until(),如果完成句柄使⽤⼀个Strand管理,则其他所有中间句柄都要由同⼀个Strand 来管理.这可以确保线程安全的访问所有在调⽤者和组合的操作中共享的对象(例如socket中的async_read(),调⽤者可以调⽤clo()来取消操作).这是通过在所有中间句柄中添加⼀个钩⼦函数实现的,在执⾏最终句柄前调⽤⾃定义的钩⼦函数:
struct my_handler
{
void operator()() { ... }
};
template<class F>
void asio_handler_invoke(F f, my_handler*)
{
// Do custom invocation here.
// Default implementation calls f();
}祭母
io_rvice::strand::wrap()函数⽣成⼀个新的定义了asio_handler_invoke的完成句柄,以便于Strand管理函数对象的运⾏.
缓冲区
通常IO在连续的内存区域(缓冲区)上传输数据.这个缓冲区可以简单的认为是包含了⼀个指针地址和⼀些字节的东西.然⽽,为了⾼效的开发⽹络应⽤程序, Boost.Asio⽀持分散聚合操作,需要⼀个或多个缓冲区:
⼀个分散读(scatter-read)接收数据并存⼊多缓冲区.
.⼀个聚合写(gather-write)传输多缓冲区中的数据.
因此需要⼀个抽象概念代表缓冲区集合.为此Boost.Asio定义了⼀个类(实际上是两个类)来代表单个缓冲区.可存储在向分散集合操作传递的容器中.
此外可将缓冲区看做是⼀个具有地址和⼤⼩的字节数组, Boost.Asio区别对待可修改内存(叫做mutable)和不可修改内存(后者在带有const 声明的存储区域上创建).这两种类型可以定义为:
活动标语typedef std::pair<void*, std::size_t> mutable_buffer;
typedef std::pair<const void*, std::size_t> const_buffer;
这⾥mutable_buffer 可以转换为const_buffer ,但反之不成⽴.
然⽽,Boost.Asio没有使⽤上述的定义,⽽是定义了两个类: mutable_buffer 和 const_buffer.⽬的是提供了不透明的连续内存概念:
类型转换上同std::pair.即mutable_buffer可以转换为const_buffer ,但反之不成⽴.
可防⽌缓冲区溢出.对于⼀个缓冲区实例,⽤户只能创建另外⼀个缓冲区来代表同样的内存序列或⼦序列.为了更加安全,ASIOI库也提供了从数组(如boost::array 或 std::vector,或std::string)中⾃动计算缓冲区⼤⼩的机制.
必须明确的调⽤buffer_cast函数进⾏类型转换.应⽤程序中通常不必如此,但在ASIO库的实现中将原始内存数据传递给底层的操作系统函数时必须如此.
养殖合作最后将多个缓冲区存⼊⼀个容器中就可以传递给分散聚合操作了.(如或).为了使⽤std::vector, std::list, std::vector 或 boost::array等容器⽽定义了MutableBufferSequence和ConstBufferSequence概念.
Streambuf与IoStream整合
类boost::asio::basic_streambuf从std::basic_streambuf继承,将输⼊输出流与⼀个或多个字符数组类型的对象相关联,其中的每个元素可以存储任意值.这些字符数组对象是内部的streambuf对象,但通过直接存取数组中的元素使其可⽤于IO操作,如在socket中发送或接收:
streambuf 的输⼊序列可以通过成员函数获取.函数的返回值满⾜ConstBufferSequence的要求.
streambuf 的输出序列可以通过成员函数获取.函数的返回值满⾜MutableBufferSequence的要求.
调⽤成员函数将数据从前端的输出序列传递到后端的输⼊序列.
调⽤成员函数从输⼊序列中移除数据.
streambuf 构造函数接收⼀个size_t的参数指定输⼊序列和输出序列⼤⼩的总和.对于任何操作,如果成功,增加内部数据量,超过这个⼤⼩限制会抛出std::length_error异常.
遍历缓冲区序列的字节
buffers_iterator<>类模板可⽤于像遍历连续字节序列⼀样遍历缓冲区序列(如MutableBufferSequence 或 ConstBufferSequence).并提供了buffers_begin() 和 buffers_end()帮助函数, 会⾃动推断buffers_iterator<>的模板参数.
例如,从socket中读取⼀⾏数据,存⼊std::string中: