异步读写
接下来主要了解一下对于ASIO中关于异步读写的模型,前情提醒,由于我是跟着另外一个博客学习的,所以自己的理解上面可能存在着一些错误的地方,如果知道,请斧正。
前情回顾
只要你学过OS,你就应该知道,对于一个同步操作来说,其存在的隐藏缺陷是什么,虽然说同步操作可以由用户自己来控制调用的顺序,但是同步操作是一个阻塞形的调用。其在发出I/O之后是会阻塞当前CPU的,而I/O操作往往会消耗上几毫秒甚至于更长的时间,这是不可接受的。所以自然就出现了一种不会使得I/O操作阻塞当前CPU的思想,那自然就是异步。
对于异步操作来说,其的本质逻辑其实很简单,就是通过将一系列的程序行为封装为一系列的对象,或者更实际点,任务。通过一个任务调度器,来管理这些任务的执行过程,但是在这里还是没有完成一种阻塞的转移,因为对于I/O操作来说,其本身无论如何都是会阻塞当前线程的。所以需要一种机制,能够使得这种阻塞不会反应到当前执行的主线程中,而这种通过开辟一个线程可以简单的实现。同时,我们要求在主线程中需要持有该调度机制的一种引用,使得在完成一定的工作后调度程序能够直接通知主线程完成了工作,但那个不是本次的内容范围了(当然,这通过线程共享空间的性质可以简单实现就是了)。
ASIO异步
对于ASIO的读写来说,为了避免同步读写时阻塞对于程序性能带来的损失,很多时候对于I/O操作都是基于异步读写的模式的。当然,ASIO中封装了一个自己的异步读写模型,可以直接使用,但是这里先不进行了解。
先回到刚才,我们知道,对于异步模型来说,其最重要的一点是基本的事务定义,由于我们使用的是别人ASIO的框架,所以我们这里需要进行的了解对应的接口需要怎么使用。
当然,想要熟悉的使用这个机制,必须了解底层的异步,也就是刚才的前情回顾这一块。当然,了解这些并不是为了让你能够真正的去手搓出一个自己的异步模型出来,或者说,对我来说并不是为了这个。对我来说,了解这种底层的思想,本质上其实是为了在接触这种高度完成的框架中去快速熟悉各种知识点。我十分推荐阅读本文的各位都去试一下先去简单的了解底层再来学习高层框架,那么会回来感谢我的(doge)。
对于ASIO的异步操作模型来说,其的使用很简单,由于模型本身已经给我们提供了一套相对完整的框架,即一系列的async_*异步读写函数。我们在这里可以先把其分割为底层api和高层api来进行了解。我们现在先来了解对应的底层apia,async_*_some系列函数。
我们在这里只介绍一套读的async函数,读写的使用之间是基本一致的,不必多说
状态保持
我们在前面已经知道,对于一个读写函数来说,其本身是可能不会完成对于数据的完整读取的,就比如我们前面的同步读系列_some函数。那么,我们需要一个结构来进行对于读写期间操作数据的状态的保持。想象一下,如果这个结构不存在,关于读写数据的信息是松散的,那么其在编码和使用层面将会是一个地狱。
1 | //最大报文接收大小 |
在一个信息结构中,其实最主要的是它的信息,而不是它的功能,所以需要的是其对于信息的提供能力,如上图,当然,你可以采用你认为的更美观合理的办法,这里不再对其进行赘诉。
异步读
1 | class Session{ |
这里其实没什么好说的,我们直接进入一个合格的读写操作的基础要素。需要注意的是,在本文中,很多东西的假设都是很不现实的,在之后会逐渐进行扩展,就像OSTEP一书中线提出一系列的约束再一步步解开一样。不过这里,诶,我不说哪里的问题。
首先我们来看一下我们这里创建的函数
1 | void WriteCallBackErr(const boost::system::error_code & ec, std::size_t bytes_transferred, |
我们需要先了解一下该函数的地位,这俩个函数是为了来实现我们一个相对完整的读操作的,void WriteToSocketErr(const std::string& buf);函数是我们提供给外界的能够完成一个正确操作的入口,只需要传入一个缓冲区数据的地址。我们来看一下这里面的实现。
1 | void Session::WriteToSocketErr(const std::string& buf) { |
对于函数的内部,其将对应的缓冲区地址封装成了一个我们实际使用的一个缓冲区地址,接下来的所有操作都是基于该缓冲区的操作。注意看该函数内的行为,其实就是一层对于外部参数往内部异步async_write_some的操作的调用参数转换与传递。
注意来看async_write_some函数。该函数需要三个函数,其中第一个是一个缓冲区地址,用于指定该次读写中的操作数据,在该例中即是由外部的缓冲区转换而来的node结构。其第二个参数是该次读取中所应该读取的全部的数据的长度。由于这里的是第一次传入,可以看到其实是一个_total_len成员长度。对于第三个长度,这是一个异步 _some 读写所能实现完整的读写的核心,在该函数中封装了对应的回调逻辑。
我们在前面已经了解过同步读写,在同步读写中,对应的读写操作并不一定会完成我们指定的缓冲区中的数据的全部读写,需要一些额外的操作来进行确保,这里也是如此。
你应该也能够大致猜测该回调函数的作用。就是在一次读写操作之后,检测当前是否完成了对应的全部的读写,如果未完成,这个回调函数会去执行接下来的读写。对于这里的函数的格式,由于我们是使用的人家ASIO的框架,所以我们要遵守对应的回调函数的约束。对于回调函数来说,其只需要俩个参数,或者说,只能存在俩个由框架来进行自动发送的参数,其分别占用前俩个参数。其中起一个参数是一个对应的错误码,其而是当前已经传递的字节数。至于为什么是这俩个,可以自行去阅读源码。
你也许已经看到了,这里使用了bind函数来进行我们第三个可调用对象的构建以及传递,想一想,能不能使用该函数去进行我们该回调函数的额外个性化扩展呢?能否使用一种更好的格式来进行这里的参数的传递呢……
回调函数
1 | void Session::WriteCallBackErr(const boost::system::error_code& ec, |
先停下来阅读一下上面的源码,分析一下为什么其为什么能够存在三个参数,我们前面不是说为什么只能有俩个参数由框架自动传递码,这里是怎么实现的,我怎么来实现它,怎么来扩展他
在该回调函数中,对应的逻辑其实也很简单,就是简单的更新我们当前对应的缓冲区结构体的已传输数据的长度大小,然后根据未传输的数据进行下一次传输设置,可以看到,这里存在了又一次的async_write_some。想象一下在该种调用中存在的函数调用链。
总的来说,回调函数其实应该相当简单,毕竟为了效率,你不能在一个I/O操作中封装过多的逻辑,这是非常令人厌恶的。
到了这里,你会发现,你已经基本了解了实现对应的读写操作所需要进行的步骤,虽然说还是很粗糙,但是我这里并不准备深入,感兴趣的可以去llfc大佬的博客中进行了解。在我这里,我将梳理一遍在这种设计中的基本逻辑来辅助你我的理解
设计步骤
对于一个异步读写来说,我们最好为其封装一个内聚的结构来进行后续的操作的基础组件提供。理论上,你完全可以通过松散的结构去实现功能。但是,这在OOP的编程中是非常令人厌恶的,首先是不美观,再者其十分的难以维护,而且在系统的开发中,各种变量满天飞是非常令人绝望的。所以,在设计一个完整的异步读写操作时,我们需要的是优先设计一个美观的结构来支持我们后续的使用。
在设计了对应的数据结构之后,我们可以进入对应的服务中的异步读写操作的设计。对于一个异步读写来说,其一般都是调用由ASIO提供的异步读写函数来实现功能。但是一般的读写函数其内部并不会保证对于数据的全部读写,至少对于_some后缀来说,你可以去自己了解额外的更高层封装的读写函数,但是你得知道,它们底层的实现都是基于 _some方法的。
对于想要实现使用_some函数来实现对应的完整读写,我们需要进行对应的回调函数的设计来实现一个函数调用链的构建来实现数据的完整链式读取。一般来说,这都是通过对于数据结构体中已读取数据与总数据的更新,维护与使用来实现的。你可以回过头去看对应的回调函数,其内部是否是通过对应的地址偏移来实现的。
对我来说,整个异步读写的精髓就是这上面的三段话,当然,由于个人精力问题,我并没有对于全部内容都进行编写,你可以将其视为一种敝帚自珍。你要是想知道更多,就来盒我吧。
包括但不限于,对于高层api如何使用底层api,为什么对应的回调函数的调用次数会不同等问题,这都是在异步读写函数中需要了解的,在这里给你列出了几条可能的方向,在你看完llfc大佬的课程后,你可以去自己想一想在这方面存在的问题已经到底怎么解决的,这会帮助你思维的建立的,如果当你能够真正看懂我这段话想讲什么,请联系我,我非常想找到一个人来交流CS的学习心得