Asio源码分析(3):使用epoll实现Proactor模式(1)

使用epoll实现Proactor模式(1)

下面是一段Asio中典型的代码(忽略错误处理和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

"), [&](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();
}

我们将以socket.async_*()和ctx.run()作为切入点,详细分析Linux下Asio是如何使用epoll实现Proactor模式的。

Proactor设计模式

在开始之前,先回顾一下Proactor设计模式。下面是一张重要的图,它比较清楚地描述了Proactor设计模式的各个组成部分,以及它们之间的关系。但它和实际的实现有一点差异。

    异步操作(Asynchronous Operation): 定义异步执行的操作,例如异步读写套接字,或者发起异步连接。它被抽象为operation类。 异步操作处理器(Asynchronous Operation Processor): 执行异步操作。当异步操作完成后,将该事件放在完成事件队列里。例如reactive_socket_service是一个异步事件处理器。 完成事件队列(Completion Event Queue): 缓存已完成的事件,直到被异步事件分发器取出。 完成处理器(Completion Handler): 处理异步操作的结果,它是一个函数对象。 异步事件分发器(Asynchronous Event Demultiplexer): 等待(阻塞)完成事件队列中有事件发生,并会返回一个完成事件给调用者。 Proactor: 调用异步事件多路分发器取事件,然后调用该事件关联的完成处理器。它被抽象成io_context类。 Initiator 启动异步操作的用户代码。initiator 通过高层接口(例如basic_stream_socket)与异步操作处理器交互,basic_stream_socket将该操作转接给一个服务(例如reactive_socket_service)。

使用Reactor模拟Proactor

在许多平台,Asio通过select,epoll或kqueue实现Proactor设计模式:

    异步操作处理器(Asynchronous Operation Processor): 一个使用select,epoll或kqueue实现的reactor。当reactor发现可以执行某些操作(例如套接字读写)时,它将执行该异步操作并将该异步操作关联的完成处理器放到完成事件队列中。 完成事件队列(Completion Event Queue): 完成处理器的链表。 异步事件多路分发器(Asynchronous Event Demultiplexer): 在一个事件或条件变量上等待,直到完成处理器队列中出现一个完成处理器。

Asio函数调用过程

先对Asio函数调用流程有一个整体的认识。

socket.async_*()相关函数调用的过程大致如下:

这里大致可以分为3个层次:

    basic_socket提供给用户的接口:例如async_connect()、async_send()。 service:basic_socket将async_connect()、async_send()转交给service。 底层API:service将使用这些API,例如socket()。

ctx.run()函数调用的过程大致如下:

io_context将它的工作交给scheduler,scheduler主要负责两件事:

    调用::epoll_wait()。 调用scheduler::complete()。

参考

经验分享 程序员 微信小程序 职场和发展