【C++ Boost】一个最基本的异步boost async tcp 服务/客户端代码的深刻解析,一文解决所有接口的用法以及一些容易出错的点

一、平台的选择以及基本构建方法

1.官网链接

https://www.boost.org/doc/libs/1_80_0/doc/html/boost_asio.html
本文代码是以官方实例代码做的一些优化

2.平台选择

    Boost 最令人惊艳的地方有两个:一是支持跨平台,即windows和linux下的接口代码都是一样的;二是支持异步操作,即可以让read和write操作不阻塞。
    因此,本文章平台选择 windows+VS2019

3.Boost库下载

    要想windows系统使用 Boost 接口,首先需要从官网下载Boost库(即所有接口函数的头文件)
在这里插入图片描述
在这里插入图片描述
    本文章不过多展示下载Boost库的详细过程。

4.构建方法

    官网有详细的构建说明,这里只列结果。
    (1)windows平台必须包含 #include <hl/version_check/boost_impl.h>
    (2)VS2019(任意版本都行)在 项目/属性/链接器/命令行 中添加 -DBOOST_ALL_NO_LIB
备注:示情况而定,有些VS即使不用(2)也行,另有报错自行分析。

二、服务端 代码片段讲解+接口解析+易错点解析

    大家可以先复制(四)的全部代码边看代码边看解析
    代码分为两个部分:启动的boost_server.cpp文件和实现的boost_server.h文件

1.main函数代码(boost_server.cpp)

 int main()
 {
   
 	boost::asio::io_context ioc;
	boost::shared_ptr<server::tcp_server::tcpserver> serv_tptr = boost::make_shared<server::tcp_server::tcpserver>(ioc);
	serv_tptr->start();
	ioc.run();//阻塞

	return 0;
}

    (1)boost::asio::io_context 直译为IO上下文,是封装了底层IO操作的一个接口类。这个IO上下文的作用就是将所有的IO操作(read,write,connect等等)交付给底层(操作系统)的IO去处理。所以,boost::asio::io_context的对象 ioc 生存期必须是大于所有的IO操作的,即一般大于整个 任务类 的生存期
    (2)serv_tptr是一个智能指针包裹的对象,这里建议大家将 任务类(类tcpserver) 的对象都以智能指针的形式包裹,因为智能指针的特性是只有最后一个引用的对象被析构时,内存才会被释放,这样 任务类 的生存期就得到了最基本的保障
    (3)这里的start函数接口有一个非常重要的作用,放到下面的任务类代码中讲。
    (4)由于main函数只有短短几句话,其生存期很快就结束,造成 任务类 中的操作还没有完成就结束了整个进程,故必须要使用 ioc.run() 成员函数让IO操作阻塞,等待所有的IO操作都结束时才返回

2. 任务类 class tcpserver 代码(boost_server.h)

(1)构造函数及启动函数start
		class tcpserver : public boost::enable_shared_from_this<tcpserver>
		{
   
		public:
			tcpserver(boost::asio::io_context& ioc) :_ioc(ioc), _socket(ioc),
				_acceptor(ioc, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), COMMUNICATE_PORT))
			{
   
				_recv_buf.resize(64);
			}

			void start()
			{
   
				accept();
			}

    <1> 继承 boost::enable_shared_from_this < tcpserver > 类的原因是确保在自己的类中调用自己的类接口(即tcpserver类调用tcpserver类的接口)时拥有足够长的生存期, boost::enable_shared_from_this类有一个成员函数shared_from_this() 可以返回 tcpserver 类的对象的智能指针,这样每调用一次shared_from_this都能确保tcpserver 类的生存期是有的。
    <2> 易错点 构造函数 tcpserver(…):…{ … } 中的代码块中不允许有任何操作函数在里面。如下面例子这样:

			tcpserver(boost::asio::io_context& ioc) :_ioc(ioc), _socket(ioc),
				_acceptor(ioc, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), COMMUNICATE_PORT))
			{
   
				start();
				_recv_buf.resize(64);
			}

    之所以不能将任何操作函数放在 类tcpserver 的构造函数中,是因为在生成 任务类对象的时候是使用的智能指针,即main中的boost::shared_ptr<server::tcp_server::tcpserver> serv_tptr = boost::make_shared<server::tcp_server::tcpserver>(ioc).make_shared 构造 tcpserver类 时需要一段时间(构造成员函数,继承,成员赋值等等操作需要时间),如果将 start函数放在构造函数中,当构造运行之后,start后面的代码就开始运行了,而自己类的成员空间和继承的类空间都还没实现完,下文就使用了这些数据(如shared_from_this),就会造成weak_ptr的错误
    <3> boost::asio::ip::tcp::acceptor类的使用。Boost封装的acceptor类和C语言的accept的概念是一致的,使用的方法也一致。

basic_socket_acceptor(const executor_type & ex, const endpoint_type & endpoint,
    	bool reuse_addr = true);

接口参数:
    ex:IO上下文,即io_context
    endpoint:端点,由IP地址+端口号组成
    reuse_addr:重用地址,可省略,因为其默认就是true
接口功能:
    acceptor类是一个IO操作,允许(服务器)接受什么样的端点。那么本例中就可以看出来,指明服务器接受Ipv4类型的ip地址+指定端口号的这种端点进来。COMMUNICATE_PORT是自己任意设置的如“12345”

(2) 接受函数accept()的解析以及shared_from_this的核心问题
void accept()
{
   
	serv_printf(DEBUG, "accept\n");
	_acceptor.async_accept(_socket, boost::bind(&tcpserver::handle_accept, shared_from_this(), _1));
}
void handle_accept(const boost::system::error_code& error)
{
   
	if (error)
	{
   
		serv_printf(ERROR, "accept error: %s\n", error.message().c_str());
		return;
	}

	serv_printf(DEBUG, "client ip: %s\n", _socket.remote_endpoint().address().to_string().data());
	do_read();
}

    <1> acceptor类支持异步接受器,即该函数不阻塞,直接运行后面的代码,将接受任务交付给io_context这个上下文调度器,在未来一个合适的时机去执行,执行完毕后通过token回调来通知到我们任务是否完成

DEDUCED async_accept(basic_socket< Protocol1, Executor1 > & peer, AcceptToken && token = DEFAULT,
     typename constraint< is_convertible< Protocol, Protocol1 >::value >::type  = 0);

接口参数:
    peer:一个socket套接字类的引用对象
    token:回调函数,即任务结束的通知函数,官方又叫令牌
接口功能:
    async_accept()是acceptor类的一个成员函数,其官方定义的第二个参数为回调函数,并且规定这个回调函数有一个参数error,这个error可以返回async_accept的操作失败原因,并可以通过printf函数打印出来
    当类的函数接口需要调用自己类的函数接口时,难免要传入参数,就比如我们这里的error, 官方推荐我们使用boost::bind(),使用它的原生定义std::bind()也同样可以。大家注意到,这里的bind()给函数指针tcpserver::handle_accept绑定了两个参数,一个为shared_from_this,一个为_1(bind占位符,其实就是指boost::system::error_code error,可以直接写入这个完整表达式)。
    读者看下文的handle_accept()函数就可以知道,这个函数仅有一个参数,那为啥bind还需要绑定shared_from_this因为当类成员调用的也是类成员,就需要传个类指针this,即其实是这样调用的this->handle_accept()那又为啥又传shared_from_this而不是this
    类成员函数accept(),当代码运行到这里后,accept成员函数调用的async_accept是异步操作,不会阻塞,也就是说,accept()这个函数会立马继续向下执行,看到代码,很清楚的表明,accept除了async_accept之后,没有任何代码了,也就是程序就会到此为止了。看回main函数,ioc.run()的阻塞最基本保证了tcpserver类的生存期,也就是说只要有IO操作,tcpserver类就不会析构,生存期是有的。那么async_accept这个IO操作的任务handle_accept()运行完之后紧接着就是do_read()

void do_read()
{
   
	boost::asio::async_read(_socket, boost::asio::buffer(_recv_buf, 25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值