系列文章目录:C++ asio网络编程-CSDN博客
在上一节中,我们实现了客户端与服务器之间的同步读写,在实际开发中,这套方案是行不通的,从这一小节开始来介绍异步读写的的写法。
1、准备工作
定义一个session类,这个session类表示服务器处理客户端连接的管理类,在构造函数中传入一个socket对象,定义一个成员函数connect,参数为要连接的端点。
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
void connect(const boost::asio::ip::tcp::endpoint& ep);
private:
std::shared_ptr<boost::asio::ip::tcp::socket> _socket;
};
Session::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket)
: _socket(socket)
{
}
void Session::connect(const boost::asio::ip::tcp::endpoint& ep)
{
_socket->connect(ep);
}
再封装一个node结构,用来管理要发送和接收的数据,该结构包含数据域首地址,数据的总长度,以及已经处理的长度(已读的长度或者已写的长度)。写了两个构造函数,两个参数的负责构造写节点,一个参数的负责构造读节点。
// 固定接受字符串长度(为了演示方便,先这样写)
const int RECVSIZE = 1024;
// 消息数据
class MsgNode {
public:
// 作为发送结点
MsgNode(const char* msg, int total_len) : _total_len(total_len), _cur_len(0) {
_msg = new char[total_len];
memcpy(_msg, msg, total_len);
}
// 作为接收结点
MsgNode(int total_len) : _total_len(total_len), _cur_len(0) {
_msg = new char[total_len];
}
~MsgNode() {
delete[] _msg;
}
int getTotalLen() {
return this->_total_len;
}
int getCurLen() {
return this->_cur_len;
}
void setCurLen(int curLen) {
this->_cur_len = curLen;
}
char* getMsg() {
return this->_msg;
}
private:
int _total_len; // 总长度
int _cur_len; // 当前已发送或已接收数据的长度
char* _msg; // 消息首地址
};
2、异步写操作
(1)async_write_some
接下来为Session添加异步写操作和负责发送写数据的节点,为什么是Err后缀,后面再解释。首先在Session类中加一个成员变量_send_node,表示要发送的消息
std::shared_ptr<MsgNode> _send_node;
然后加上成员函数:
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
void connect(const boost::asio::ip::tcp::endpoint& ep);
void writeCallBackErr(const boost::system::error_code& ec, std::size_t bytes_transferred,
std::shared_ptr<MsgNode>);
void writeToSocketErr(const std::string buf);
private:
std::shared_ptr<boost::asio::ip::tcp::socket> _socket;
};
void Session::writeCallBackErr(const boost::system::error_code& ec,
std::size_t bytes_transferred, std::shared_ptr<MsgNode> msg_node)
{
if (bytes_transferred + msg_node->getCurLen() < msg_node->getTotalLen()) {
_send_node->setCurLen(_send_node->getCurLen() + bytes_transferred);
this->_socket->async_write_some(
boost::asio::buffer(_send_node->getMsg() + _send_node->getCurLen(),
_send_node->getTotalLen() - _send_node->getCurLen()),
std::bind(
&Session::writeCallBackErr,
this,
std::placeholders::_1,
std::placeholders::_2,
_send_node
)
);
}
}
void Session::writeToSocketErr(const std::string buf)
{
this->_send_node = std::make_shared<MsgNode>(buf.c_str(), buf.length());
this->_socket->async_write_some(
boost::asio::buffer(_send_node->getMsg(), _send_node->getTotalLen()),
// 绑定回调函数
std::bind(
&Session::writeCallBackErr, // 回调函数
this, // 当前对象实例
std::placeholders::_1, // 占位符,回调函数的第一个参数
std::placeholders::_2, // 占位符,回调函数的第二个参数
_send_node // 传递 MsgNode 对象的共享指针
)
);
}
在writeToSocketErr函数中,先构造_send_node,然后调用async_write_some方法。关于async_write_some方法的参数,我们可以点进源码看一下(c++基础不太好的同学可以先跳过下面这一段,先掌握基本的使用即可,也可以先学一下std::placeholders,这样会好理解很多)
第一个参数其实就是一个buffer,直接通过boost::asio::buffer函数去构造就可以了,第二个参数为WriteToken类型,而WriteToken在上面定义了,是一个函数对象类型,返回值为void,参数为error_code和size_t,所以我们为了调用async_write_some函数也要传入一个符合WriteToken定义的函数,就是我们声明的writeCallBackErr函数,前两个参数为WriteToken规定的参数,第三个参数为MsgNode的智能指针,这样通过智能指针保证我们发送的Node生命周期延长。
writeCallBackErr是一个回调函数,在里面先检查数据是否发送完成,没完成的话继续使用同样的方法发送数据,保证信息发送完成。
重点是各位看到这里几乎是白雪,因为这种方法是有缺陷的,并不能放到实际项目中去使用,也是我加上Err后缀的原因。因为async_write_some回调函数返回已发送的字节数可能并不是全部长度。比如TCP发送缓存区总大小为8字节,但是有3字节未发送(上一次未发送完),这样剩余空间为5字节。
此时我们调用async_write_some发送hello world!实际发送的长度假设为5,也就是只发送了hello,剩余world!通过我们的回调继续发送用。而实际开发的场景户是不清楚底层tcp的多路复用调用情况的,用户想发送数据的时候就调用writeToSocketErr,或者循环调用WriteToSocketErr,很可能在一次没发送完数据还未调用回调函数时再次调用writeToSocketErr,因为boost::asio封装的时epoll和iocp等多路复用模型,当写事件就绪后就发数据,发送的数据按照async_write_some调用的顺序发送,所以回调函数内调用的async_write_some可能并没有被及时调用。比如我们如下代码:
//用户发送数据
writeToSocketErr("Hello World!");
//用户无感知下层调用情况又一次发送了数据
writeToSocketErr("Hello World!");
那么很可能第一次只发送了Hello,后面的数据没发完,第二次发送了Hello World!之后又发送了World!所以对端收到的数据很可能是”HelloHello World! World!”
那怎么解决这个问题呢,我们可以通过队列保证应用层的发送顺序。我们在Session中定义一个发送队列,然后重新定义正确的异步发送函数和回调处理
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
void connect(const boost::asio::ip::tcp::endpoint& ep);
void writeCallBackErr(const boost::system::error_code& ec,
std::size_t bytes_transferred,
std::shared_ptr<MsgNode>);
void writeToSocketErr(const std::string buf);
void writeCallBack(const boost::system::error_code& ec,
std::size_t bytes_transferred);
void writeToSocket(const std::string buf);
private:
std::shared_ptr<boost::asio::ip::tcp::socket> _socket;
std::shared_ptr<MsgNode> _send_node;
std::queue<std::shared_ptr<MsgNode>> _send_queue;
bool _send_pending;
};
void Session::writeCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (ec.value() != 0) {
std::cout << "错误,错误码:" << ec.value() << ",错误信息:" << ec.message() << std::endl;
return;
}
auto& send_data = _send_queue.front();
send_data->setCurLen(send_data->getCurLen() + bytes_transferred);
if (send_data->getCurLen() < send_data->getTotalLen()) {
this->_socket->async_write_some(
boost::asio::buffer(send_data->getMsg() + send_data->getCurLen(),
send_data->getTotalLen() - send_data->getCurLen()
),
std::bind(
&Session::writeCallBack,
this,
std::placeholders::_1,
std::placeholders::_2
)
);
return;
}
_send_queue.pop();
if (_send_queue.empty()) {
_send_pending = false;
}
else {
auto& send_data = _send_queue.front();
this->_socket->async_write_some(
boost::asio::buffer(send_data->getMsg() + send_data->getCurLen(),
send_data->getTotalLen() - send_data->getCurLen()),
std::bind(
&Session::writeCallBack,
this,
std::placeholders::_1,
std::placeholders::_2
)
);
}
}
void Session::writeToSocket(const std::string buf)
{
_send_queue.emplace(new MsgNode(buf.c_str(), buf.length()));
if (_send_pending) {
// 有未发完的数据
return;
}
this->_socket->async_write_some(
boost::asio::buffer(buf),
std::bind(
&Session::writeCallBack,
this,
std::placeholders::_1,
std::placeholders::_2
)
);
_send_pending = true;
}
定义了bool变量_send_pending,该变量为true表示一个节点还未发送完。_send_queue用来缓存要发送的消息节点,是一个队列。async_write_some函数不能保证每次回调函数触发时发送的长度为要总长度,这样我们每次都要在回调函数判断发送数据是否完成,asio提供了一个更简单的发送函数async_send,这个函数在发送的长度未达到我们要求的长度时就不会触发回调,所以触发回调函数时要么时发送出错了要么是发送完成了,其内部的实现原理就是帮我们不断的调用async_write_some直到完成发送,所以async_send不能和async_write_some混合使用。下面我们基于async_send再封装另外一个发送函数。
(2)async_send
async_send函数的使用方法和async_write_some类似,甚至更简单。
void Session::writeAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (ec.value() != 0) {
std::cout << "错误,错误码:" << ec.value() << ",错误信息:" << ec.message() << std::endl;
return;
}
_send_queue.pop();
if (_send_queue.empty()) {
_send_pending = false;
}
else {
auto& send_data = _send_queue.front();
this->_socket->async_send(
boost::asio::buffer(send_data->getMsg() + send_data->getCurLen(),
send_data->getTotalLen() - send_data->getCurLen()),
std::bind(
&Session::writeAllCallBack,
this,
std::placeholders::_1,
std::placeholders::_2
)
);
}
}
void Session::writeAllToSocket(const std::string& buf)
{
_send_queue.emplace(new MsgNode(buf.c_str(), buf.length()));
if (_send_pending) {
return;
}
// 这个函数内部通过调用多次async_write_some函数来实现发送所有
// 实际工程直接用这个就可以了,比较简单
this->_socket->async_send(boost::asio::buffer(buf),
std::bind(
&Session::writeAllCallBack,
this,
std::placeholders::_1,
std::placeholders::_2
)
);
_send_pending = true;
}
3、异步读操作
异步读操作和异步的写操作类似同样又async_read_some和async_receive函数,前者触发的回调函数获取的读数据的长度可能会小于要求读取的总长度,后者触发的回调函数读取的数据长度等于读取的总长度。
先基于async_read_some封装一个读取的函数readFromSocket,同样在Session类的声明中添加一些变量,_recv_node用来缓存接收的数据,_recv_pending为true表示节点正在接收数据,还未接受完。
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
void connect(const boost::asio::ip::tcp::endpoint& ep);
void writeCallBackErr(const boost::system::error_code& ec, std::size_t bytes_transferred,
std::shared_ptr<MsgNode>);
void writeToSocketErr(const std::string buf);
void writeCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
void writeToSocket(const std::string buf);
void writeAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
void writeAllToSocket(const std::string& buf);
void readCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
void readFromSocket();
void readAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
void readAllFromSocket();
private:
std::shared_ptr<boost::asio::ip::tcp::socket> _socket;
std::shared_ptr<MsgNode> _send_node;
std::shared_ptr<MsgNode> _read_node;
std::queue<std::shared_ptr<MsgNode>> _send_queue;
bool _send_pending;
bool _read_pending;
};
代码实现就直接放出来了,理解方式跟async_write_some和async_send差不多
void Session::readCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
_read_node->setCurLen(_read_node->getCurLen() + bytes_transferred);
if (_read_node->getCurLen() < _read_node->getTotalLen()) {
_socket->async_read_some(
boost::asio::buffer(_read_node->getMsg() + _read_node->getCurLen(),
_read_node->getTotalLen() - _read_node->getCurLen()),
std::bind(&Session::readCallBack,
this,
std::placeholders::_1,
std::placeholders::_2)
);
return;
}
_read_pending = false;
_read_node = nullptr;
}
void Session::readFromSocket()
{
if (_read_pending) {
return;
}
_read_node = std::make_shared<MsgNode>(RECVSIZE);
// 相反,接收数据推荐使用这个函数,因为可控性高
_socket->async_read_some(
boost::asio::buffer(_read_node->getMsg(), _read_node->getTotalLen()),
std::bind(&Session::readCallBack, this, std::placeholders::_1, std::placeholders::_2)
);
_read_pending = true;
}
void Session::readAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
_read_node->setCurLen(_read_node->getCurLen() + bytes_transferred);
_read_pending = false;
_read_node = nullptr;
}
void Session::readAllFromSocket()
{
if (_read_pending) {
return;
}
_read_node = std::make_shared<MsgNode>(RECVSIZE);
_socket->async_receive(
boost::asio::buffer(_read_node->getMsg(), _read_node->getTotalLen()),
std::bind(&Session::readAllCallBack,
this,
std::placeholders::_1,
std::placeholders::_2)
);
_read_pending = true;
}