【C++】BOOST ASIO 异步客户端代码分析

参考资料:《Boost Asio C++网络编程》第四章

这里将对asio库编写的异步客户端进行解读。异步客户端比同步客户端更复杂。同步客户端相对简单,实用性不大,一般用于业务测试,基本就是一条线的逻辑代码。

环境设置

asio客户端项目仅需包含boost.asio头文件,无需导入lib文件:

#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/noncopyable.hpp> // 原文代码缺了这行

using namespace boost::asio;
io_context service;

这里对初学者陌生的是enable_shared_from_this,后面将进行解析。io_context 是asio框架中的调度器,所有异步io事件都是通过它来分发处理的,有个旧版本是io_service(也能用,用法一样)。io_context本身是单线程运作的。在多线程项目中,几个线程可以独享一个io_context,每个io_context均可以进行单独的异步操作。一般小规模业务中,采用的是一个io_context加异步/协程。大规模业务和大数据传输业务,需要开启多线程多io_context。

主函数
int main(int argc, char* argv[]) {
	ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 8001);
	const char* names[] = { "John", "James", "Lucy", "Tracy", "Frank", "Abby",0 };
	for (const char ** name = names; *name; ++name) {
		talk_to_svr::start(ep, *name);
		boost::this_thread::sleep(boost::posix_time::millisec(100));
	}
	service.run();
}

主函数非常简单,首先ip::tcp::endpoint是asio库中表示tcp端口的类,指定连接主机地址和连接方式;ip::address::from_string可以直接把字符串ip地址和端口变为ip地址数据;‘127.0.0.1’表示本机ip,做测试用的。
const char* names[] = {。。。}初始化字符串数组,其中原文缺少"const"会报错,因为右边的字符串是不可更改的,带有const属性;字符串初始的最后一个是0,必须加上,在下面的for循环代码中作为终止条件。
talk_to_svr是之后要介绍的客户端类;talk_to_svr::start是客户端自定义的静态成员函数,不是asio的api,用于生成一个talk_to_svr对象并启动。
上述工作完成后,io_context.run()便开始执行异步调度工作,只要asio的api还有处于工作状态的,io_context.run()便会一直循环下去;否则就会退出循环,结束程序。因此实际程序中,我们总是要保留至少一个客户端对象,使其处于准备异步连接的状态,保证程序一直处于工作中。

客户端

客户端属性
class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr>
	, boost::noncopyable{
	typedef talk_to_svr self_type;
  1. 根据业务逻辑,客户端类是可不拷贝的(noncopyable),每个客户端应该独一无二。
  2. 客户端类是异步的,当处于异步操作中,客户端类有可能莫名地被析构,因此要使用智能指针来管理客户端类,后面我们将看到这个属性的必要性。
两个start
#define MEM_FN(x) boost::bind(&self_type::x, shared_from_this())
#define MEM_FN1(x,y) boost::bind(&self_type::x, shared_from_this(),y)
void start(ip::tcp::endpoint ep) {
		sock_.async_connect(ep, MEM_FN1(on_connect, _1));
	}
static ptr start(ip::tcp::endpoint ep, const std::string & username) {
		ptr new_(new talk_to_svr(username));
		new_->start(ep);
		return new_;
	}

下面的静态成员函数start生成一个对象,再调用成员函数start开始工作,调用async_connect进入等待连接状态,此时客户端对象进入异步等待状态,占用CPU资源极少。
async_connect的参数列表,第一个是端口,第二个是回调函数,指向成员函数void talk_to_svr::on_connect(const boost::system::error_code & err) ,当异步连接成功时,就会唤醒并调用这个成员函数。boost::system::error_code定义了几种常见的系统错误代码用于异常处理。如果没有错误,on_connect函数就开始调用工作函数。
MEM_FN1(on_connect, _1)宏命令解析为boost::bind(talk_to_svr::on_connect, shared_from_this(), _1),_1是bind函数所用的占位符,async_connect会在_1处填入:error_code数据。shared_from_this()返回一个包含自身指针的智能指针,只有继承了enable_shared_from_this<talk_to_svr>的类才能这样用(已经加入标准库了)。下面来看看为什么要用这个方法。

enable_shared_from_this

以start成员函数为例子,根据其外在表现,可以类比下面的形式便于理解(实际实现更复杂):

void start(talk_to_svr* this, ip::tcp::socket sock, ip::tcp::endpoint ep){
	std::thread t(thread_connect, sock, ep, this, bind(talk_to_svr::callback,XXX));
	t.detach();//不用join阻塞,直接跑完这个代码段
}
void thread_connect(ip::tcp::socket, ip::tcp::endpoint , talk_to_svr* this, std::function<void(talk_to_svr*,error_code)> f){
	....//连接工作
	(*f)(this, ec);
}
void callback(talk_to_svr* this, error_code arg){....}

std::function是函数指针。异步连接的外在表现,可以等价于新建了一个线程用于连接,线程有detach属性,所以当前代码段直接运行完毕退出。等线程执行完毕,又回调后续的处理函数。在线程运行阶段,线程函数只有talk_to_svr的裸指针,如果talk_to_svr对象析构了,那么这个指针就会指向异常地址而发生内存错误。为了保证talk_to_svr对象指针在工作期间始终有效,我们将talk_to_svr类继承boost::enable_shared_from_this属性,这样成员函数的第一个参数*this,可以用std::shared_ptr<talk_to_svr> this替代;然后将std::shared_ptr<talk_to_svr> this传入异步工作区中,保证talk_to_svr对象始终是存在的。

长连接

由于使用了tcp连接,可以保持长时间的连接。实际工作中,talk_to_svr一直处于以下工作状态:
async_write_some异步等待写入,登录----》async_read_some异步等待读取,获取结果—》async_write_some异步等待写入,发送请求-----》async_read_some异步等待读取,获取请求结果-----》反复重复或者关闭连接。
所有的操作指令和数据都转换为字符串进行传递,然后解析收到的回应。剩下的其他工作就与asio无关了,具体业务调用具体的库。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章