Boost.Asio网络开发基础知识(一):读读文档

  虽然C++可以用Libevent等纯C实现框架进行异步操作,但是总感觉Boost.Asio才算是相对正统纯正的C++网络开发,而且据说已经起草的C++ Network Library就是基于Boost.Asio的,这不禁让我这个初入Boost的菜鸟暗中激动了一把。
  在我所了解的Libevent和Boost.Asio当中,最大的差别是前者是Reactor模式,后者是Proactor模式了,不过Linux平台底层操作系统级别的select/poll/epoll只能适合Reactor模式的开发,所以Linux类平台下的Boost.Asio也是通过synchronous event demultiplexors的方式模拟实现的Proactor模式。除此之外,Libevent提供了一个Evbuffer的IO缓冲数据结构,而Boost.Asio同样也提供了boost::asio::buffer和boost::asio::streambuf两种缓冲类型,不过这也算是个壳吧,至少boost::asio:buffer构造的时候还是需要传输底层数组或者容器类实例作为实际数据的载体的。再则,Boost.Asio提供了自带线程池的功能,包括单个io_service运行在多个线程上和每个线程自己单独的io_service两种模式,这在网络库中并不多见吧。这个机制确实提高了多线程开发的效率,同时也免去了很多初学者自己制造垃圾线程池的烦恼。
  可惜的是,据我Google关于Boost.Asio网上的资料确实好少,其中找到的大多都是那种教你怎么使用的简单例子,很少有深入剖析的资料,作为实际工业实现标准级别C++网络库Boost.Asio不觉得免有些奇怪,希望C++网络库标准敲定之后,这类部分能得到更多的关注吧。不过Boost.Asio确实还蛮好用的,看看附加的例子基本就能模仿出个不错的服务端了,今天把Boost.Asio的文档过了一遍,在此做个记录。

1. 概述
  说到Boost.Asio,就不得不祭出这张Proactor设计模式的大图:
Boost.Asio Proactor
  关于图中的术语描述如下(其中当末尾明确指出了Reactor的,是在Windows和Linux类实现有所不同的地方,Linux类使用基于Reactor来模拟Proactor的模式):
  a. Initiator:应用程序相关的代码,主要是发起异步操作的请求,在启动异步操作的时候负责创建一个异步回调对象;
  b. Asynchronous Operation Processor:执行异步操纵,并且将执行结束后,将时间排列到时间完成队列上去。在Reactor模式下的实现是,当select/epoll等指示出等待的资源就绪可以进行操作的时候,processor会执行实际的异步操作,完成之后会将与其关联的completion handler添加完成事件队列上去;
  c. Asynchronous Operation:定义了异步执行的操作;
  d. Completion Event Queue:缓存了已经完成事件直到被asynchronous event demultiplexer取出队列。在Reactor模式下通常是链接的函数对象;
  e. Asynchronous Event Demultiplexer:阻塞形式地等待在完成事件队列上,直到有事件发生,然后返回一个完成事件给调用者。在Reactor模式下,是通过等待在某个事件或者某个条件变量上面,直到完成时间队列上completion handler就绪;
  f. Completion Handler:用以处理异步操作完成的结果,其是个函数对象(function objects),通常使用boost:bind()创建的;
  g. Proactor:调用asynchronous event demultiplexer去从完成队列中取出完成的事件,并且调用处理(dispatch)事件对应的completion handler。

2. Strand
  strand可以保证严格序列化地执行event handlers,使用strand可以安全的在多线程环境下执行程序,而不需要使用者显式的使用mutex等方式来同步。strand的可以以隐式或者显式的方式存在:
  a. 如果只在一个线程中调用io_service::run(),那么所有的event handlers都是在一个隐式strand中序列化执行的;
  b. 对于一个链接只有一个单链异步操作的时候,其不可能并发的执行event handlers,这也是隐式strand的。(文中提到了比如HTTP的单双工协议,我的理解是HTTP只会客户端请求、服务端返回数据这种形式,而不会服务端主动请求,没有并行的可能性,所以所有的请求应当必然是串行化的);
  c. 可以显式实例化一个strand对象,然后所有的event handlers必须使用io_service::strand::wrap()进行包裹,或者使用该strand对象显式post/dispatch;
  关于Strand,Boost.Asio的手册说的不是很清楚,在此明确一下表示:
  Boost.Asio对线程安全的保证描述是,concurrent使用不同的object是安全的,但是concurrent使用相同的object是不安全的,这就比如一个socket可以在一个线程中随便使用,或者在两个线程中不同时使用,但是绝对不允许在两个线程中重叠使用(即使通常意义上的socket也不要同时在多线程中使用)。Boost.Asio这里复杂了是因为async_write、async_read包括async_read_until这类函数被称为composed operation,他会在底层零次或者多次调用async_write_some这类函数来实现的,所以对这一类函数的调用,必须显式使用strand进行串行化其内部的操作,同时用户端程序还必须保证在执行这个操作的过程中不会有其他的async_write、async_write_some等函数的调用。这样HTTP Example3的strand也就不难解释了。

3. 缓冲类型
  Boost.Asio是支持聚合读写(scatter-gather operations)的,当需要的时候将各个buffer装入容器中传递给聚合操作。对于单独的buffer,
  a. boost::asio::buffer
  主要有mutable_buffer、const_buffer两种类型,从一般的概念上讲是如下的含义

1
2
typedef std::pair<void*, std::size_t> mutable_buffer;
typedef std::pair<const void*, std::size_t> const_buffer;

  但是实际Boost.Asio是定义了这两个类,主要基于以下考虑:(1)如果是上面类型,那么mutable可以转换成const的,但是反向的转换是不允许的;(2)可以保护防止缓冲区溢出,类可以通过传递的array、std::vector、std::string、std::arry、POD等各种类型自动推断出buffer的长度;(3)可以定义buffer_cast等成员函数做更丰富的操作需求,而不是底层数据的野蛮转换。
  b. boost::asio::streambuf
  派生自std::basic_streambuf类而关联了input和output两个序列,序列底层用一个或者多个字符数组存储数据的,当然这些数据是streambuf内部使用的,而streambuf提供一系列的接口来操作这些数据:
  (1) data()成员函数访问input,返回的类型满足ConstBufferSequence类型;
  (2) prepare()成员函数用于访问output,返回的类型满足MutableBufferSequence类型;
  (3) commit()成员函数用于将output头部的数据移动到input的尾部;
  (4) consume()成员函数用于移除input头部的数据;
  streambuf的构造函数可以传递一个size_t的参数来指明input和output总共的最大尺寸,当使用的总空间超过这个限制的时候,会抛出std::length_error的异常。
  此外,streambuf提供了迭代器的接口,可以连续访问内部存储的字节序列,其操作的模板是:

1
2
3
4
5
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);

4. 流数据操作
  Boost.Asio中的大多数操作都是基于流(stream)的,所以:对消息没有边界的概念,真正传输的数据都是一些列连续的字节序列;读和写操作实际传输的字节数目可能会比请求的数目要少。常用的同步和异步操作有:read_some()、async_read_some()、write_some()、async_write_some()。
  当开发过程中有需要传输指定长度的消息的时候,可以使用Boost.Asio的read()、async_read()、write()、async_write()来实现,他们会自动重复执行传输操作,直到请求的操作数完成。不嫌麻烦的话用户程序不断尝试直到完成也是可以的。
  当流请求操作结束后会涉及到EOF,成功读返回读取长度为0表示该流已经结束了。

5. Reactor模式风格的操作
  有时候有的程序可能自己进行I/O操作,Boost.Asio也支持这种机制。当在IO读写函数需要传递buffer对象的地方传递null_buffers对象既可,这时候null_buffers的操作会在底层的数据准备好(可以成功读写)之后返回。
  然后,客户程序就可以使用同步读写函数来进行IO操作了。

1
2
3
4
5
6
7
8
socket.async_read_some(null_buffers(), read_handler);
...
void read_handler(boost::system::error_code ec) {
if (!ec) {
std::vector<char> buf(socket.available());
socket.read_some(buffer(buf));
}
}

6. 基于行模式的操作
  很多网络引用层的协议(HTTP、SMTP、FTP)都是基于行(line-based)的,所以这些协议的元素都是用”\r\n”来分隔的,此时Boost.Asio的read_until和async_readuntil就可以方便的来处理基于行或者基于特定分隔符的应用层协议了,简单用法其支持的分隔符包括char、std::string、boost::regex类型的表达式。
  更高端的用法是(async
)read_until支持接收一个用户定义的函数或者函数对象,其函数或者函数对象具有以下的形式:

1
2
3
4
5
6
7
8
typedef boost::asio::buffers_iterator<
boost::asio::streambuf::const_buffers_type> iterator;

std::pair<iterator, bool>
match_whitespace(iterator begin, iterator end){
...
return std::make_pair(i, true);
}

  所以我在开发HTTP简单应用的时候,都是先用streambuf的async_read_until读取”\r\n\r\n”表示HTTP头部结束的位置,分析头部的Length字段得到数据体的具体大小,然后调用async_read添加transfer_exactly参数来进行精确传输的。这里使用需要格外注意的是,HTTP的头和体不是分开传输的,绝大多数async_read_until会读一部分的BODY,这时候需要手动将这部分的数据转移到async_read的缓冲区里面去。

7. 其它
  C++11提供了右值引用和移动语义,所以在Boost.Asio中可以使用这些语义来移动IO object和Handler。在移动IO Object的时候需要特别的注意,当其上面还有pending的异步操作时候,移动它是很危险的,常常诸如async_read()会保存这些对象的引用,意味着很可能后面会用到move-from的对象。一般在开始构造connection的时候可以利用移动语句,后面使用还需谨慎。

1
2
3
4
5
tcp::socket socket_;
connection(tcp::socket&& s) : socket_(std::move(s)) {}
...
std::make_shared<connection>(std::move(socket_))->go();
acceptor_.async_accept(socket_, ...);

8. 小结
  哈哈,基本没什么干货或者新东西,Boost.Asio的开发还是建议多看看官方提供的那些例子,另外我的airobot_msgd也囊括了大多数的操作,尽请指正。

参考