ASIO模型

事件驱动,把网络IO和处理逻辑封装成一个事件,丢进事件列表,启动事件处理。

ASIO API

基于ASIO 1.85.0,万变不离其宗,默认using namespace boost::asio;

1. IO上下文

事件驱动,得有个处理事件的东西。io_context类就是处理事件的东西,在设计中它被抽象为一个IO上下文。所谓上下文,就是理解当前内容所需的前提。比如要“改变当前变量的值为100”,就要先知道“当前变量是什么,类型是什么”。一个IO上下文就是进行IO操作的时候需要知道的东西,比如“程序运行在计算机上还是座机上”。有了上下文,才能执行IO操作。

io_context io_ctx;

2. 端点

ip + port = endpoint,就是一种封装

// 以tcp server为例,ip::tcp::v4()就是自己的ip,12345是要监听的端口号,这俩组合成了端点
ip::tcp::endpoint ep(ip::tcp::v4(), 12345);

3. 接受器(acceptor)

同样以tcp server为例,一个tcp server的完整工作流程是bind -> listen -> accept -> process -> close

一个接受器的本职工作是完成accept,要完成accept则必须要进行bind和listen。bind和listen所需的参数其实只有一个端点。可能有人会在想,原生网络编程中,bind和listen还需要一个socket,为什么只有端点一个参数呢:因为接受器就是对这个socket的抽象。

既然接受器是个socket,它就得能够IO。而IO是IO上下文提供的服务,所以接受器要绑定在一个IO上下文上。

ip::tcp::acceptor acceptor(io_ctx, ep);

4. 套接字

刚说过接受器是个socket,裸socket就只是少了端点参数,只用绑定IO上下文即可。

ip::tcp::socket sock(io_ctx);

5. 异步accept

异步accpet是接受器的成员,用于在(IO上下文的)事件列表中添加异步accept事件。

accept时会成功建立一个连接,在网络编程中一个连接也被抽象为一个socket,因此异步accept函数的调用要传入一个用于表示连接的socket。

如果只是接受连接请求,那么提供一个socket就足够了。但是在accept后更重要的是对数据的处理。由于将异步accept作为一个事件进行处理,我们并不会等待accept处理完再执行后续指令,因此我们需要提前指定当accept完成后应当干什么。

我们就要传递一个函数对象来指定accept完成后的操作。

acceptor.async_accept(sock, std::function<void(const boost::system::error_code &ec)>)

6. 异步write

与异步accept类似,我们通过异步wrtie往代表连接的socket里写入数据完成通信。

同样的,我们需要传递一个函数对象来指定write结束后应当做什么。

sock.async_write_some(buffer("Hello from server!"), std::function<void(const boost::system::error_code &ec, std::size_t)>) {

7. 开启事件循环

所有的异步操作都作为事件添加进了(IO上下文的)事件列表,接下来就可以执行这个事件列表了。如果事件列表中尚有异步操作未完成,则事件列表不会执行完毕,从外部来看,就是线程阻塞在了开启事件循环的这个语句上。

io_ctx.run()

异步IO的异步性则体现在了io_ctx内部,io_ctx事件列表中的各个事件是并发的。

面向过程的基本例子

#include <boost/asio.hpp>
#include <exception>
#include <iostream>

int main()
{
    using namespace boost::asio;
    // 1. 创建io上下文
    io_context io_ctx;
    // 2. 创建endpoint(ip与端点的组合)
    ip::tcp::endpoint ep(ip::tcp::v4(), 12345);
    // 3. 定义accept这个动作的上下文,要accept则必须listen,这里定义的就是listen这个动作
    // 但是并没有开始listen,也并没有开始accept
    ip::tcp::acceptor acceptor(io_ctx, ep);
    // 4. 创建socket
    ip::tcp::socket sock(io_ctx);
    // 5. 定义异步accept这个动作
    // accpet时绑定socket;当accept成功时,会调用lambda函数
    acceptor.async_accept(sock, [&sock](const boost::system::error_code &ec) {
        // acceptor.async_accept::lambda
        // 5.1 如果有错误,抛出异常
        if (ec)
            throw boost::system::system_error(ec);
        // 5.2 如果没有错误,输出Accepted!
        std::cout << "Accepted!" << std::endl;
        // 6. 进一步定义accept成功后的操作,这里是异步write,实际上还有异步read等操作
        // write是向socket写入Hello from server!,当写入成功时,调用lambda函数
        sock.async_write_some(buffer("Hello from server!"), [&sock](const boost::system::error_code &ec, std::size_t) {
            // sock.async_write_some::lambda
            // 6.1 如果有错误,抛出异常
            if (ec)
                throw boost::system::system_error(ec);
            // 6.2 如果没有错误,输出Data sent!
            std::cout << "Data sent!" << std::endl;
            // 6.3 关闭socket,断开连接
            sock.close();
        });
    });
    // 到此为止,server没有进行bind, listen, accept, write, close等任何操作
    // 我们只是在io_ctx上下文中定义了这些操作
    // io_ctx更像是一个TODO list,里面有很多操作,但是并没有开始执行
    // 7. 现在我们开始io_ctx的事件循环
    io_ctx.run();
    return 0;
}

1.程序编译运行后,因为异步accept事件尚未结束(未成功accept),所以程序进行事件循环,即不断监听端口

 *  正在执行任务: xmake run asio 

2.通过数据包发送软件向127.0.0.1:12345发送任意tcp数据流

数据包发送软件收到回复

Hello from server!\00

服务器terminal

 *  正在执行任务: xmake run asio 

Accepted!
Data sent!
 *  终端将被任务重用,按任意键关闭。 

可以看到异步accept完成后它的std::function成为了下一个异步事件并被触发,在这个std::function里又添加了一个异步write事件。异步write事件完成后,又触发了它的std::function,这个std::function运行结束后,所有异步事件结束,异步事件列表为空,io_ctx.run()返回,主函数返回,程序结束。

如果觉得我的文章对你有用,请随意赞赏