asio 学习

345 阅读6分钟

简介

  • 参考 boost-asio-study
  • asio,异步io,Asynchronous Input/Output,是一个跨平台的C++库,用于网络和底层I/O编程,它为开发者提供了一种一致的异步模型,采用现代C++的方法

示例1

  • 每个 asio 程序都有一个 io_context 对象,代表一个 io 服务,通过这个服务操作 asio。
  • 下面的代码是一个空 io_context 的示例,context.run() 是一个阻塞的操作,将事件全部消费后就会退出。这段代码因为没有任何事件,执行后就立即结束了。
void test001()
{
    asio::io_context context;
    context.run();
}

示例2

  • 所以需要给服务生产事件才能让其发挥作用,下面则是通过定时器来实现
void test002()
{
    asio::io_context   context;
    asio::steady_timer timer(context, std::chrono::seconds(3));
    timer.async_wait([](const system::error_code& ec) {
        std::cout << "3 sec tick" << std::endl;
    });
    context.run();
}
  • 创建定时器 steady_timer, 指定 3 秒,然后异步等待,时间到后触发执行 lambda 函数打印内容
  • 有几点需要注意
    • 所有IO对象都需要一个 io_context,一般在构造时传入
    • async_wait 是一个异步操作,但真正执行是在 context.run()
    • async_wait 传入的参数是一个回调函数,超时后会被调用
    • 每个IO对象的 async_xxx 都有回调函数,而每个回调函数的参数是不一样的,具体的需要看文档
    • 除了 async_wait 异步等待,也可以同步等待 wait,此时就会阻塞,直到超时结束

示例3

  • async_wait 的回调函数的参数是 boost::system::error_code,如果需要给回调函数增加额外的参数,就需要使用 bind
void test003_1(const system::error_code& ec, asio::steady_timer* timer,
               int* counter)
{
    if (*counter >= 3) {
        return;
    }

    std::cout << "tick: " << *counter << std::endl;
    ++(*counter);
    timer->expires_after(std::chrono::seconds(1));
    timer->async_wait(
        std::bind(&test003_1, std::placeholders::_1, timer, counter));
}

void test003()
{
    int                counter = 0;
    asio::io_context   context;
    asio::steady_timer timer(context, std::chrono::seconds(1));
    timer.async_wait(
        std::bind(&test003_1, std::placeholders::_1, &timer, &counter));
    context.run();
}
  • 这个示例中通过 std::bind 生成了一个新的回调函数,这个函数的第一个参数使用了占位符 std::placeholders::_1,表示在调用这个回调函数时,需要传入第1个参数,而第2个和第3个参数则是使用了 timercounter,不需要额外传入,其实就是相当于默认参数

示例4

  • 除了使用定时器,还可以使用 socket 来生产事件,下面是一个 echo server 的实现
  • 首先使用同步方式
void test004_1(asio::ip::tcp::socket socket)
{
    try {
        while (true) {
            array<char, 1024>  buff;
            system::error_code ec;
            std::size_t length = socket.read_some(asio::buffer(buff), ec);
            if (ec == asio::error::eof) {
                std::cout << "session close" << std::endl;
                break;
            } else if (ec) {
                throw system::system_error(ec);
            }

            asio::write(socket, asio::buffer(buff, length));
        }
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
}

void test004()
{
    asio::io_context        context;
    asio::ip::tcp::acceptor acceptor(
        context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));
    try {
        while (true) {
            test004_1(acceptor.accept());
        }
    }
    catch (const std::exception& e) {
        std::cerr << "exception: " << e.what() << std::endl;
    }
}
  • 上面的示例启动时会监听 12345 端口
  • acceptor.accept() 时阻塞等待客户端建立连接
  • 一旦有连接上来,则触发 test004_1 函数
  • 接着通过 socket.read_some 阻塞等待客户端发送内容
  • 收到内容后通过 boost::asio::write 将内容发送给客户端
  • 有几点需要注意
    • tcp::acceptor 是一个IO对象,用来处理TCP连接,端口由 tcp::endpoint 指定
    • 因为直接使用 acceptread_somewrite 这些阻塞操作,整个过程是同步的,所以每次只能处理一个连接,但可以连续发消息,除非客户端断开连接
    • 写回数据时没有使用 socket.write_some,因为它不能一次性写完所有数据,但 boost::asio::write 可以
    • test004_1 函数中的参数虽然是值传递,但是 accept 返回的结果使用了移动语义,所以没有拷贝值,这个是 C++11 中引入的一项优化,当一个函数返回一个局部对象时,这个对象会被移动,而不是被复制

示例5

  • 接着是使用异步的方式
  • 使用异步,主要的问题是对象生命周期,可以使用 shared_ptr 解决
  • 为了同时处理多个连接,需要保留每个连接的 socket 对象,所以抽象一个处理连接的类 Session
class Session : public std::enable_shared_from_this<Session> {
public:
    Session(asio::ip::tcp::socket socket) : socket_(std::move(socket)) {}

    void Start()
    {
        DoRead();
    }

    void DoRead()
    {
        auto self(shared_from_this());
        socket_.async_read_some(
            asio::buffer(buffer_),
            [this, self](const system::error_code& ec, std::size_t length) {
                if (!ec) {
                    DoWrite(length);
                }
            });
    }

    void DoWrite(std::size_t length)
    {
        auto self(shared_from_this());
        asio::async_write(
            socket_, asio::buffer(buffer_, length),
            [this, self](const system::error_code& ec, std::size_t length) {
                if (!ec) {
                    DoRead();
                }
            });
    }

private:
    asio::ip::tcp::socket  socket_;
    std::array<char, 1024> buffer_;
};

class Server {
public:
    Server(asio::io_context& context, std::uint16_t port)
        : acceptor_(context,
                    asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))
    {
        DoAccept();
    }

private:
    void DoAccept()
    {
        acceptor_.async_accept([this](const system::error_code& ec,
                                      asio::ip::tcp::socket     socket) {
            if (!ec) {
                std::make_shared<Session>(std::move(socket))->Start();
            }
            DoAccept();
        });
    }

private:
    asio::ip::tcp::acceptor acceptor_;
};

void test005()
{
    asio::io_context context;
    Server           server(context, 12345);
    context.run();
}
  • 上面的示例启动后创建 Server 类,该类监听 12345 端口
  • 使用 async_accept 等待客户端连接,这个是异步等待
  • 当有连接上来后,创建 Session 类处理连接
  • Session 里使用 async_read_some 异步等待客户端发送数据
  • 收到数据后使用 async_write 异步发送数据
  • 可以看到每个读写逻辑都是用了 async_xxx 来实现,这样程序就不会阻塞在读写初可以处理后面的任务
  • 有几点需要注意
    • Session 里有两个成员变量,socket_ 负责跟客户端通信,在构造时通过move移动语义传递进来;buffer_ 负责缓存读取的数据
    • socket_ 变量一旦销毁,连接就会中断,所以需要保证 Session 不销毁
    • async_accept 后通过 std::make_shared 生成 Sessionshared_ptr 对象,此时计数+1,如果不做任何处理,那么在执行 async_read_some 后,shared_ptr 对象就会离开作用域,进而销毁 Session 对象,因为 async_read_some 是异步的,里面的 lambda 函数只有在有数据时才会执行。所以需要将 Session 的引用传入到 lambda 函数里引用住,这就是 auto self(shared_from_this()); 和 lambda 里的 [this, self] 的作用。
    • 使用 shared_from_this() 共享了 Session 的控制权,避免多个 shared_ptr 同时持有同一个对象而导致的重复销毁问题,传入 this 是让 lambda 函数能像正常的成员函数一样去调用成员变量和其他成员函数,传入 self 则是为了引用住 Session 对象,延后其生命周期,保证在数据达到并触发 lambda 函数后 Session 对象依然存在
    • async_write 原理跟 async_read_some 一样,也是通过 shared_from_this 延续 Session 对象的生命周期,同时在发送数据后重新 async_read_some 等待下一次数据的到达