C++ 网络编程(15) 利用asio协程搭建异步服务器

发布于:2025-07-06 ⋅ 阅读:(17) ⋅ 点赞:(0)

🚀 [协程与异步服务器实战]:[C++20协程原理与Boost.Asio异步服务器开发]
📅 更新时间:2025年07月05日
🏷️ 标签:C++20 | 协程 | Boost.Asio | 异步编程 | 网络服务器


前言

今天我们学习协程的基本概念,以及如何用协程来搭建一个简单的异步服务器来进行与客户端的收发数据


一、什么是协程?

协程(Coroutine)是一种比线程更轻量级的“并发编程”方式
它允许你在一个线程内,把任务分成多个可以挂起和恢复的小段,写出“像同步一样的异步代码”。

协程的特点
可以在执行过程中主动暂停(挂起),等条件满足后恢复执行
多个协程可以在同一个线程内切换,切换速度非常快
由程序员或框架调度,而不是操作系统

总结
协程就是可以随时挂起和恢复的轻量级任务,让你用很少的资源实现高效的并发和异步编程

二、线程与协程?

直观比喻
线程像“多个人各自做事”
协程像“一个人做多件事,可以随时暂停当前任务,去做别的,再回来继续”

三、使用协程搭建异步服务器进行通信

1.服务端

1.流程图

在这里插入图片描述

1.程序启动(main)
初始化 io_context
设置信号处理(监听 Ctrl+C 等信号,优雅退出)
启动 lisenter 协程

2.lisenter 协程
创建 acceptor,监听 10086 端口
进入无限循环:
异步等待新连接(co_await acceptor.async_accept()
每有一个新连接,启动一个 echo 协程处理该连接

3.echo 协程
进入无限循环:
异步读取客户端数据(co_await socket.async_read_some()
异步写回数据(co_await async_write()
信号处理
收到终止信号时,调用 ioc.stop(),优雅关闭服务器

2.服务端代码

1.主函数入口

我们在主函数中利用 try catch 来进行跑代码,这样可以防止后续的出错,
我们先定义一个信号集signal_set来实现服务器的优雅退出,当客户端使用Ctrl+C等操作的时候,我们可以通知 上下文 io_context直接调用 .stop() 来暂停服务

int main()
{
    try
    {
        boost::asio::io_context ioc(1);
        boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
        signals.async_wait([&](auto,auto)
            {
                ioc.stop();
            });
        co_spawn(ioc,lisenter(),detached);//启动协程
        ioc.run();

    }
    catch(std::exception& e)
    {
        std::cout << "main Exception is" << e.what() << std::endl;
    }
}

在写 Lambda 表达式的时候如果要调用上下文io_context必须用&捕获
核心原因是:io_context(以及很多 Asio 相关对象)本身禁止拷贝,只能被引用捕获,不能被值捕获

co_spawn(ioc,lisenter(),detached);//启动协程

co_spawn
co_spawn表示启动一个协程,参数分别为调度器执行的函数,以及启动方式, 比如我们启动了一个协程,deatched表示将协程对象分离出来,这种启动方式可以启动多个协程,他们都是独立的,如何调度取决于调度器,在用户的感知上更像是线程调度的模式,类似于并发运行,其实底层都是串行的

此时需要传入三个参数
第一个参数
boost::asio::io_context
所有异步事件和协程都要绑定到某个 io_context,它负责调度和执行

第二个参数
是你自定义的协程函数,返回类型通常是 awaitable<void>
这里我们自定义的函数是lisenter()

第三个参数
协程的完成方式
总共有三种,分别是
detached
作用:协程分离运行,主程序不关心协程的返回值和异常。
用法:适合“只管启动,不关心后续”的场景(如服务器监听、后台任务)

use_awaitable
作用:让协程的结果可以被 co_await 等待,用于协程之间的嵌套和组合。
用法:适合你想在另一个协程里等待这个协程的结果

最后一种是自定义的回调函数

所以我们这句话

co_spawn(ioc,lisenter(),detached);//启动协程

的完整意思就是
在 ioc 这个事件循环中,启动一个 lisenter 协程,让它自己运行,主程序不关心它的结果

2.lisenter 协程函数

我们定义一个协程函数,然后在协程函数中我们创建一个监听器acceptor进行绑定上下文tcp协议端口号

然后我们调用一个死循环,内部异步的进行监听然后创建一个socket,然后我们根据这个socket再创建一个协程echo单独管理此客户端的通信

awaitable<void> lisenter()
{
    auto executor = co_await this_coro::executor;//co_await异步获取调度器
    tcp::acceptor acceptor(executor, { tcp::v4(),10086 });

    for (;;)
    {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
        co_spawn(executor, echo(std::move(socket)), detached);//为每一个连接单独启动 一个协程进行收发数据
    }
}

如果要写协程函数,必须是这种类型

awaitable<T>

我们在协程中进行了对当前协程获取调度器的实现

auto executor = co_await this_coro::executor;//co_await异步获取调度器

Boost.Asio 中,executor 是一个“执行环境”,负责调度和管理异步操作的执行
常见的 executorio_context::executor_type,它和 io_context 绑定

因为我们在主函数中使用了

co_spawn(ioc, lisenter(), detached)

启动协程时,协程会自动和 ioc 绑定
但在协程体内,如果你要创建新的异步对象(如 acceptor),需要明确告诉它用哪个 executor,否则它不知道该和哪个事件循环关联
所以相当于给这个监听器绑定了一个上下文io_context

然后我们再来介绍一下
co_await
co_awaitC++20 协程的通用关键字,它的作用是等待一个 “可等待对象” 完成,并获取其结果

比如这里我们用来获取当前协程的调度器和监听器分配的socket

auto executor = co_await this_coro::executor;
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
3.echo协程函数

这个协程函数就是单独为当前分配的客户端进行读写通信的实现

awaitable<void>echo(tcp::socket socket)
{
    try
    {
        char data[1024];
        for (;;)
        {
            std::size_t n=co_await 
            socket.async_read_some(boost::asio::buffer(data), use_awaitable);
            
            co_await async_write
            (socket, boost::asio::buffer(data, n), use_awaitable);
        }
    }
    catch (std::exception& e)
    {
        std::cout << "echo Exception is" << e.what() << std::endl;
    }
}

我们多次利用这个co_await实现了将看似同步的代码,实现了异步等待的操作,比如这句

//获取收到数据长度
std::size_t n=co_await 
socket.async_read_some(boost::asio::buffer(data), use_awaitable);

//异步写
co_await async_write
(socket, boost::asio::buffer(data, n), use_awaitable);

2.客户端

客户端我们还是用以前的简易的版本,发送 hello world 进行测试,不考虑其他的问题

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

using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024;


int main()
{
    try
        
    {
        boost::asio::io_context ioc;
        tcp::endpoint remote_ep(boost::asio::ip::make_address("127.0.0.1"), 10086);
        tcp::socket sock(ioc);
        boost::system::error_code error = boost::asio::error::host_not_found;
        sock.connect(remote_ep, error);
        if (error)
        {
            cout << "connect failed, code is" << error.value() <<"  error msg is "<<error.what() << endl;
            return 0;
        }


        cout << "Enter Message:" << endl;
        char request[MAX_LENGTH];
        cin.getline(request, MAX_LENGTH);
        size_t request_len = strlen(request);
        boost::asio::write(sock, boost::asio::buffer(request, request_len));

        char reply[MAX_LENGTH];
        size_t reply_len = boost::asio::read(sock, boost::asio::buffer(reply, request_len));

        cout << "reply is" << string(reply,reply_len) << endl;
        getchar();
    }
    catch (std::exception& e)
    {
        std::cout << "main exception is " << e.what() << std::endl;
    }

    return 0;
}



四、测试

客户端成功与服务器进行通信
在这里插入图片描述

总结

学习了协程了相关概念,以及如何利用协程来搭建一个简易的异步服务器的小demo


网站公告

今日签到

点亮在社区的每一天
去签到