Buffers in Boost.Asio

本文最后更新于:December 1, 2021 am

介绍了Boost.Asio中的缓冲区

主要参考Buffers - 1.72.0 (boost.org)

Buffers

基本上,I/O操作往往包括从一片连续的内存空间中搬入和搬出数据,这片内存空间就叫做缓冲区Buffer。这些缓冲区可以被简单理解为一个二元组,(pointer, size),前一个指针存储缓冲区的首地址,而第二个size存储缓冲区的大小。

为了能够编写更高效地网络应用,Boost.Asio支持发散聚合操作scatter-gather operations。这种操作包括一至多个缓冲区:

  • scatter-read操作将数据读入多个缓冲区
  • gather-write将数据送入多个缓冲区

因此,我们需要一层抽象来表示一些缓冲区的集合。Boost.Asio中采用的方案是,定义一种类型来代表单个缓冲区(实际上有两种类型,下文会提到)。这些缓冲区们可以被存入容器,然后我们再将容器传给发散聚合操作scatter-gather operations

除了能够通过二元组的方式来声明一个缓冲区以外,Boost.Asio区分了可变内存mutable和不可变内存non-modifiable。因此,可以像如下的方式来定义这两种缓冲区。

1
2
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_bufferconst_buffer。这样做可以对外提供一个不透明内存连续读写功能,同时达到:

  • 在类型转换中,这个类表现得像std::pair一样。也就是mutable_buffer只能单向转换到const_buffer

  • 提供了缓冲区泛滥保护。给定一个buffer实例,在创建一个新的缓冲区时,用户所指定的内存空间至多和这个实例一致,或者是这个实例的内存空间的一个子集。

    为了进一步保证安全,库也有从一个数组(比如boost::array,std::vector,std::string)自动决定缓冲区大小的机制。

  • 外部通过data()来显示获取底层的数据。一般情况下,应用层面的代码不会需要做这一步,但是在库的实现上,需要用户代码将裸的内存传给内部的操作系统函数。

最后,通过将多个缓冲区加入容器的方式,很多种缓冲区都可以被传给发散聚合操作scatter-gather operation。定义MutableBufferSequenceConstBufferSequence这两种概念,它们适用于很多容器,比如std::vector,std::list,std::array

Streambuf for Integration with Iostreams

The class boost::asio::basic_streambuf is derived from std::basic_streambuf to associate the input sequence and output sequence with one or more objects of some character array type, whose elements store arbitrary values.

boost::asio::basic_streambufstd::basic_streambuf派生,目的是将输入和输出的序列input sequence and output sequence同一个或者多个某种字符数组类型的对象相关联(其中对象内部存储任意值)。

这些字符数组对象是streambuf对象内部的,但是也提供了对于这些数组对象的元素的直接访问接口,用以允许在I/O操作中使用它们,比如对一个socket执行send或者receive操作。

  • streambuf的输入序列通过data()成员函数访问。这个函数的返回值类型满足ConstBufferSequence的约束。
  • streambuf的输出序列通过成员函数prepare()访问。返回值类型满足MutableBufferSequence的约束。
  • 数据从output sequence的头部被转移到input sequence的尾部。通过调用commit()函数来实现。
  • 数据可以被从input sequence的头部移除。通过调用consume()函数实现。

streambuf的构造器接受一个size_t的变量来表示 输入序列和输出序列大小的和的最大值。任何会导致两者之和超出范围的操作都会抛出异常std::length_error

Bytewise Traversal of Buffer Sequences

可以通过buffers_iterator<>模板类来对缓冲区序列(比如满足MutableBufferSequence或者ConstBufferSequence约束的类)进行“连续性”访问。帮助函数buffer_begin()buffer_end()用于返回头尾的迭代器。

如果要从一个socket读一行内容到std::string中,可以这样写:

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

Buffer Debugging

某些标准库的实现,提供一种叫做iterator debugging的特性。这意味着迭代器的有效性在运行时可以得到检查。如果程序试图使用某个失效的迭代器,断言将会被触发,比如

1
2
3
4
std::vector<int> v(1)
std::vector<int>::iterator i = v.begin();
v.clear(); // invalidates iterators
*i = 0; // assertion!

Boost.Asio利用这一特性,实现了缓冲区Debug。考虑下面一段代码:

1
2
3
4
void dont_do_this{
std::string msg="Hello,world";
boost::asio::async_write(sock,boost::asio::buffer(msg),my_handler);
}

当调用异步读写操作之前,我们必须先确保用于操作的缓冲区在直到换成回调completion handler被调用时都是有效的。在这段代码里面,这个缓冲区是msg,它被分配在栈上,当回调被调用时,已经超出了这个变量的作用域。如果你运气好,应用会直接崩掉,但是更可能出现的是随机错误。

当启用buffer debugging时,Boost.Asio将会存储一个指向string内部的迭代器,并且在异步操作完成时试图解引用来检验它的有效性。在上面这个例子中,在调用完成回调之前就会观察到断言错误。

这个特性在Microsoft Visual Studio 8.0及以后的版本是自动生效的,或者对于gcc,当_VLIBCXX_DEBUG被定义时。这样的检查会有性能代价,因此只在debug build中使用这一特性。对于其它编译器,可以通过定义 BOOST_ENABLE_BUFFER_DEBUGGING来使用,或者通过BOOST_ASIO_DISABLE_DEBUGGING来关闭。