C++高性能网络编程:Boost.Asio

在这里插入图片描述

标题:解锁C++高性能网络编程:Boost.Asio入门指南

什么是Boost.Asio?

Boost.Asio是一个跨平台的C++库,用于网络和底层I/O编程。它提供了一个现代化的、基于C++的异步编程模型,极大地简化了涉及套接字、定时器、串口等I/O操作的程序开发。

Boost库就像标准库的前瞻一样,创新的提供一个适合C++发展的方向,为C++标准的发展和更新提供了良好的启发和指导的作用,作为一个C++工程师/ 学者,了解Boost的使用是很有必要的。

为什么选择Boost.Asio?

  1. 高性能与可扩展性: 基于事件驱动和异步模型,Asio能够用极少的线程处理成千上万的并发连接,非常适合需要高吞吐量和低延迟的服务器应用。
  2. 跨平台: 在Windows、Linux、macOS等系统上提供一致的API,底层封装了如epoll(Linux)、kqueue(macOS/BSD)、IOCP(Windows)等操作系统特有的高性能I/O机制。
  3. 纯粹的头文件库(大部分): 大部分功能只需包含头文件即可使用,易于集成到项目中。
  4. 成为标准库的基石: Boost.Asio的设计思想直接影响了C++20标准中的网络库(std::net)。
核心概念:同步 vs. 异步

理解这两种操作模式是掌握Asio的关键。

  • 同步操作

    • 当你调用一个同步函数(如socket.read)时,调用线程会一直阻塞,直到操作完成(数据成功读取或发生错误)。
    • 逻辑简单直观,但一个线程在同一时间只能处理一个连接,资源利用率低。
    // 伪代码:同步读取
    char data[1024];
    size_t len = socket.read_some(buffer(data)); // 线程在这里等待,直到有数据到来
    process_data(data, len); // 处理数据
    
  • 异步操作

    • 当你调用一个异步函数(如async_read)时,函数会立即返回,而不会等待操作完成。你需要提供一个完成处理程序(Completion Handler),即一个回调函数。
    • 当异步操作在后台完成时,Asio会调用你提供的处理程序。
    • 调用线程在等待期间可以自由地去处理其他任务,从而最大限度地提高CPU利用率。
    // 伪代码:异步读取
    void on_data_received(error_code ec, size_t len) {
        if (!ec) {
            process_data(buffer_, len);
            // 再次发起异步读取,形成循环
            socket.async_read_some(buffer(buffer_), on_data_received);
        }
    }
    
    // 启动第一次异步读取
    socket.async_read_some(buffer(buffer_), on_data_received);
    // 此时,当前线程可以立即去做别的事情
    

Asio的心脏:io_context

io_context是Boost.Asio的核心。它是在应用程序和操作系统I/O服务之间的接口。你需要它来做几乎所有事情:

  • 分发I/O事件: 它负责检测底层的I/O操作(如套接字数据到达、定时器超时)是否完成,并调用相应的完成处理程序。
  • 运行事件循环: 你必须调用io_context::run()方法来启动事件循环。这个循环会持续运行,等待并分发事件,直到所有工作都完成。

Boost库的底层基于事件循环的机制

不了解事件驱动的IO多路复用的可以先了解一下,无论是Linux下面的epollmacos下面的kqueue, 还是Windows下面的ICOP,本质都是基于事件触发的IO多路复用的模型,对于一般的阻塞式服务器来说,一个执行流同时只能被一个IO任务阻塞, 如果一个IO任务陷入了阻塞, 这个服务器都会陷入阻塞,有的人就会想, 我是不是采用多线程的模式,给每一个IO任务都分配一个线程作为执行流,当然是可以的,在一定程度上,这确实提高了服务器同时处理任务的能力,但是对于业务巨大的服务器来说,服务器的线程也显得太过重,因此,大佬就提出了基于事件的IO处理模型。具体的细节可以自己去浏览。

动手实践:一个简单的异步TCP Echo服务器

这是Asio官网提供的样例

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

using boost::asio::ip::tcp;

// 表示一个与客户端连接的会话
class Session : public std::enable_shared_from_this<Session> {
public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}

    void start() {
        do_read();
    }

private:
    void do_read() {
        // 使用shared_from_this()确保Session对象在异步操作期间保持存活
        auto self(shared_from_this());
        socket_.async_read_some(
            boost::asio::buffer(data_, max_length),
            [this, self](boost::system::error_code ec, std::size_t length) {
                if (!ec) {
                    // 读取成功,将数据写回(Echo)
                    do_write(length);
                } else {
                    // 发生错误(如客户端断开连接)
                    std::cerr << "Read error: " << ec.message() << std::endl;
                }
            });
    }

    void do_write(std::size_t length) {
        auto self(shared_from_this());
        boost::asio::async_write(
            socket_,
            boost::asio::buffer(data_, length),
            [this, self](boost::system::error_code ec, std::size_t /*length*/) {
                if (!ec) {
                    // 写入成功,继续读取下一条消息
                    do_read();
                } else {
                    std::cerr << "Write error: " << ec.message() << std::endl;
                }
            });
    }

    tcp::socket socket_;
    enum { max_length = 1024 };
    char data_[max_length];
};

// 服务器类,负责监听和接受新连接
class Server {
public:
    Server(boost::asio::io_context& io_context, short port)
        : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) {
        do_accept();
    }

private:
    void do_accept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    // 新连接建立成功,创建一个Session对象来管理它
                    std::make_shared<Session>(std::move(socket))->start();
                    std::cout << "New client connected!" << std::endl;
                } else {
                    std::cerr << "Accept error: " << ec.message() << std::endl;
                }
                // 继续接受下一个连接
                do_accept();
            });
    }

    tcp::acceptor acceptor_;
};

int main() {
    try {
        boost::asio::io_context io_context;
        Server server(io_context, 8080); // 监听8080端口

        std::cout << "Echo server is running on port 8080..." << std::endl;

        // 运行事件循环,开始处理异步操作
        io_context.run();
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

代码解析

  1. Session: 管理一个独立的客户端连接。它使用async_read_someasync_write进行异步的读和写。当一个操作完成时,它会触发下一个操作,形成一个持续的处理循环。
  2. Server: 使用tcp::acceptor来异步地接受新的客户端连接。每当有新连接时,它就创建一个Session对象并将其启动。
  3. shared_from_this: 这是关键。它确保了在异步操作 pending 时,Session对象不会被意外销毁,防止了悬空指针和内存访问错误。
  4. io_context.run(): 主函数中最重要的调用。它启动了Asio的事件循环,让整个异步机制开始运转。

在这里插入图片描述
执行流程

  • 初始化acceptor, 首先注册accet事件
  • 连接到来accet事件就绪,执行accet回调
  • accet事件内容就是创建一个Session对象, 并且注册一个read事件
  • 对方发送数据, read事件就绪,执行read回调
  • read事件内容就是处理发送的数据,并且生成发送数据,注册发送回调
  • 任务处理完毕,发送数据就绪,执行发送回调

注意生命周期问题

这里的实例代码中也存在一些C++的小细节,比如auto self(shared_from_this), 为什么代码要这么写,熟悉异步操作的肯定一下就能反应过来,Boost.Asio是一个异步框架,也就是说我不知道真正处理回调的时机是什么时候,我只是注册了一个回调函数,我们的回调函数通过lambda表达式进行了注册, 我们传入了this指针,这样就能保证回调函数里面访问的是对应的Session对象,但是由于这里我们外部并没有对Session对象就行生命周期的管理,也就是说在调用accept的回调函数时创建的Session对象会随着栈的析构而析构,所以,为了保证Seesion对象的声明周期

  1. 我们将Session继承一个std::enable_shared_from_this的标准基类,这样,我们就可以创建一个智能指针的副本self并传递给lambda表达式,这样在lambda表达式的作用域的内部,就保存了一份Session,或者说指针,这样由于智能指针计数器>1所以对象不会被析构。(这也是我们Boost官方样例中的做法)
  2. 我们在外部定义一个双端链表管理Session对象, 这样Session对象的声明周期的管理者就成了双端链表,但是,我们这个时候Session对象中还应该保存一份双端链表中自己所在位置的ListNode指针,在Session注册write回调函数里面,通过ListNode指针将Session对象从双端链表中删去,避免内存泄露。

进一步学习

希望这篇博客能帮助你更好了解Boost.Asio

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值