Asio源码分析(1):概述

Asio源码分析 专栏收录该内容
3 篇文章 0 订阅

Asio源码分析(1):概述

Asio是一个跨平台的C++网络库,目前它的一部分已经成为了C++ Networking TSC++ Technical specifications),并有望加入到C++23。从Asio将并入标准库这一点来看,它应该是一个不错的网络库,有一定的学习价值。

这是Asio源码分析系类文章的第一篇,这个系列要解决的问题主要有:

  • Asio的源码用到了哪些C++技巧,做了哪些优化?
  • 在Linux下,Asio是如何通过epoll来实现Proactor模式的?
  • 在Linux下,Asio是如何实现并发的?
  • Asio的异步模型是如何与C++20协程结合的?

该系列使用的Asio版本为1.18.2。通过对Asio源码的阅读,可以感受到作者C++编码水平的高超,整个库提供了很多定制点(Customization points)。

Proactor和Reactor设计模式

Asio的异步支持基于Proactor设计模式。首先,我们简单对比一下Reactor模式和Asio的Proactor模式有什么不同。

当前普遍存在的网络库很多都是Reactor设计模式,例如libeventmuduo。而Asio是Proactor模式。Asio的Proactor模式会导致编写的代码很奇怪,它会将程序的业务逻辑分散在各个回调函数之中,提高了编码的复杂度,相对不如Reactor模式清晰(其实Asio也支持Reactor-style的编码方式)。例如使用Asio实现一个tcp echo server但Proactor可以很轻易地与协程相结合,让我们可以编写和goroutine一样简洁的代码。

从UNIX网络编程中定义的5中I/O模型来看,Proactor模式类似于异步I/O,而Reactor模式类似于I/O多路复用。


Reactor模式的实现方式基本为I/O多路复用,其中的selectpollepoll就是一个Reactor。

  • 以读操作为例,Reactor模式的大致流程为:用户注册一个读回调函数,程序阻塞在Reactor上,每当有读操作可用时便调用该回调函数(一些网络库还会管理缓冲,在执行完读操作后才调用回调函数)。一般读回调函数不需要重复注册,只需注册一次即可。
  • 而进行写操作时,一般会由网络库负责完整地将数据发出,用户只需要调用对应的函数然后直接返回即可。

Reactor模式在用户的角度来看,程序阻塞于Reactor,等待读操作可用,而写操作立即返回。


而Proactor模式与Reactor模式的流程有一些不同。

  • 以读操作为例,Proactor模式有一个Initiator用于发起一次异步读操作,其中指定一个回调函数,然后立即返回。当读操作可用并执行完读操作之后,调用Initiator指定的回调函数。
  • 写操作与读操作类似,只是将发起一次读操作换成发起一次异步写操作而已。

Proactor模式在用户的角度来看,读写操作都立即返回。


Asio的Proactor模式的典型代码大致如下(忽略错误处理和tcp消息边界相关问题) :

#include <iostream>

#include "asio.hpp"

char read_buf[4096];

int main() {
    asio::io_context ctx;
    asio::ip::tcp::endpoint endpoint{asio::ip::make_address("127.0.0.1"), 80};
    asio::ip::tcp::socket socket{ctx};

    socket.async_connect(endpoint, [&](std::error_code err) {
        socket.async_send(asio::buffer("GET / HTTP/1.0\r\n\r\n"), [&](std::error_code err, std::size_t n) {
            socket.async_receive(asio::buffer(read_buf), [&](std::error_code err, std::size_t n) {
                std::cout << read_buf;
            });
        });
    });

    ctx.run();
}

上面的代码先连接到目标主机,然后发送HTTP请求,最后接收并打印HTTP响应内容。

由于Proactor仅发起一次异步操作,所以这段代码形成了所谓的回调地狱。这样的代码会让程序的业务逻辑分散在各个回调函数中,导致难以阅读。

因为Proactor仅发起一次异步操作这个特点,使得它非常容易与协程结合。作为回调函数的替换,我们只需要在发起异步操作时将当前协程挂起,然后在I/O操作完成后恢复这个挂起的协程。

C++20引入了协程(Coroutines),但它和Go语言中的goroutine有很大的不同。C++20的协程只实现了上下文的切换,没有实现调度功能,所以还需要相应库的支持。

而Asio正是这样一个库,作者的C++功底很深,留下了许多定制点(Customization points),可以轻松转变为协程。这使得我们可以轻易写出和Go语言一样简洁的代码。例如上面的回调地狱可以变成:

#include <iostream>

#include "asio.hpp"

asio::awaitable<void> http_request(asio::ip::tcp::socket socket, const asio::ip::tcp::endpoint& endpoint) {
    char read_buf[4096]{};

    try {
        // Asio的设计很灵活,`asio::use_awaitable`这个参数可以通过一些手段忽略掉。
        co_await socket.async_connect(endpoint, asio::use_awaitable);
        co_await socket.async_send(asio::buffer("GET / HTTP/1.0\r\n\r\n"), asio::use_awaitable);
        co_await socket.async_receive(asio::buffer(read_buf), asio::use_awaitable);

        std::cout << read_buf;
    } catch (const std::exception& e) {
        std::cout << e.what() << "\n";
    }
}

char read_buf[4096];

int main() {
    asio::io_context ctx;
    asio::ip::tcp::endpoint endpoint{asio::ip::make_address("220.181.38.148"), 80};
    asio::ip::tcp::socket socket{ctx};

    asio::co_spawn(ctx, http_request(std::move(socket), endpoint), asio::detached);

    ctx.run();
}

可以做到以同步的方式实现异步逻辑,非常优雅!

参考

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值