boost::asio协程实现并发服务器

394 阅读6分钟

1.简介:利用协程实现并发程序有两个好处

1   将回调函数改写为顺序调用,提高开发效率。
2   协程调度比线程调度更轻量化,因为协程是运行在用户空间的,线程切换需要在用户空间和内核空间切换。

#include<boost/asio/co_spawn.hpp> 提供了 `co_spawn` 函数,该函数用于启动协程。
#include<boost/asio/detached.hpp> 提供了 `detached` 类型,用于指示 `co_spawn` 不等待协程的完成。
#include<boost/asio/io_context.hpp> -   提供了 `io_context` 类,它是 Boost.Asio 的核心,用于驱动异步操作。
-   ... 其他与 TCP 和信号相关的头文件。
#include<boost/asio/ip/tcp.hpp>
#include<boost/asio/signal_set.hpp>
#include<boost/asio/write.hpp>

#include <iostream>
using boost::asio::ip::tcp;
using boost::asio::awaitable;//在 Boost.Asio 的协程中,`awaitable` 是用于表示异步操作的可等待对象。
using boost::asio::co_spawn;//启动协程关键字
using boost::asio::detached;
using boost::asio::use_awaitable;//异步可等待,它通常用于指定一个操作应返回一个 `awaitable` 对象,从而使其可等待。
namespace this_coro = boost::asio::this_coro;//返回当前协程所执行的环境,它提供了对当前协程的引用。这使得您可以在协程内部访问和修改协程的状态。
`this_coro::yield` 通常用于在协程中暂停执行,直到某个条件为真或某个异步操作完成。它允许其他协程或异步操作在当前协程恢复之前运行。

1.服务器echo代码

//
#include<boost/asio/co_spawn.hpp>
#include<boost/asio/detached.hpp>
#include<boost/asio/io_context.hpp>
#include<boost/asio/ip/tcp.hpp>
#include<boost/asio/signal_set.hpp>
#include<boost/asio/write.hpp>

#include <iostream>
using boost::asio::ip::tcp;
using boost::asio::awaitable;//异步等待
using boost::asio::co_spawn;//启动协程关键字
using boost::asio::detached;
using boost::asio::use_awaitable;//异步可等待
namespace this_coro = boost::asio::this_coro;//返回当前协程所执行的环境

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 (const std::exception& e)
	{
		std::cout << "echo  exception is " << e.what() << std::endl;
	}
}
awaitable<void> listen() {//为了让协程中调用,设置可等待
	auto executor = co_await this_coro::executor;//这个作用域下有调度器,然后返回存到局部变量,co_await异步查这个调度器
	//查不到会挂起,把使用权交给其它主线程中协程
	tcp::acceptor acceptor(executor, { tcp::v4(),10086 });//接收操作在协程内部执行,acceptor
	for (;;) {//协程最大好处是把异步变成同步
		tcp::socket socket = co_await acceptor.async_accept(use_awaitable);//co_await相当于阻塞等待接收.释放主动权交给cpu
		//主线程继续往下持行,调用其它协程。use_awaitable告诉asio要使用协程,使这函数变成可等待
		co_spawn(executor, echo(std::move(socket)), detached);//再启动一个协程,移动构造socket,原socket失效
	}
}
//底层事件循环通过executor派发的
int main()
{
	try
	{
		boost::asio::io_context io_context(1);
		boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
		signals.async_wait([&](auto,auto) {
			io_context.stop();
			});
		co_spawn(io_context, listen(),detached);//启动协程,上下文里面调度器与协程绑定,协程的调度交给上下文的调度器调动
		//然后与其它协程分离独立执行
		//协程没找到调度器会挂起,往下走,io调度,找到调度器后,唤醒协程
		//协程的持行器说白了就是io_contest
		io_context.run();
	}
	catch (const std::exception& e)
	{
		std::cout << " Exception is " << e.what() << std::endl;
	}
}

解释:

1   我们用awaitable声明了一个函数,那么这个函数就变为可等待的函数了,比如被添加之后,就可以被协程调用和等待了。
2   表示启动一个协程,参数分别为调度器,执行的函数,以及启动方式, 比如我们启动了一个协程,deatched表示将协程对象分离出来,这种启动方式可以启动多个协程,他们都是独立的,如何调度取决于调度器,在用户的感知上更像是线程调度的模式,类似于并发运行,其实底层都是串行的。listener``awaitable<void>``co_spawn

co_spawn(io_context, listener(), detached);

我们启动了一个协程,执行listener中的逻辑,listener内部co_await 等待 acceptor接收连接,如果没有连接到来则挂起协程。执行之后的逻辑。所以协程实际上是在一个线程中串行调度的,只是感知上像是并发而已。
3   当acceptor接收到连接后,继续调用co_spawn启动一个协程,用来执行echo逻辑。echo逻辑里也是通过co_wait的方式接收和发送数据的,如果对端不发数据,执行echo的协程就会挂起,另一个协程启动,继续接收新的连接。当没有连接到来,接收新连接的协程挂起,如果所有协程都挂起,则等待新的就绪事件(对端发数据,或者新连接)到来唤醒。io_context.run()

简单客户端测试代码

#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(address::from_string("127.0.0.1"), 10086);
		tcp::socket socket(ioc);
		boost::system::error_code error = boost::asio::error::host_not_found;
		socket.connect(remote_ep,error);
		if (error)//连错了非0
		{
			std::cout << " connect failed code is " << error.value() << "msg error is" << error.message() << std::endl; 
			return 0;
		}
		cout << "Enter message:";
		char request[MAX_LENGTH];
		std::cin.getline(request, MAX_LENGTH);
		size_t request_length = strlen(request);
		boost::asio::write(socket, boost::asio::buffer(request, request_length));
		char reply[MAX_LENGTH];
		size_t reply_length = boost::asio::read(socket, boost::asio::buffer(reply, request_length));
		cout << "reply is " << string(reply, reply_length) << endl;
		getchar();
	}
	catch (const std::exception& e)
	{
		std::cout << " Exception is " << e.what() << std::endl;
	}
}

1.改进服务器

我们可以利用协程改进服务器编码流程,用一个iocontext管理绑定acceptor用来接收新的连接,再用一个iocontext或以IOServicePool的方式管理连接的收发操作,在每个连接的接收数据时改为启动一个协程,通过顺序的方式读取收到的数据

void CSession::Start() {
    auto shared_this = shared_from_this();
    //开启接收协程
    co_spawn(_io_context, [=]()->awaitable<void> {
    try {
    for (;!_b_close;) {
    _recv_head_node->Clear();
    std::size_t n = co_await boost::asio::async_read(_socket,
    boost::asio::buffer(_recv_head_node->_data, HEAD_TOTAL_LEN),
    use_awaitable);
    if (n == 0) {
    std::cout << "receive peer closed" << endl;
    Close();
    _server->ClearSession(_uuid);
    co_return;
}
    //获取头部MSGID数据
    short msg_id = 0;
    memcpy(&msg_id, _recv_head_node->_data, HEAD_ID_LEN);
    //网络字节序转化为本地字节序
    msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);
    std::cout << "msg_id is " << msg_id << endl;
    //id非法
    if (msg_id > MAX_LENGTH) {
    std::cout << "invalid msg_id is " << msg_id << endl;
    _server->ClearSession(_uuid);
    co_return;
    }
    short msg_len = 0;
    memcpy(&msg_len, _recv_head_node->_data + HEAD_ID_LEN, HEAD_DATA_LEN);
    //网络字节序转化为本地字节序
    msg_len = boost::asio::detail::socket_ops::network_to_host_short(msg_len);
    std::cout << "msg_len is " << msg_len << endl;
    //长度非法
    if (msg_len > MAX_LENGTH) {
    std::cout << "invalid data length is " << msg_len << endl;
    _server->ClearSession(_uuid);
    co_return;
    }
    _recv_msg_node = make_shared<RecvNode>(msg_len, msg_id);
    //读出包体
    n = co_await boost::asio::async_read(_socket,
    boost::asio::buffer(_recv_msg_node->_data, _recv_msg_node->_total_len), use_awaitable);
    if (n == 0) {
    std::cout << "receive peer closed" << endl;
    Close();
    _server->ClearSession(_uuid);
    co_return;
    }
    _recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
    cout << "receive data is " << _recv_msg_node->_data << endl;
    //投递给逻辑线程
    LogicSystem::GetInstance().PostMsgToQue(make_shared<LogicNode>(shared_from_this(), _recv_msg_node));
    }
    }
    catch (std::exception& e) {
    std::cout << "exception is " << e.what() << endl;
    Close();
    _server->ClearSession(_uuid);
}
    }, detached);
}