Asio 讲座


#1

ASIO 讲座

在讲asio之前,我必须给大家讲讲工具库和框架之间的区别,asio是被设计成一套工具库而不是框架。

###那么,什么是框架?

框架就是一套固定了编程结构的库,任何用户使用它,必须按照框架库的结构设计自己的应用,比如MFC中的OnOK, OnXXX之类,又或者ACE中的ACE_Handler::handle_xxx_yyy之类,用户通过在这些派生类的虚函数中实现自己需要的处理。

###什么是工具库?

而工具库就是一套api,而不是框架,用户使用它,无需从指定的地方去填写代码。程序的框架由用户自己决定。类似的库有C++中的STL,C中的标准库,它们从来不提供编程框架,而只提供工具函数,让用户通过这些工具函数,正确的组织自己的程序结构。

###工具库和框架之间的区别是什么? 上面的简单介绍,区别是很明确的,工具库使用更为灵活简单,容易学习,而框架的学习周期一般较长。

###为什么我不建议使用框架,而是推荐使用工具库?

到这里,已经很清楚了,框架的限制非常多,不够灵活, 由其是在设计程序的过程中,总是需要考虑如何兼容框架, 像ACE、MFC这种库,甚至连main函数都被框架给封装了,这样的情况下,程序流程变得难以捉摸。 有时候就得靠猜。

工具库和框架,不是今天的主题,继续讲述ASIO。


先讲讲asio的特性

我总结了ASIO主要有以下特性:

  1. 移植性,跨操作系统平台,接口兼容性良好
  2. 扩展性,比如用户可以使用asio.io_service自定义处理其它的业务。
  3. 高效,在API层,总是使用对应操作系统上高效API实现对应IO功能,实现上使用由其使用模板技术,编译期可以得到足够的优化。
  4. 易用,APIs的命名规范,没有胡乱瞎搞的概念。
  • 第一点, 不用说, 通吃很多平台
  • 第二点,生产消费队列,都可以扩展在io_service上,利用post、dispath即可实现。
  • 第三点,在windows上的实现默认是iocp,linux上epoll, 这两种都是咱经常听说的高效的网络模型。
  • 第四点,asio依然使用bsd socket api的概念,这是大家都最熟悉的概念,所以,学习起来非常容易。

通过这几点,咱可以对asio有一个小小的了解。


ASIO的基本用法很灵活

比如socket可以异步和同步同时使用,在同步的情况下,asio库的具体调用通过下面这样的方式进行的:

your program --> io_object --> io_service --> operating system

这里的io_object可以是socket,也可以是accepter,也可以是定时器。 io_service抽象了操作系统相关的细节,你不再需要了解各操作系统的API的细节,就可以编写其它平台的socket程序。

对应的代码是

boost::asio::io_service io_service;               // IO服务对象。
boost::asio::ip::tcp::socket socket(io_service);  // socket对象。
socket.connect(server_endpoint);			      // 同步连接到对应的endpoint,失败将抛出异常。

这段代码是很简单明确的,和api所表示的语义相符合, 初学asio的人,也许会对这种长长的名字空间不习惯,这不要紧,等到足够了解ip,tcp,socket这些层次关系,就会明白,这么做的用意。

也许会有人觉得最后一句里面,我的注释中,注明了”失败将抛出异常“这句话。

这个我需要跟大家讲解下,asio对操作处理结果的2种方式。

  1. 异常
  2. error_code

比如上面那个函数connect,它有两个重载的版本,一个是没有error_code参数的(通过抛出异常),另一个是带有error_code参数的,将错误信息写在error_code里。 所以,你不打算使用异常的情况下,你可以使用error_code来避免异常,获得函数操作的错误信息。 这种方式,是非常不错的的方式,我的avhttp也遵循了这种设计。

使用error_code 和 抛异常,两种方式,用户可以灵活的作出选择自己需要的方式。做为一个库, 应该让用户能选择。

说一下 io_service

如果一个io_service上面,没有任何的操作投递,默认是会退出循环的,也就是run会结束。有时,我们不需要它立即结束。

比如,你要预先跑几个io_service.run,后面再投递操作就可以使用work来保证run不会提前退出。

有人说投一个其它玩意来保证run不退出,我说:投一个其它玩意,用于保证run不退出,这不是做事的办法。

io_service抽象了各平台的不同细节,任何IO操作(无论异步或同步)都将通过io_service提供服务,相关实现被通过add_service注册在io_service中的一个模板队列中,可以通过对应的模板类型找回注册的类型对象。

下面讲异步

异步的顺序是这样的

your program --> io_object --> io_service --> operating system

这是调用过程

image

操作系统一旦操作完成,把结果放在一个队列中,通过io_service 回调回来。

如果有一个socket,执行下面操作

socket.async_connect(server_endpoint, your_completion_handler);

在调用io_service.run()之后,这个connect的操作完成时就会执行下面这个函数:

void your_completion_handler(const boost::system::error_code& ec);

error_code里保存了操作的错误信息,。 这是asio的异步基本用法。

整理如下:

asio::io_service io_service; // io_servide对象,提供各平台之间的抽象,所有io操作基于它完成.
//...
tcp::socket socket(io_service); // 这个socket就是一个 i/o object.
// ...
socket.async_connect( // 发起一个异步连接操作.
  server_endpoint, // 要连接到的目标主机.
  your_completion_handler);// your_completion_handler函数(或函数对象/lambda/bind)将在连接操作完成时被调用.
// ...
io_service.run(); // 开始执行事件循环.

io_service.run就是通过检查操作系统的队列(利用系统提供的 epoll/select/IOCP 调用),是否已经完成对应的操作。

  • io_service可以通过post来扩展自己的应用。
  • io_service可以是任何的操作投递, 不限于socket object…

可以看成它是一个队列模型, 每次异步调用, 就是往io_service中的队列投递的过程, 这就是刚刚那个图的意义

在asio中基本的异步概念?不知道大家有没有点明白?我总结一下:

  • asio 中 async_xxx 之类的调用,在操作完成时,总会回调你对应的handler。

其中当然除了async_xxx之类的调用,还有post和disaptch,下面是这两个东西的一些介绍:

  • post的东西就是一个handler。
  • dispatch会判断一个前提,如果是在io_run的线程中做的操作,那么会当作直接调用,如果不是,效果类似post。

讲解asio的设计

asio的异步是以Proactor设计的,通过io_service抽象了各种不同操作系统之间的区别,提供了统一的接口调用。 下面是asio基于Proactor的设计图:

image

这里的流程就是发起异步,入队列,出队列,执行handler的整个过程。

平台细节:

在windows上,iocp的设计本身就是一个良好的proactor模型。

通过WSASend/PostQueuedCompletionStatus之类的API将操作投递到队列中,然后通过GetQueuedCompletionStatus 之类的函数,获得操作执行的结果。

linux上是通过epoll/select模拟的,先等待 IO 状态,然后再发起IO操作,最后调用 handler 返回结果。

在linux中用epoll/select模拟proactor,asio自己需要实现几个部件,一个是 操作执行部分(如read之类的操作),队列,和 事件多路分离器。而 asio 这些都帮你做好了。 如read操作将在asio中执行掉,而不是用户自己去read,read到的东西,通过回调返回。


使用asio也可以编写Reactor风格的程序,示例如下:

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<char> buf(socket.available());
    socket.read_some(buffer(buf));
  }
}

上面代码就是一个Reactor风格的程序。

一些常见的错误

不注意对象生命期,如下例:

class connection
{
    tcp::socket socket_;
    boost::array<char, 1024> data_;
    // ...
    void start()
    {
       socket_.async_read_some(buffer(data_, 1024),
           boost::bind(&connection::handle_read, this
              asio::placeholders::error,
              asio::placeholders::bytes_transferred
           )
       );
    }
    // ...
    ~connection()
    {
        socket_.close();
    }
    // ...
    void handle_read(const boost::system::error_code &ec, std::size_t bytes_transferred)
   {
      // ...
   }
};

上面代码一旦start被调用后,如果提前销毁了这个connection对象,那么当async_read_some操作回调回来时,由于所在的对象不存在了,将可能导致crash。 那该如何解决这个问题呢?除了明确保证对象的生命期在所有回调完成之后,还可以使用enable_shared_from_this,这里不再举例。

另外:常常在网上有人说到,关于同时多次调用async_send的之类的问题(类似的有同时WSASend多次),首先说下,不要在项目中使用这种傻逼的方式编程,因为多次async_send之类的操作行为是不能确定的,你不停的调用async_send,而不等待上一次async_send调用的完成,这样做,结果可能是某次发送是不完整的,不要试图做这种傻事,也不要研究如何通过多次async_send来提高发送效率,因为一开始就犯了方向性错误!

关于asio的编程思维,我想应该讲的差不多了,下面讲一些细节的东西,主要是编程中常用到的


buffers

asio::buffer 是一个用作缓冲的warpper,它本身不管理内存,这一点要牢记

{
  std::string str = "hello";
  async_write(asio::buffer(str), handler);
}

这样的代码是可能会挂掉的,没挂掉是运气,也就是:

传进异步方法的东西都要明确的保证能活到他被用到的时候 这个很关键!

asio的各种异步/同步函数的buffer参数,可以支持多段不同的缓冲区:

std::vector<const_buffer> bufs2;
bufs2.push_back(boost::asio::buffer(d1));
bufs2.push_back(boost::asio::buffer(d2));
bufs2.push_back(boost::asio::buffer(d3));
bytes_transferred = sock.send(bufs2);

比如像上面 已经是个vector容器了

发送时,一段一段的连着,接收也是一段一段的连着,利用多段 buffer 可以很优雅的实现数据处理而无需手写 struct。

strand

strand 用于保证回调有顺。

strand内部是个队列,在队列中如果还有未执行的handler,新的投递,将会加入队列,如果队列为空,则将直接投递到系统。

streambuf

streambuf,是个非常好的变长buffer。 streambuf可以结合std::iostream 处理。 streambuf也可以把数据取出来,当作buffer处理,sgetn之类的函数,可以让你做到。 特别适合用来配合 read_until这类条件读取。

条件传输

asio 的 io 还提供了以条件作为操作终止条件的,并内置了:

  • transfer_all
  • transfer_at_least
  • transfer_exactly

三个条件,你也可以自己定制自己的条件对象来处理自己需要的方式,比如avhttp中的async_read_body.hpp就是一个很好的范例。

read_until

在这个实现中,实际上,每次是读取一块固定大小的长度,比如可能是512byte,然后在streambuf中match你的完成条件. 一旦match到,将返回完成这次操作。

并返回match到的字节数,而不是读取到的!

多读取到的部分,用于下次 read_until,因此下次 read_until 可能并不需要进入系统读取数据,也能返回数据哦~

比如对方发送的数据是 “123456789”

你使用 read_until(streambuf, “4”, handler);

其实第一次读取,已经将 “123456789” 存入 streambuf,但是 readuntil返回长度是4。

这时,你如果只从streambuf中,取出了前4个字符,再继续使用 read_until(streambuf, “9”, handler); 读取时,asio会直接将上一次读取的东西返回并返回读取的字节数为 5,而不是发起新的读取操作。


#2

帖子恢复了啊 还好保存了源码啊


#3
  1. API是工具库而非框架,其提供机制功能而不定义策略,Unix系统调用也是如此。
  2. post和disaptch,未能了解
  3. enable_shared_from_this, 为了保证connection对象存活下来: We will use shared_ptr and enable_shared_from_this because we want to keep the tcp_connection object alive as long as there is an operation that refers to it.
  4. 细节的东西,推后阅读

#4

本主题已全局置顶,它将始终显示在它所属分类的顶部。可以由版主解除置顶或者点击清除置顶按钮。


#5

#6

最后那个特性很酷哦