Christopher Kohlhoff
Copyright © 2003-2012 Christopher M. Kohlhoff
以Boost1.0的软件授权进行发布(见附带的LICENSE_1_0.txt文件或从http://www.boost.org/LICENSE_1_0.txt)
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同样使用这种已存在的技术进行构建.
· 易于使用Ease of use.库提供了工具箱而不是框架来降低新手入门门槛.使用最少时间投入来学习基本的规则和方针.而后,库的使用者只需要理解用到的特定函数即可.
· 基于更多的抽象.Basis for further abstraction.asio库提供了高层次的抽象允许其他开发者对库进行扩展.例如,实现常用的HTTP协议.
虽然Boost.Asio一开始定位于网络通信,但其异步IO的概念已经扩展到了如串口通信,文件描述符等其他操作系统的IO操作方面.
核心概念和功能 解析Boost.AsioBoost.Asio 可用于如socket等IO对象的同步或异步操作.在使用Boost.Asio前首先了解一下Boost.Asio概念图, 以及与应用程序的相互集成方式.
第一个范例,看看处理socket连接的情况.首先从同步操作开始.
应用程序必须有一个io_service对象. io_service对象负责连接应用程序与操作系统的IO服务.
boost::asio::io_service io_service;
要执行IO操作应用程序需要一个像TCP Socket的IO对象:
boost::asio::ip::tcp::socket socket(io_service);
而后执行同步连接操作,发送如下事件:
1. 应用程序调用IO对象的初始化连接操作:
socket.connect(server_endpoint);
2. IO对象向io_service 提出请求.
3. io_service 调用操作系统的功能执行连接操作.
4. 操作系统向io_service 返回执行结果.
5. io_service将错误的操作结果翻译为boost::system::error_code类型. error_code可与特定值进行比较,或作为boolean值检测(false表示无错误).结果再传递给IO对象.
6. 如果操作失败,IO对象抛出boost::system::system_error类型的异常.开始操作的代码如下所示:
boost::system::error_code ec;
socket.connect(server_endpoint, ec);
而后error_code类型的变量ec被赋予操作的结果值,但不会抛出异常.
对于异步操作,事件顺序有所不同.
1. 应用程序调用IO对象进行连接操作:
socket.async_connect(server_endpoint, your_completion_handler);
your_completion_handler函数的签名为:
void your_completion_handler(const boost::system::error_code& ec);
执行的异步操作需要严格的函数签名.每种操作的合法形式可见参考文档.
2. IO对象请求io_service的服务.
3. io_service 通知操作系统其需要开始一个异步连接.
时序过程:(在同步情况下等待包括连接操作时间.)
4. 操作系统指示连接操作完成, io_service从队列中获取操作结果.
5. 应用程序必须调用io_service::run()(或io_service相似的成员函数)以便于接收结果.调用io_service::run()会阻塞应用程序等待未完成的异步操作,因此可在启动第一个异步操作后调用这个函数.
6. 调用io_service::run()后,io_service返回一个操作结果,并将其翻译为error_code,传递到事件处理器中.
这是Boost.Asio的简单图形.更多特性可从文档中获取,如使用Boost.Asio执行其他类型的异步操作.
Proactor设计模式:无线程并发Boost.Asio库同时支持同步和异步操作.异步支持基于Proactor设计模式.下面讨论这种方式与同步操作及Reactor方式相比的优缺点.
Proactor和Boost.Asio现在讨论Proactor不依赖于平台细节的特性.
— 异步操作:定义一个异步执行操作,如Socket异步读写.
— 异步操作处理器:执行异步操作,并在操作完成后执行完成事件队列中的队列事件.从更高层次上说,服务端的stream_socket_service 就是一个异步操作处理器.
— 完成事件队列:存放完成事件,这些事件会被异步事件信号分离器移出队列.
— 完成句柄:异步操作完成后执行的函数.这是一个函数对象,通常使用boost::bind创建.
— 异步事件信号分离器:在完成事件队列中阻塞等待事件,受信后向调用者返回完成事件.
— Proactor :调用异步事件信号分离器将相应的处理事件移出队列,并为这个事件分配一个完成句柄(如调用函数对象).这个功能封装在io_service类中.
— 初始化器: 执行特定程序代码启动异步操作.初始化器通过如basic_stream_socket等高层次接口与异步操作处理器交互,并返回stream_socket_service等类型的服务端代理.
使用Reactor的实现在很多平台上Boost.Asio按照Reactor机制实现Proactor设计模式,如select,epoll或kqueue等.相对于Proactor,其实现方式如下:
— 异步操作处理器:Reactor使用select,epoll或kqueue机制实现.如果reactor指示运行操作的资源已经就位,处理器执行异步操作并在完成事件队列中插入相应的完成句柄.
— 完成事件队列:存放完成句柄的链表(如函数对象).
— 异步事件信号分离器:在事件或条件变量上等待,直到完成事件队列中的完成句柄可用.
实现Windows的重叠IO在Windows NT,2000,和XP系统中, Boost.Asio使用重叠IO高效实现了Proactor设计模式:
— 异步操作处理器:由操作系统实现.调用如AcceptEx等重叠函数进行初始化.
— 完成事件队列:由操作系统实现,与IO的完成端口相关联.每个io_service实例对应一个IO完成端口.
— 异步事件信号分离器:由Boost.Asio从队列中移除事件及其相关联的完成句柄.
优点— 可移植性Portability:很多操作系统都提供原生的异步操作IO API(如Windows中的重叠IO),是开发者开发高性能网络应用程序的最佳选择.ASIO库尽可能使用原生的异步IO.如果原生支持不可用,ASIO库可使用同步事件信号分离器实现典型的Reactor模式,如POSIX的select().
— 去除多线程并发的耦合
应用程序使用异步方式调用长时间运行的操作.但应用程序不必为增加的并发而显式的创建大量线程.
每个连接一个线程的实现策略(仅需要同步的方式)--太多的线程会产生大量的CPU上下文切换,同步和数据移动,导致系统效率降低.异步操作创建最小数量的操作系统线程,避免线程上下文切换的代价--CPU是典型的有限资源,这时仅由激活的逻辑线程进行事件处理.
— 简单的应用程序同步.
异步操作完成句柄可以在已存在的单线程环境中进行设置,此时开发出来的应用程序逻辑清晰,不必涉及同步问题.
— 功能组合.
功能组合是指实现高层次的操作功能,如按特定格式发送消息.每个功能的实现都是依赖于多次调用底层的读或写操作.
例如, 一个包含固定长度包头和变长包体的协议,包体长度在包头中指定.假设read_message操作由两个底层的读来实现,第一次接收包头,得到长度,第二次接收包体.
如果用异步方式来组合这些功能,可将这些异步调用连接到一起.前一个操作的完成句柄初始化下一个操作.使用时只需要调用封装链中的第一个操作,调用者不会意识到这个高层次的操作是按异步操作链的方式实现的.
按此方式可以很简单的向网路库中添加新的操作,开发出更高层次的抽象,如实现特定协议的功能和支持.
缺点— 抽象复杂.
由于操作初始化和完成在时间和空间上是分离的,增加了使用异步机制开发程序的难度.而且控制流颠倒使应用程序很难调试.
— 内存占用.
在读和写过程中必须独占缓冲区空间,而后缓冲区又变为不确定的,每个并发操作都需要一个独立的缓冲区.换句话说,在Reactor模式中,直到socket准备读或写时才需要缓冲区空间.
线程和Boost.Asio 线程安全通常在并发操作中使用相互独立的局部对象是安全的,但并发操作中使用同一个全局对象就不安全了.然而如io_service 等类型可以保证并发操作中使用同一个对象也是安全的.
线程池在多个线程中调用io_service::run()函数,就可以创建一个包含这些线程的线程池,其中的线程在异步操作完成后调用完成句柄.也可通过调用io_service::post()实现横跨整个线程池的任意计算任务来达到相同的目的.
注意所有加入io_service池的线程都是平等的,io_service可以按任意的方式向其分配工作.
内部线程这个库的特定平台实现会创建几个内部线程来模拟异步操作.同时,这些线程对库用户是不可见的.这些线程特性:
- 不会直接调用用户的代码
- 阻塞所有信号
使用如下担保来实现:
- 异步完成句柄只会由调用io_service::run()的线程来调用.
因此,由库的使用者来建立和管理投递通知的线程.
原因:
- 在单线程中调用io_service::run(),用户代码避免了复杂的线程同步控制.例如,ASIO库用户可以使用单线程实现伸缩性良好的服务端(特定用户观点).
- 线程启动后在其他应用程序代码执行前,ASIO库用户需要在线程中执行一些简短的初始化操作.例如在线程中调用微软的COM操作之前必须调用CoInitializeEx.
- 将ASIO库接口与线程的创建和管理进行了解耦,确保可在不支持线程的平台中进行调用.
Strand被定义为严格按顺序调用(如无并发调用)事件句柄的机制.使用Strand可以在多线程程序中同步执行代码而无需显式地加锁(如互斥量等).
Strand可以按如下两种方式来隐式或显式的应用:
· 仅在一个线程中调用io_service::run()意味着所有的事件句柄都执行在一个隐式的Strand下,因为io_service保证所有的句柄都在run()中被调用.
· 链接中只有一个相关的异步操作链(如半双工的HTTP),句柄是不可能并发执行的.也是隐式Strand的情况.
· 显式Strand需要创建一个io_service::strand实例.所有的事件句柄函数对象都需要使用io_service::strand::wrap()进行包装,或使用io_service::strand进行投递或分发.
在组合的异步操作中,如async_read() 或 async_read_until(),如果完成句柄使用一个Strand管理,则其他所有中间句柄都要由同一个Strand来管理.这可以确保线程安全的访问所有在调用者和组合的操作中共享的对象(例如socket中的async_read(),调用者可以调用close()来取消操作).这是通过在所有中间句柄中添加一个钩子函数实现的,在执行最终句柄前调用自定义的钩子函数:
struct my_handler
{
void operator()() { ... }
};
template
void asio_handler_invoke(F f, my_handler*)
{
// Do custom invocation here.
// Default implementation calls f();
}
io_service::strand::wrap()函数生成一个新的定义了asio_handler_invoke的完成句柄,以便于Strand管理函数对象的运行.
缓冲区通常IO在连续的内存区域(缓冲区)上传输数据.这个缓冲区可以简单的认为是包含了一个指针地址和一些字节的东西.然而,为了高效的开发网络应用程序, Boost.Asio支持分散聚合操作,需要一个或多个缓冲区:
- 一个分散读(scatter-read)接收数据并存入多缓冲区.
- .一个聚合写(gather-write)传输多缓冲区中的数据.
因此需要一个抽象概念代表缓冲区集合.为此Boost.Asio定义了一个类(实际上是两个类)来代表单个缓冲区.可存储在向分散集合操作传递的容器中.
此外可将缓冲区看做是一个具有地址和大小的字节数组, Boost.Asio区别对待可修改内存(叫做mutable)和不可修改内存(后者在带有const声明的存储区域上创建).这两种类型可以定义为:
typedef std::pair mutable_buffer;
typedef std::pair 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库的实现中将原始内存数据传递给底层的操作系统函数时必须如此.
最后将多个缓冲区存入一个容器中就可以传递给分散聚合操作了.(如read()或write()).为了使用std::vector, std::list, std::vector 或 boost::array等容器而定义了MutableBufferSequence和ConstBufferSequence概念.
Streambuf与IoStream整合类boost::asio::basic_streambuf从std::basic_streambuf继承,将输入输出流与一个或多个字符数组类型的对象相关联,其中的每个元素可以存储任意值.这些字符数组对象是内部的streambuf对象,但通过直接存取数组中的元素使其可用于IO操作,如在socket中发送或接收:
- streambuf 的输入序列可以通过data()成员函数获取.函数的返回值满足ConstBufferSequence的要求.
- streambuf 的输出序列可以通过prepare()成员函数获取.函数的返回值满足MutableBufferSequence的要求.
- 调用commit()成员函数将数据从前端的输出序列传递到后端的输入序列.
- 调用consume()成员函数从输入序列中移除数据.
streambuf 构造函数接收一个size_t的参数指定输入序列和输出序列大小的总和.对于任何操作,如果成功,增加内部数据量,超过这个大小限制会抛出std::length_error异常.
遍历缓冲区序列的字节buffers_iterator类模板可用于像遍历连续字节序列一样遍历缓冲区序列(如MutableBufferSequence 或 ConstBufferSequence).并提供了buffers_begin() 和 buffers_end()帮助函数, 会自动推断buffers_iterator的模板参数.
例如,从socket中读取一行数据,存入std::string中:
boost::asio::streambuf sb;
...
std::size_t n = boost::asio::read_until(sock, sb, '\n');
boost::asio::streambuf::const_buffers_type bufs = sb.data();
std::string line(
boost::asio::buffers_begin(bufs),
boost::asio::buffers_begin(bufs) + n);缓冲区调试
有些标准库实现,如VC++8.0或其后版本,提供了叫做迭代器调试的特性.这意味着运行期会验证迭代器是否合法.如果应用程序试图使用非法的迭代器,会抛出异常.例如:
std::vector v(1)
std::vector::iterator i = v.begin();
v.clear(); // invalidates iterators
*i = 0; // assertion!
Boost.Asio 利用这个特性实现缓冲区调试.对于下面的代码:
void dont_do_this()
{
std::string msg = "Hello, world!";
boost::asio::async_write(sock, boost::asio::buffer(msg), my_handler);
}
当调用异步读或写时需要确保此操作的缓冲区在调用完成句柄时可用.上例中,缓冲区是std::string变量msg.变量在栈中,异步完成前已经过期了.如果幸运的话程序崩溃,但更可能会出现随机错误.
当缓冲区调试启动后,Boost.Asio会存储一个字符串的迭代器,一直保存到异步操作完成,然后解引用来检查有效性.上例中在Boost.Asio调用完成句柄前就会看到一个断言错误.
当定义_GLIBCXX_DEBUG 选项时,会自动在VS8.0及其以后版本和GCC中启动这个特性.检查需要性能代价,因此缓冲区调试只在debug生成时启用.其他编译器可通过定义BOOST_ASIO_ENABLE_BUFFER_DEBUGGING选项来启动.也可显式的通过BOOST_ASIO_DISABLE_BUFFER_DEBUGGING选项停止.
流,短读短写很多Boost.Asio中的IO对象都是基于流的.意味着:
- 无消息边界.被传输的数据就是一个连续的字节序列.
- 读写操作可能会传递少量不需要的字节.这被称为短读或短写.
提供基于流IO操作模型的对象需要模拟如下几个操作:
- SyncReadStream, 调用成员函数read_some()执行同步读操作.
- AsyncReadStream, 调用成员函数async_read_some()执行异步读操作.
- SyncWriteStream, 调用成员函数write_some()执行同步写操作.
- AsyncWriteStream, 调用成员函数async_write_some()执行异步写操作.
基于流的IO对象包括ip::tcp::socket, ssl::stream, posix::stream_descriptor, windows::stream_handle等等.
通常程序需要传递指定数量的字节数据.启动操作后就会发生短读或短写,直到所有数据传输完毕.Boost.Asio提供了通用函数来自动完成这些操作: read(), async_read(), write() 和 async_write().
为什么EOF是一个错误- 流终止会导致read, async_read, read_until or async_read_until 函数违反约定.如要读取N个字节,但由于遇到EOF而提前结束.
- EOF错误可以区分流终止和成功读取0个字节.
Reactor风格操作
有时应用程序必须整合第三方的库来实现IO操作.为此,Boost.Asio提供一个可用于读和写操作的null_buffers类型.null_buffers直到IO对象准备好执行操作后才会返回.
例如,如下代码执行无阻塞读操作:
ip::tcp::socket socket(my_io_service);
...
socket.non_blocking(true);
...
socket.async_read_some(null_buffers(), read_handler);
...
void read_handler(boost::system::error_code ec)
{
if (!ec)
{
std::vector buf(socket.available());
socket.read_some(buffer(buf));
}
}
Socket在所有平台上都支持这种操作,是POSIX基于流的描述符合类.
基于行的传输操作很多常用的网络协议都是基于行的,即这些协议元素被字符序列"\r\n"限定.例如HTTP,SMTP和FTP.为了便于实现基于行的协议,以及其他使用分隔符的协议,Boost.Asio包括了read_until() 和 async_read_until()函数.
如下代码展示在HTTP服务端使用async_read_until()接收客户端的第一行HTTP请求:
class http_connection
{
...
void start()
{
boost::asio::async_read_until(socket_, data_, "\r\n",
boost::bind(&http_connection::handle_request_line, this, _1));
}
void handle_request_line(boost::system::error_code ec)
{
if (!ec)
{
std::string method, uri, version;
char sp1, sp2, cr, lf;
std::istream is(&data_);
is.unsetf(std::ios_base::skipws);
is >> method >> sp1 >> uri >> sp2 >> version >> cr >> lf;
...
}
}
...
boost::asio::ip::tcp::socket socket_;
boost::asio::streambuf data_;
};
Streambuf数据成员用于存储在查找到分隔符前接收的数据.记录分隔符以后的数据也是很重要的.这些保留在streambuf中的冗余数据会被保留下来用于随后调用的read_until()或async_read_until()函数进行检查.
分隔符可以是单个字符,一个std::string或一个boost::regex. read_until() 和 async_read_until()函数也可接收一个用户定义函数作为参数,获取匹配条件.例如,在streambuf中读取数据,遇到一个空白字符后停止.:
typedef boost::asio::buffers_iteratoriterator;
std::pair
match_whitespace(iterator begin, iterator end)
{
iterator i = begin;
while (i != end)
if (std::isspace(*i++))
return std::make_pair(i, true);
return std::make_pair(i, false);
}
...
boost::asio::streambuf b;
boost::asio::read_until(s, b, match_whitespace);
从streambuf中读取数据,直到遇到匹配的字符:
class match_char
{
public:
explicit match_char(char c) : c_(c) {}
template
std::pair operator()(
Iterator begin, Iterator end) const
{
Iterator i = begin;
while (i != end)
if (c_ == *i++)
return std::make_pair(i, true);
return std::make_pair(i, false);
}
private:
char c_;
};
namespace boost { namespace asio {
template struct is_match_condition
: public boost::true_type {};
} } // namespace boost::asio
...
boost::asio::streambuf b;
boost::asio::read_until(s, b, match_char('a'));
is_match_condition类型会自动的对函数,及带有嵌套result_type类型定义的函数对象的返回值值进行评估是否为true.如上例所示,其他类型的特性必须显式指定.
自定义内存分配很多异步操作需要分配对象,用来存储与操作有关的状态数据.例如,Win32的实现中需要将重叠子对象传递给Win32 API函数.
幸好程序包含一个易于识别的异步操作链.半双工协议(如HTTP服务)为每个客户端创建一个单操作链.全双工协议实现有两个并行的执行链.程序运用这个规则可以在链上的所有异步操作中重用内存.
假设复制一个用户定义的句柄对象h,如果句柄的实现需要分配内存,应该有如下代码:
void* pointer = asio_handler_allocate(size, &h);
同样要释放内存:
asio_handler_deallocate(pointer, size, &h);
这个函数实现了参数依赖的定位查找.asio空间中有这个函数的默认实现:
void* asio_handler_allocate(size_t, ...);
void asio_handler_deallocate(void*, size_t, ...);
实现了::operator
new() 和 ::operator
delete()功能.
函数实现保证了相关句柄调用前会发生内存重新分配,这样就可以实现同一个句柄上的异步操作重用内存.
在任意调用库函数的用户线程中都可调用自定义内存分配函数.实现保证库中的这些异步操作不会并发的对句柄进行内存分配函数调用.实现要加入适当的内存间隔,保证不同线程中调用的内存分配函数有正确的内存可见性.
句柄跟踪为调试异步程序,Boost.Asio提供了句柄跟踪支持.当激活BOOST_ASIO_ENABLE_HANDLER_TRACKING定义,Boost.Asio向标准错误流中写入调试输出信息.输出信息记录异步操作及其句柄间的关系.
这个特性对调试很有帮助,需要知道异步操作是如何被链接在一起的,或什么异步操作被挂起了.如下是HTTP服务输出的调试信息,处理单个请求,而后按Ctrl+C退出:
@asio|1298160085.070638|0*1|signal_set@0x7fff50528f40.async_wait
@asio|1298160085.070888|0*2|socket@0x7fff50528f60.async_accept
@asio|1298160085.070913|0|resolver@0x7fff50528e28.cancel
@asio|1298160118.075438|>2|ec=asio.system:0
@asio|1298160118.075472|2*3|socket@0xb39048.async_receive
@asio|1298160118.075507|2*4|socket@0x7fff50528f60.async_accept
@asio|1298160118.075527|3|ec=asio.system:0,bytes_transferred=122
@asio|1298160118.075731|3*5|socket@0xb39048.async_send
@asio|1298160118.075778|5|ec=asio.system:0,bytes_transferred=156
@asio|1298160118.075831|5|socket@0xb39048.close
@asio|1298160118.075855|1|ec=asio.system:0,signal_number=2
@asio|1298160122.827333|1|socket@0x7fff50528f60.close
@asio|1298160122.827359|4|ec=asio.system:125
@asio|1298160122.827378|n程序进入了第n个句柄.描述句柄参数.
{ 0 }};
关注打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?