asio学习——第一篇

asio是c++编写的网络框架,其使用一套统一的视角实现了跨平台的异步网络IO机制,其对异步IO的封装、c++模版的使用都值得学习,加之c++目前没有大一统网络框架,而asio是其中相当具有代表性的一个,为了日后的工作积累技术经验,培养自身c++能力,故选择asio进行学习,因本人目前只在linux平台开发,故不对asio的window部分进行说明。

1. asio异步模型

这部分不详细描述了,学生时期各种网络模型已经烂熟于胸。下面是描述asio异步模型的文章,其大部分内容来自asio的官网参考文档,可以直接阅读该文章,也可以去asio官网阅读参考文档。

参考资料:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p244...

2. 一个异步API的使用示例

#include <iostream>
#include "asio.hpp"
void timer_hander(const asio::error_code &code) {
 if (!code) {
 std::cout << "time out" << std::endl;
 }
}
int main() {
 asio::io_context ioc;
 asio::steady_timer async_timer(ioc, asio::chrono::seconds(5));
 async_timer.async_wait(timer_hander);
 ioc.run();
 return 0;
}

该示例来自asio官方文档,代码主要包含有三部分组成

  1. 创建io_context
  2. 创建异步定时器,其组成部分中包含io_context(从上述的代码中可以看出
    定时器创建时传入了一个io_context,因此很容易联想到二者之间是复合的关系,
    但实际上仅仅是关联关系,即异步定时器作为一个异步对象,“uses” io_context)
  3. io_context的执行

顺着这样的思路,接下来本文章从四个问题浅析asio,

  1. 在asio中io_context是什么,它到底扮演何种角色?
  2. 异步对象/操作与io_context之间的关系是什么?
  3. async_wait发生了什么?
  4. io_context run执行之后会发生什么?

3. 什么是io_context?

这个问题首先可以在asio源码中找到答案,

* The io_context class provides the core I/O functionality for users of the
* asynchronous I/O objects, including:

* @li asio::ip::tcp::socket
* @li asio::ip::tcp::acceptor
* @li asio::ip::udp::socket
* @li asio::deadline_timer.

“为异步IO对象提供基础的IO操作”,咋看之下,io_context就是为异步对象提供了一种工具,这种工具能够让异步对象进行IO操作,仔细琢磨会觉得上面这句话又太抽象,linux系统调用read/write也是提供io操作,io_context相比这些又封装了哪些功能,又是如何实现的呢?一句提供了IO操作,未免掩盖了太多的细节。

以下是本人猜测
我们都知道,linux平台上用的最多的多路复用机制是epoll,asio利用epoll模拟异步io之前早有耳闻,故asio的io_context应是在epoll基础之上,将io事件注册、驱动、回调以及数据的管理等等进行封装,并在这层epoll之上再架设一层事件分发的机制,以达到读写数据之后,执行任务的目的。


除了源码之外,还能找到更多关于设计io_context的信息吗?在asio官网或者第一节提到的文章中,可以看到下面这张图

首先让我们来理解一下这张图的内容
最顶层是一个叫做Asynchronous agent的虚拟对象,该对象并不真实存在于asio代码中,从官方的解释来看,

“An asynchronous agent is a sequential composition of asynchronous operations.”

着实像是一句说了什么,但是又好像什么也没说的话。除此之外有两个很关键的话,

“An asynchronous agent is an entity that may perform work concurrently with other agents. Asynchronous agents are to asynchronous operations as threads are to synchronous operations.”,

以及一副很重要的图,

从图中以及上述的描述,我们可以得出一个重要信息,一个Async agent中的异步操作是顺序执行的,也即一个异步操作完成之后引发的另一个异步操作(或者仅有一个异步操作),由此形成的链式异步操作可以称之为一个Async agent,这符合我们对于异步操作中“异步”的想象吗?仔细想想,可以发现,此处一个Async agent其实代表一个大动作,而其中由一系列小的异步事件组成,虽然各个异步事件是线性的,但这种线性却不是从操作系统角度出发的,而是从这个大动作本身,或者说业务本身出发的,也即一个业务可能由多个事件组成,而多个事件本身又是一个个异步操作。所以一个Async agent之于异步操作,相当于一个线程之于同步操作,还是一个比较恰当的类比的。只不过一个线程是许多同步操作的物理实体的承载,而一系列线性异步操作则从逻辑上构成了Async agent。

再往下分别是Async operation和Associated characteristics,Async operation自不必说,代表一个异步操作,比如网络套接字异步地读取数据。一个异步操作由一个初始化函数启动,当异步操作结束时会执行一个回调函数,执行进一步的动作。而Associated characteristics则是一系列抽象属性,这样的抽象类似Policy编程。上图中分别展现了三种属性,分别是执行器、分配器和终止位,io_context则是执行器的一种,其功能前面提到过,为异步对象提供IO操作功能,也符合此处的“uses”关系。除此之外还有thread_pool executor、strand adapter等executor。

稍微总结一下,在asio中,以一个大事件为主体,其中包含许多小的异步操作,这些异步操作在运行过程中,需要依赖一些的功能,比如异步操作需要能够进行IO、需要将IO获取的数据进行缓存、需要能够执行IO之后的更具体的任务、需要能够决定如何取消自身等等。而io_context则是为异步操作提供了最基础的IO能力,需要注意的是一个异步操作与io_context之间并不是包含关系,io_context相较于异步操作,是一个独立的运行实体,异步操作仅仅是借助它实现IO操作。

讲到这里,第二节中的前两个问题应该有个大概的认识了。接下来看后两个问题。

4. async_wait剖析

不妨先来看一下steady_timer的定义

template <typename Clock,
 typename WaitTraits = asio::wait_traits<Clock>,
 typename Executor = any_io_executor>
class basic_waitable_timer;
typedef basic_waitable_timer<chrono::steady_clock> steady_timer;

可以看到,steady_timer是由basic_waitable_timer特化而成,而basic_waitable_timer有三个模版参数,分别是Clock、WaitTraits以及Executor,这其中Clock应该最容易理解,即定时器所用的时钟类型,比如steady_timer中使用的steady_clock是“稳定的时钟”,即时间均匀流逝的时钟。除此之外Executor这个参数貌似与第三节提到的Executor有关系,这里默认的Executor是any_io_executor。但是wait_traits却不知道是何作用。

此处说明一下关于模版的运用问题,模版声明时可以提供默认参数,在进行模版实例化必须使用<>号,即便其所有模版参数都有默认参数,比如以下定义
template <typename T = int>
MyClass;

template <typename T>
MyClass {};

这个类只有一个模版参数,并且有默认参数,此时实作这个类对象需要使用一个空的<>,如下所示,否则编译器报错。
MyClass<> c;

除此之外,还可以利用偏特化,进一步分流

再来看看wait_traits的定义,wait_traits中定义了两个函数,准确的说就是一个函数,改函数的作用是返回需要等候的时钟周期,定义如下,当传入具体的时钟时,对应的duration单位会相应发生改变。

template <typename Clock>
struct wait_traits
{
 /// Convert a clock duration into a duration used for waiting.
 /**
 * @returns @c d.
 */
 static typename Clock::duration to_wait_duration(
 const typename Clock::duration& d)
 {
 return d;
 }
 /// Convert a clock duration into a duration used for waiting.
 /**
 * @returns @c d.
 */
 static typename Clock::duration to_wait_duration(
 const typename Clock::time_point& t)
 {
 typename Clock::time_point now = Clock::now();
 if (now + (Clock::duration::max)() < t)
 return (Clock::duration::max)();
 if (now + (Clock::duration::min)() > t)
 return (Clock::duration::min)();
 return t - now;
 }
};

至于Executor太过复杂,先放到一边。

接下来看一看,async_wait这个函数的定义

template <
 ASIO_COMPLETION_TOKEN_FOR(void (asio::error_code))
 WaitToken ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
 ASIO_INITFN_AUTO_RESULT_TYPE_PREFIX(
 WaitToken, void (asio::error_code))
 async_wait(
 ASIO_MOVE_ARG(WaitToken) token
 ASIO_DEFAULT_COMPLETION_TOKEN(executor_type))
 ASIO_INITFN_AUTO_RESULT_TYPE_SUFFIX((
 async_initiate<WaitToken, void (asio::error_code)>(
 declval<initiate_async_wait>(), token)))
 {
 return async_initiate<WaitToken, void (asio::error_code)>(
 initiate_async_wait(this), token);
 }

一眼看过去,这个定义真的是非常复杂,完全看不懂。。。,所有我们首先先搞懂这个定义到底是个什么东西。

  1. 首先能看到这个函数是一个模版函数,所以以下代码只是对模版参数的声明

     template <
     ASIO_COMPLETION_TOKEN_FOR(void (asio::error_code))
     WaitToken ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>

    ASIO_COMPLETION_TOKEN_FOR这个宏定义是做什么的?查看定义之后发现它只是一个typename而已,而该宏中的参数实际上对于代码而言没有任何作用,其只是起到一个“告示牌”的作用,你可以认为它就是一个注释,它表明WaitToken这个模版参数是一个返回值为void,参数为asio::error_code的函数而已。而ASIO_DEFAULT_COMPLETION_TOKEN_TYPE这个宏定义是为WaitToken指定了默认的参数。

  2. 末尾{}部分的内容则是函数的实际内容,这个比较容易识别
  3. 下面看中间的部分

    ASIO_INITFN_AUTO_RESULT_TYPE_PREFIX(
     WaitToken, void (asio::error_code))
     async_wait(
     ASIO_MOVE_ARG(WaitToken) token
     ASIO_DEFAULT_COMPLETION_TOKEN(executor_type))
     ASIO_INITFN_AUTO_RESULT_TYPE_SUFFIX((
     async_initiate<WaitToken, void (asio::error_code)>(
     declval<initiate_async_wait>(), token)))

    首先根据宏的命名可以看出来这个结构分为三部分,分别是prefix、async_wait、suffix,即前缀、函数声明、后缀。prefix是函数的返回值这个应当是比较简单的,那suffix是什么呢?函数名称及参数后面还可以有别的声明内容吗?确实如此,查看cppreference可以发现,c++11之后的函数声明结构如下所示,

    noptr-declarator ( parameter-list ) cv (optional) ref  (optional) except (optional) attr (optional) -> trailing

    具体内容请查看cppreference,所以这个结构也瞬间明朗了。值得一提的是在ASIO_INITFN_AUTO_RESULT_TYPE_SUFFIX中出现了“告示牌”用法,即其参数对于宏定义来说并没有实际的作用,但其被函数体所使用了。接下来,我们看看函数体

  4. 很简单的一句话,即调用了async_initiate函数,并传入了两个参数,其中一个参数是WaitToken类型的(还记得它是什么类型的吗?)。关于async_initiate,在第三节,我们提到过,一个异步操作由initiating function发起,还记得那个图吗?此处的async_initiate就是这个function。

    return async_initiate<WaitToken, void (asio::error_code)>(
     initiate_async_wait(this), token);

    未完待续。。。

作者:金金原文地址:https://segmentfault.com/a/1190000043836908

%s 个评论

要回复文章请先登录注册