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 > 类:
①继承该类的主要目的是为了让一个对象能够安全地返回自身的"shared_ptr"副本。
②直接使用该类的成员函数shared_from_this()
就能够返回指向自身的 shared_ptr
,并且这个shared_ptr
与外部管理该对象的shared_ptr
共享同一个控制块,因此引用计数是统一的。(外部管理指的是main函数中创建的shared_ptr
对象)
③因此,在回调函数中调用类中的资源就能够得到有效的保障。因为shared_ptr引用计数会+1。
④注意:在调用shared_from_this()
之前,必须已经有一个shared_ptr
管理该对象(如main函数中创建对象就要用shared_ptr
)。否则,行为是未定义的(通常会抛出std::bad_weak_ptr
异常)
<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();//该函数里面调用了accept()函数,而accept()函数中便调用了类的成员_acceptor
//别看这里走了两个函数,但每个函数中的代码量极少,运行后很快就能走到_acceptor
_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后面的代码就开始运行了,而自己类的成员空间和继承的类空间都还没实现完,下文就使用了这些数据(如_acceptor、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);
接口参数:
executor_type :IO上下文,即io_context
endpoint_type :端点,由IP地址+端口号组成
reuse_addr:重用地址,可省略,因为其默认就是true
接口功能:
acceptor类是一个IO操作,允许(服务器)接受什么样的端点。从本例中就可以看出来,指明服务器接受Ipv4类型的ip地址+指定端口号的这种端点进来。
(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);
接口参数:
basic_socket:一个socket套接字类的引用对象
AcceptToken :回调函数,即任务结束的通知函数,官方又叫令牌
<2> 接口解析:
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()
?
在C++中,非静态成员函数都有一个额外的隐式参数,即指向该类对象的指针(this)。因此,当使用boost::bind绑定成员函数时,需要提供这个对象参数。而且必须先传this再传其他显示参数。那为啥又传shared_from_this()而不是this
?
类成员函数accept(),当代码运行到这里后,accept成员函数调用的async_accept()
是异步操作,不会阻塞,也就是说,accept()这个函数会立马继续向下执行,看到代码,很清楚的表明,accept除了async_accept之后,没有任何代码了,也就是程序就会到此为止了。看回main函数,ioc.run()的阻塞保证了最基本tcpserver类的生存期,也就是说只要有IO操作,tcpserver类就不会析构,生存期便一直是存在。
总结就是async_accept()
这个IO操作保证了外部管理的shared_ptr
的生存期。
async_accept()
的回调函数handle_accept()
被调用后,代表async_accept()
IO操作结束,但回调中的do_read()
里面也有IO操作,即async_read():
使用this:
void do_read()
{
boost::asio::async_read(_socket, boost::asio::buffer(_recv_buf, 25),
boost::bind(&tcpserver::handle_read, this, _1, _2));
}
使用shared_from_this():
void do_read()
{
boost::asio::async_read(_socket, boost::asio::buffer(_recv_buf, 25),
boost::bind(&tcpserver::handle_read, shared_from_this(), _1, _2));
}
幸运的是,由于do_read()
里的IO操作,让tcpserver类的生存期又因ioc.run()延长了。 由于tcpserver类是由智能指针管理的,所以传递this并不能使引用计数+1,只能依靠IO操作延长shared_ptr<tcpserver>
的生存期。所以本文的例子,即使不用shared_from_this,直接用this程序是一样没问题的。
即在单线程、操作链连续且生命周期明确控制的简单场景中,使用this是安全的。
那我偏不公有继承boost::enable_shared_from_this< tcpserver >,偏不使用它的成员函数shared_from_this(),而是直接创建一个this
指针的shared_ptr对象
,然后传入这个新的智能指针对象可以吗?
答:不可以。
因为大家很明显就能看出,新的shared_ptr对象是独立的,与原始管理的shared_ptr没有关联,会导致多个独立的shared_ptr
管理同一个对象,从而可能造成多次删除或其他未定义行为。
<3> 本文只是一个最简单的例子,在真正使用Boost过程中,一旦程序复杂,或者多线程任务,再用this
就很容易造成悬挂指针
造成悬挂指针的原因:如果调用reset()
等接口(shared_ptr的成员函数),外部管理的shared_ptr就会立即销毁,若此时正在进行异步操作,传this时,由于唯一的外部对象被销毁,this就成了悬挂指针。
所以用智能指针的优势就在于此,只要传的是shared_from_this(),那么shared_ptr引用计数+1,即便外部管理的shared_ptr被销毁,那么正在进行的异步操作仍然有效。
因此本文在官网的基础上是推荐大家直接使用智能智能指针,方便管理。
总之一句话,类的声明和类的成员函数都用智能指针绝对没错!!!
(3) async_read()与async_read_some()注意事项
使用实例:
boost::asio::async_read(_socket, boost::asio::buffer(_recv_buf, 25),
boost::bind(&tcpserver::handle_read, shared_from_this(), _1, _2));
_socket.async_read_some(boost::asio::buffer(_recv_buf),
boost::bind(&tcpclient::handle_read, shared_from_this(), _1, _2));
官方定义:
DEDUCED async_read(AsyncReadStream & s, const MutableBufferSequence & buffers,
ReadToken && token = DEFAULT, typename constraint< is_mutable_buffer_sequence< MutableBufferSequence >::value >::type = 0);
DEDUCED async_read_some(const MutableBufferSequence & buffers,
ReadToken && token = DEFAULT);
接口参数:
AsyncReadStream :可读的流类型,如socket
MutableBufferSequence :流缓冲区
ReadToken :读回调函数
<1>接口解析:
大家都看到代码截图使用了两个read函数吧,可以看到 async_read 和 async_read_some 的最基本的两个区别:一是 async_read()
是boost::asio里面的接口,而async_read_some()
是socket类的成员函数;二是async_read()
的buffer这个参数必须要指定接收数据的数量,而async_read_some()
不需要。
async_read()函数功能是启动一个异步操作,读取指定数量的字节,直到缓冲区满或遇到EOF
。接收数据时,必须明确缓冲区buffer
的大小,另一端必须发送这么大的数据量,async_read()
才会返回并调用回调。也就是说,async_read()一次就能保证接收到整个缓冲区大小的数据。但缺陷就是如果你没发送这么多的数据,就会阻塞(这里的阻塞是指不会立即调用回调,而不是主进程被阻塞)。
async_read_some()函数功能是启动一个异步操作,从流中读取至少一个字节的数据,最多一次性接收不超过缓冲区大小的数据量
。即这个接口只要有数据就返回,并立即调用回调。
这两个接口各有各的优势:
async_read():
可以指定缓冲区大小,非常适合接收协议头
等固定长度的数据,比如协议头100个字节,那么缓冲区大小设置为101即可。解析完协议头获取到消息体的大小,将消息体的大小设置给缓冲区大小,然后再次调用async_read()
就可以接收可变数据量的消息了。
async_read_some():
适合接收大量的流数据,因为这些数据很难确认它的大小,于是循环调用async_read_some()
,直到接收的数据量达到期望值。
通常两者组合使用最佳:
95% 的情况:优先使用 async_read
简化代码
高性能场景:切换到 async_read_some
精细控制
协议解析:组合使用两者,先用 async_read
读头部,再用 async_read_some
处理流式体
void handle_read(const boost::system::error_code& error, std::size_t bytes)
{
if (error)
{
serv_printf(ERROR, "read error: %s\n", error.message().c_str());
return;
}
/*打印接收到的数据*/
std::cout << "recvlen: " << _recv_buf.size() << "\n" << "recv: " << _recv_buf.data() << std::endl;
/*发送响应*/
_resp_data = "{flag:";
if(_recv_buf.empty())
_resp_data += "failed}\n";
else
_resp_data += "success}\n";
std::cout << "send: " << _resp_data.data() << std::endl;
boost::asio::async_write(_socket, boost::asio::buffer(_resp_data.data(),
_resp_data.size()), boost::bind(&tcpserver::handle_write, shared_from_this(), _1, _2));
}
(4) async_write()注意事项
DEDUCED async_write(AsyncWriteStream & s, const ConstBufferSequence & buffers,
WriteToken && token = DEFAULT, typename constraint< is_const_buffer_sequence< ConstBufferSequence >::value >::type = 0);
接口参数:
AsyncWriteStream :可发送的流类型,如socket
ConstBufferSequence :流缓冲区
WriteToken :回调函数
接口功能:
这个和async_read()
是一对,使用的方式也是类似,需要指定发送字节数。当然,如果buffers的类型是std::vector或者std::string这种连续的容器,是允许缺省大小的,buffers会自适应,将vector或者string的size()成员的值作为缓冲区大小 自动填入的。
如果是数组,则必须指定buffers的大小。
非常建议还是指定大小为好,这是最佳的使用方式。
三、客户端 代码片段讲解+接口解析+易错点解析
main函数代码与服务端一样,async_read和async_write也一样使用,这些都不再解析
1. 任务类 class tcpclient代码(boost_client.h)
void resolve()
{
cli_printf(DEBUG, "resolve\n");
endpoints = _resolver.resolve(TCP_SERVER_HOST, TCP_SERVER_PORT);
connect();
}
类的成员函数_resolver解析地址有两种接口:异步的async_resolve()和同步的resolve():
DEDUCED async_resolve(string_view host, string_view service,
ResolveToken && token = DEFAULT);
results_type resolve(string_view host, string_view service);
接口参数:
host:string类型的ip地址或主机名
service:string类型的端口号或服务名
ResolveToken :回调函数
接口功能:
将地址解析成端点,用于connect。这里用不用异步都可以,用异步就要加个回调函数。没什么区别。
void connect()
{
cli_printf(DEBUG, "connect\n");
boost::asio::async_connect(_socket, endpoints,
boost::bind(&tcpclient::handle_connect, shared_from_this(), _1, _2));
}
DEDUCED async_connect(basic_socket< Protocol, Executor > & s, const EndpointSequence & endpoints,
RangeConnectToken && token = DEFAULT, typename constraint< is_endpoint_sequence< EndpointSequence >::value >::type = 0);
接口参数:
basic_socket:一个socket套接字类的引用对象
EndpointSequence :要连接的端点
RangeConnectToken :回调函数
接口功能:
客户端主动连接一个端点(服务端)
Boost官网上每一个接口都有很多种用法,本文只是讲解其中一种(最常用的),实际有其他需求的可以去官网上查其他接口的用法
。
四、完整代码
1.服务端代码
boost_server.cpp
#include "boost_server.h"
#include <iostream>
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;
}
boost_server.h
#pragma once
#include <hl/version_check/boost_impl.h>
#include <iostream>
#include <boost/asio.hpp>
#include <hl/strand.h>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind/bind.hpp>
#include <string>
#include <vector>
#define DEBUG 3
#define ERROR 5
#define SERVER_PRINTF(pszModeName, pszFmt, ...) \
do{\
fprintf(stderr, "[%s][%s]line[%u] " pszFmt, pszModeName, __func__, __LINE__, ##__VA_ARGS__);\
fflush(stderr);\
}while(0)
#define serv_printf(u32Level, pszFmt, ...) \
do{\
if(u32Level)\
{\
SERVER_PRINTF("server", pszFmt, ##__VA_ARGS__);\
}\
}while(0)
using namespace boost::placeholders;
namespace server
{
namespace tcp_server
{
//定义通信端口
#define SERVER_ADDR "172.16.8.133"
#define COMMUNICATE_PORT 12345
class tcpserver :public hl::strand::cpu, 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();
}
void close()
{
serv_printf(DEBUG, "tcp close\n");
_socket.close();
_acceptor.close();
}
~tcpserver()
{
serv_printf(DEBUG, "bye bye tcp server\n");
}
private:
boost::asio::io_context& _ioc;
boost::asio::ip::tcp::acceptor _acceptor;
boost::asio::ip::tcp::socket _socket;
//std::vector<char> _recv_buf;
std::string _recv_buf;
std::string _resp_data;
void accept()
{
serv_printf(DEBUG, "accept\n");
_acceptor.async_accept(_socket, boost::bind(&tcpserver::handle_accept, 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();
}
void do_read()
{
printf("sizeof(_recv_buf):%d\n", sizeof(_recv_buf));
boost::asio::async_read(_socket, boost::asio::buffer(_recv_buf, 25),
boost::bind(&tcpserver::handle_read, this, _1, _2));
}
void handle_read(const boost::system::error_code& error, std::size_t bytes)
{
if (error)
{
serv_printf(ERROR, "read error: %s\n", error.message().c_str());
return;
}
/*打印接收到的数据*/
std::cout << "recvlen: " << _recv_buf.size() << "\n" << "recv: " << _recv_buf.data() << std::endl;
/*发送响应*/
_resp_data = "{flag:";
if(_recv_buf.empty())
_resp_data += "failed}\n";
else
_resp_data += "success}\n";
std::cout << "send: " << _resp_data.data() << std::endl;
boost::asio::async_write(_socket, boost::asio::buffer(_resp_data.data(), _resp_data.size()),
boost::bind(&tcpserver::handle_write, shared_from_this(), _1, _2));
}
void handle_write(const boost::system::error_code& error, std::size_t bytes_transferred)
{
if (error)
{
serv_printf(ERROR, "write error: %s\n", error.message().c_str());
return;
}
}
};
}
}
2.客户端代码
boost_client.cpp
#include "boost_client.h"
#include <iostream>
int main()
{
boost::asio::io_context ioc;
boost::shared_ptr<client::async_tcp_client::tcpclient> clint_tptr = boost::make_shared<client::async_tcp_client::tcpclient>(ioc);
clint_tptr->start();
ioc.run();
return 0;
}
boost_client.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS //清除ctime的警告
#include <iostream>
#include <boost/enable_shared_from_this.hpp>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/smart_ptr/make_shared.hpp>
#include <hl/version_check/boost_impl.h> //windows
#include <boost/asio.hpp>
#include <hl/strand.h>
#include <boost/bind/bind.hpp>
#include <string>
#include <vector>
#include <ctime>
#define DEBUG 3
#define ERROR 5
#define CLIENT_PIRNTF(pszModeName, pszFmt, ...) \
do{\
fprintf(stderr, "[%s][%s]line[%u] " pszFmt, pszModeName, __func__, __LINE__, ##__VA_ARGS__);\
fflush(stderr);\
}while(0)
#define cli_printf(u32Level, pszFmt, ...) \
do{\
if(u32Level)\
{\
CLIENT_PIRNTF("client", pszFmt, ##__VA_ARGS__);\
}\
}while(0)
using namespace boost::placeholders;
/*
1.如果使用make_shared来构造类的实例时,类的构造函数不能出现函数接口的入口,防止生存期问题。
*/
namespace client
{
std::string make_time()
{
std::time_t now = std::time(0);
return ctime(&now);
}
namespace async_tcp_client
{
#define TCP_SERVER_HOST "172.16.8.133"
#define TCP_SERVER_PORT "12345"
class tcpclient:public hl::strand::cpu, public boost::enable_shared_from_this<tcpclient>
{
public:
tcpclient(boost::asio::io_context& ioc) :_ioc(ioc), _socket(_ioc), _resolver(_ioc)
{
_recv_buf.resize(512);
}
~tcpclient()
{
cli_printf(DEBUG, "bye bye tcp client\n");
}
void start()
{
resolve();
}
void stop()
{
cli_printf(DEBUG, "tcp stop\n");
_socket.close();
return;
}
private:
boost::asio::io_context& _ioc;
boost::asio::ip::tcp::socket _socket;
boost::asio::ip::tcp::resolver _resolver;
std::vector<char> _recv_buf;
//std::vector<char> _send_buf;
//std::string _recv_buf;
std::string _send_buf;
boost::asio::ip::tcp::resolver::results_type endpoints;
void resolve()
{
cli_printf(DEBUG, "resolve\n");
endpoints = _resolver.resolve(TCP_SERVER_HOST, TCP_SERVER_PORT);
connect();
}
void connect()
{
cli_printf(DEBUG, "connect\n");
boost::asio::async_connect(_socket, endpoints,
boost::bind(&tcpclient::handle_connect, shared_from_this(), _1, _2));
}
void handle_connect(const boost::system::error_code& error, const boost::asio::ip::tcp::endpoint& endpoint)
{
if (error)
{
cli_printf(ERROR, "connect error: %s\n", error.message().c_str());
return;
}
cli_printf(DEBUG, "server ip: %s\n", endpoint.address().to_string().data());
/*发送数据*/
std::string str(client::make_time());
std::cout << "sendlen: " << str.size() << "\n" << "send: " << str.data() << std::endl;
boost::asio::async_write(_socket, boost::asio::buffer(str.data(), str.size()),
boost::bind(&tcpclient::handle_write, shared_from_this(), _1, _2));
}
void handle_write(const boost::system::error_code& error, std::size_t bytes)
{
if (error)
{
cli_printf(ERROR, "write error: %s\n", error.message().c_str());
return;
}
cli_printf(DEBUG, "send success!\n");
_socket.async_read_some(boost::asio::buffer(_recv_buf),
boost::bind(&tcpclient::handle_read, shared_from_this(), _1, _2));
}
void handle_read(const boost::system::error_code& error, std::size_t bytes)
{
if (error)
{
cli_printf(ERROR, "read error: %s\n", error.message().c_str());
return;
}
/*打印接收数据*/
std::cout << "recvlen: " << _recv_buf.size() << "\n" << "recv: " << _recv_buf.data() << std::endl;
}
};
}
}