异步读写API

异步写操作

首先,我们定义一个session类,这个session类表示服务器处理客户端连接的管理类

class Session {
public:
    Session(std::shared_ptr<asio::ip::tcp::socket> socket);
    void Connect(const asio::ip::tcp::endpoint& ep);
private:
    std::shared_ptr<asio::ip::tcp::socket> _socket;
};

对端的连接读写,封装了Connect函数

void Session::Connect(const asio::ip::tcp::endpoint  &ep) {
    _socket->connect(ep);
}

 在读写操作前,我们先封装一个Node结构,用来管理要发送和接收的数据,该结构包含数据域首地址,数据的总长度,以及已经处理的长度(已读的长度或者已写的长度)

const int RECVSIZE = 1024; //这里为分别演示,我们就设置了一个固定的长度,但实际上这个长度是由tlv的这种协议,对端传过来告诉我们有多长,我们进行切包处理,这样能防止粘包
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];
	}


//先设为公共,后期后优化
	char* _msg;
	int _total_len;
	int _cur_len;
};

写了两个构造函数,第一个构造函数的两个参数负责构造写节点,第二个构造函数负责构造读节点。

async_write_some


接下来为Session添加异步写操作和负责发送写数据的节点 ,这里我们先用一个有隐患的WriteCallBackWriteToSocket函数来完成异步写操作

class Session{
public:
    void WriteCallBack(const boost::system::error_code & ec, std::size_t bytes_transferred,
    std::shared_ptr<MsgNode>);
    void WriteToSocket(const std::string& buf);
private:
    std::shared_ptr<MsgNode> _send_node;
};

async_write_some与WriteToken源码解析

WriteToSocket函数为我们封装的写操作,WriteCallBack为异步写操作回调的函数,为什么会有三个参数呢,我们可以看一下asio源码,async_write_some是异步写的函数,这个异步写函数有两个参数,第一个参数为ConstBufferSequence常引用类型的buffers,第二个参数为WriteToken类型

BOOST_ASIO_COMPLETION_TOKEN_FOR(void (boost::system::error_code,
        std::size_t)) WriteToken
          BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
  BOOST_ASIO_INITFN_AUTO_RESULT_TYPE_PREFIX(WriteToken,
      void (boost::system::error_code, std::size_t))
  async_write_some(const ConstBufferSequence& buffers,//常量类型buffer结构
      BOOST_ASIO_MOVE_ARG(WriteToken)token  //WriteToken是一个写的回调函数
        BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type))

WriteToken的源码定义,是一个函数对象类型,返回值为void,参数为error_codesize_t

 template <typename ConstBufferSequence,
     BOOST_ASIO_COMPLETION_TOKEN_FOR(void (boost::system::error_code,
       std::size_t)) WriteToken = default_completion_token_t<executor_type>>


BOOST_ASIO_COMPLETION_TOKEN_FOR这个是宏,定义了一个返回值为void类型的函数

所以这是boost::asio要求的回调函数的两个参数。那么再回到我们自己封装的回调函数,则比它多了一个参数,MsgNode的智能指针,保证我们发送的Node生命周期延长。

具体实现一下我们封装的WriteToSocket函数

void Session::WriteToSocket(const std::string buf) {
	_send_node = std::make_shared<MsgNode>(buf.c_str(), buf.length());

	//async_write_some异步写函数,传入的参数第一个buffer结构,第二个回调函数
	this->_socket->async_write_some(asio::buffer(_send_node->_msg, _send_node->_total_len),
		std::bind(&Session::WriteCallBack, this, std::placeholders::_1, std::placeholders::_2,
			_send_node));
		//成员函数需要bind才能传入,bind的第一个参数是成员函数地址,第二个是类对象,根据需求,回调函数需要传入两个参数,这里先用占位符的方式告诉bind函数给我们保留这两个参数的位置,
		//第三个参数是我们需要的MsgNode智能指针类型参数
}
  • 当你调用 async_write_some 方法时,实际的写操作在后台进行,当前线程不会被阻塞,会继续执行后面的代码
  • 一旦数据成功发送,boost::asio 将调用回调函数(在这里是 WriteCallBack),并将两个参数传递给它:    

        第二个参数 bytes_transferred 是一个 std::size_t 类型的值,表示此调用中实际发

         送的字节数。

        第一个参数 ec 是一个 boost::system::error_code 对象,指示操作是否成功。

  • 在上述代码中,std::bind(&Session::WriteCallBack, this, std::placeholders::_1, std::placeholders::_2, _send_node) 使用了占位符 _1 和 _2。这两个占位符分别对应于异步操作返回的 ec 和 bytes_transferred 参数。当异步写操作完成后,Boost ASIO 会用实际的参数替换这些占位符,并调用 WriteCallBack 函数。

接下来我们实现WriteCallBack 函数

void Session::WriteCallBack(const boost::system::error_code& ec, std::size_t bytes_transfeered, 
	std::shared_ptr<MsgNode> msg_node) {
	if (bytes_transfeered + msg_node->_cur_len < msg_node->_total_len) {
		_send_node->_cur_len += bytes_transfeered;
		this->_socket->async_write_some(asio::buffer(_send_node->_msg + _send_node->_cur_len,
			_send_node->_total_len - _send_node->_cur_len),
			std::bind(&Session::WriteCallBack, this, std::placeholders::_1, std::placeholders::_2,
				_send_node));
	}

}
  • 在WriteCallBack函数里判断如果已经发送的字节数没有达到要发送的总字节数,那么就更新节点已经发送的长度,然后计算剩余要发送的长度,如果有数据未发送完,再次调用async_write_some函数异步发送。

但是这个函数并不能投入实际应用,因为async_write_some回调函数返回已发送的字节数可能并不是全部长度。比如TCP发送缓存区总大小为8字节,但是有3字节未发送(上一次未发送完),这样剩余空间为5字节,当我们再次调用它发送一个hello world!,此时只把hello发生出去了,world!还未发送,已发送的字节数为5,并不是总长,所以要进行判断,通过回调函数继续发生。

这里还有一个隐患,就是在实际开发中,我们不知道底层tcp多路复用的调用顺序,如果在循环发送调用WriteToSocket,很可能在一次没发送完数据还未调用回调函数时再次调用WriteToSocket,因为boost::asio封装的时epolliocp等多路复用模型,当写事件就绪后就发数据,发送的数据按照async_write_some调用的顺序发送,所以回调函数内调用的async_write_some可能并没有被及时调用,boost::asio底层并不帮我们做顺序处理。
比如我们如下代码就进入下一个信息的发送了

/用户发送数据
WriteToSocketErr("Hello World!");
//用户无感知下层调用情况又一次发送了数据
WriteToSocketErr("Hello World!");
  • 那么很可能第一次只发送了Hello,后面的数据没发完,第二次发送了Hello World!之后又发送了World!
  • 所以对端收到的数据很可能是”HelloHello World! World!”

那怎么解决这个问题呢,我们可以通过队列保证应用层的发送顺序。我们在Session中定义一个发送队列,然后重新定义正确的异步发送函数和回调处理

class Session{
public:
    void WriteCallBack_(const boost::system::error_code& ec, std::size_t bytes_transferred);
    void WriteToSocket_(const std::string &buf);
private:
    std::queue<std::shared_ptr<MsgNode>> _send_queue;
    std::shared_ptr<asio::ip::tcp::socket> _socket;
    bool _send_pending;  //发送状态,是否有消息正在发送
};
Session::Session(std::shared_ptr<asio::ip::tcp::socket> socket) 
	:_socket(socket),_send_pending(false) {}   //-send_pending初始化为false


void Session::WriteCallBack_(const boost::system::error_code& ec, std::size_t bytes_transfeered) {
	if (ec.value() != 0) {
		std::cout << "Error code is" << ec.value() << " .Message" << ec.what()<<std::endl;
		return;
	}

	auto& send_data = _send_queue.front();
    send_data->_cur_len += bytes_transfeered;
	if (send_data->_cur_len<send_data->_total_len ) {
		this->_socket->async_write_some(asio::buffer(send_data->_msg + send_data->_cur_len, send_data->_total_len - send_data->_cur_len),
			std::bind(&Session::WriteCallBack_, this, std::placeholders::_1, std::placeholders::_2));
		return; //返回,剩下的都不继续执行了,继续调用回调函数
	}
	//如果执行到这里说明队列中位于队首的对象数据已经发送完了
	_send_queue.pop(); //将其弹出

	if (_send_queue.empty()) {
		_send_pending = false;
		return;
	}
	//如果队列不是空,则继续将队首元素发送
	if (!_send_queue.empty()) {  
		auto& send_data = _send_queue.front();
		this->_socket->async_write_some(asio::buffer(send_data->_msg + send_data->_cur_len, send_data->_total_len - send_data->_cur_len),
			std::bind(&Session::WriteCallBack_, this, std::placeholders::_1, std::placeholders::_2));
		return;
	}

}

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(asio::buffer(buf),
		std::bind(&Session::WriteCallBack_, this, std::placeholders::_1, std::placeholders::_2));  //异步,执行这个函数时不会进行阻塞在这里,会立即返回控制权,继续执行后面的代码

	_send_pending = true; //将状态置true,表示有数据正在发送
}
  • 这里有个隐患,队列中的数据pop掉后,在堆上分配的内存并没有被释放,会导致内存泄漏,可以使用智能指针自动管理,                                                                                                                                                _send_queue.emplace(std::make_shared<MsgNode>(buf.c_str(), buf.length()));                         来优化

上面这个async_write_some函数因为不会一次发送完数据,所以会不断调用回调函数

async_send

asio提供一种可以一次性发完的函数async_send,其内部的实现原理就是帮我们不断的调用async_write_some直到完成发送,所以async_send不能和async_write_some混合使用,我们基于async_send封装另外一个发送函数,而我们设置的回调函数只有在发送数据出错的时候或者是发送完数据时才会调用,大大简化了一些操作,使用示例:

void WriteAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transfeered);
void WriteAllToSocket(const std::string buf);
void Session::WriteAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transfeered) {
	if (ec.value() != 0) {
		std::cout << "Error code is" << ec.value() << " .Message is" << ec.what() << std::endl;
		return;
	}
	_send_queue.pop();

	if (_send_queue.empty()) {
		_send_pending = false;
	}

	if (!_send_queue.empty()) {
		auto& send_data = _send_queue.front();
		this->_socket->async_send(asio::buffer(send_data->_msg, send_data->_total_len),
			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;
	this->_socket->async_send(asio::buffer(buf),
		std::bind(&Session::WriteAllCallBack, this, std::placeholders::_1, std::placeholders::_2));
	_send_pending = true;
}

异步读操作

异步读操作和异步的写操作类似同样有async_read_some和async_receive函数,前者触发的回调函数获取的读数据的长度可能会小于要求读取的总长度,后者触发的回调函数读取的数据长度等于读取的总长度。

async_read_some

先基于async_read_some封装一个读取的函数ReadFromSocket,同样在Session类的声明中添加一些变量

class Session {
public:
    void ReadFromSocket();
    void ReadCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
private:
    std::shared_ptr<asio::ip::tcp::socket> _socket;
    std::shared_ptr<MsgNode> _recv_node;
    bool _recv_pending;
};
Session::Session(std::shared_ptr<asio::ip::tcp::socket> socket) 
	:_socket(socket),_send_pending(false),_recv_pending(false){

}

//不考虑粘包情况,先用固定的字节接收
void Session::ReadCallBack(const boost::system::error_code& ec, std::size_t bytes_transfeered) {
	if (ec.value() != 0) {
		std::cout << "Error code is" << ec.value() << " .Message is" << ec.what() << std::endl;
		return;
	}
	_recv_node->_cur_len += bytes_transfeered;
	if (_recv_node->_cur_len  < _recv_node->_total_len) {
		this->_socket->async_read_some(asio::buffer(_recv_node->_msg + _recv_node->_cur_len, _recv_node->_total_len - _recv_node->_cur_len),
			std::bind(&Session::ReadCallBack, this, std::placeholders::_1, std::placeholders::_2));
		return;
	}

	_recv_pending = false;//如果读完了则将标记置为false
	_recv_node = nullptr;
}
void Session::ReadFromSocket(const std::string buf) {
	//这里其实可以不必要判断,因为一般读操作的是被动的,很少有主动发起读操作
	if (_recv_pending) {
		return;
	}
	_recv_node = std::make_shared<MsgNode>(RECVSIZE);
	this->_socket->async_read_some(asio::buffer(buf),
		std::bind(&Session::ReadCallBack, this, std::placeholders::_1, std::placeholders::_2));
	_recv_pending = true;
}

async_receive

async_receive函数一次性读完

void Session::ReadAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transfeered) {
	if (ec.value() != 0) {
		std::cout << "Error code is" << ec.value() << " .Message is" << ec.what() << std::endl;
		return;
	}
	_recv_pending = false;//如果读完了则将标记置为false
	_recv_node = nullptr;
}

void Session::ReadAllFromSocket(const std::string buf) {
	if (_recv_pending) {
		return;
	}
	_recv_node = std::make_shared<MsgNode>(RECVSIZE);
	this->_socket->async_receive(asio::buffer(_recv_node->_msg, _recv_node->_total_len),
		std::bind(&Session::ReadAllCallBack, this, std::placeholders::_1, std::placeholders::_2));
	_recv_pending = true;
}

同样async_read_some和async_receive不能混合使用,否则会出现逻辑问题。

多路复用模型

多路复用模型(Multiplexing Model)在计算机网络和并发编程中,通常是指一种机制,它允许多个数据流或信号通过同一个通道进行传输,这种机制可以提高资源利用率,并减少延迟。这个概念可以应用于多个层次,例如在 TCP/IP 协议栈、操作系统的 I/O 处理以及网络编程中的异步处理等。

常见的多路复用技术有:

  • 时间分复用(TDM):在固定的时间槽内轮流使用同一信道。
  • 频分复用(FDM):将信道划分为多个频段,每个信号占用不同的频带。
  • 波分复用(WDM):用于光纤通信,将不同波长的光信号复用到同一光纤中。

网络编程中的多路复用

在网络编程中,多路复用通常指的是在同一线程中处理多个连接的能力,而不必为每个连接创建一个单独的线程或进程。这样可以有效地管理大量的并发连接,减少上下文切换的开销。

在 UNIX/Linux 系统中,通常使用以下几种系统调用来实现网络 I/O 的多路复用:

  • select():监视多个文件描述符(如套接字),当其中任何一个变为可读或可写时返回。这是一个较早的多路复用方法。

  • poll():类似于 select(),但支持更多的文件描述符和更灵活的事件类型。

  • epoll():Linux 特有的高效 I/O 多路复用机制,特别适合处理大量并发连接。epoll 提供了更好的性能,因为它只在需要时通知应用程序。

在 Windows 操作系统中,通过iocp实现高效 I/O 多路复用机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值