1. ip::tcp::socket
liburl库使用"curl*" 代表socket 句柄
asio库使用ip::tcp::socket类代表TCP协议下的socket对象。
将“句柄”换成“对象”,因为asio库是不打折扣的C++库
ip::tcp::socket提供一下常用异步操作都以async开头
async_connect() | Start an asynchronous connect |
async_read_some() | Start an asynchronous read |
async_wrtie_some() | Start an asynchronous write |
对应的注释以“Start...”开始,表明一个异步操作函数只是负责开始一件事,并不一直等到这件事情完成。
async_connect() : 主动发起一个连接请求
async_read_some() : 从该网络读一些数据,即有多少读多少
async_wrtie_some() : 向该网络写一些数据,即能写多少写多少
网络数据的传输,无论是发是收,是块是慢,相比CPU的计算速度,总是可以认为数据是在“断断续续”地流动的。
在libcurl下载新浪和搜狐网站的例子说明中,我们已经有关相关描述。带“_some”后缀的读写操作,正是用于实现“有多少处理多少”的思路。
不过,也会有许多时候程序明确知道需要读入或写出多少字节。asio提供一对*函数,用于处理这种情况,如表13-4所列
async_read() | Start an asynchronous read |
async_write() | Start an asynchronous write |
【小提示】:“读/写”还是“接受/发送”
asio::ip::tcp:;socket也提供receive()和send()方法。入参的功能与read_some()和write_some()一样。细微差异是“receive()/send()”有另一套较少使用的重载版本。
既然是异步操作,就和C++的async()方法类似,调用时需要传入一个动作,用于在操作完成时回调,不管是读操作还是写操作,它们都需要这样一个原型的操作:
void handler(/*原型的名字无所谓*/
const boost::system::error_code& error
, std::size_t bytes_transferred
);
如果操作发生错误,error传入出错信息,这一点和定时器的回到操作的入参一样,其实是asio中各类回调都必须有的入参。如果操作成功,第二个入参表示本次读到或者写出多少字节。
【课堂作业】: 对比libcurl和asio网络读写回调
复习libcurl设置CURLOPT_READFUNCTION、CURLOPT_WRITEFUNCTION时所使用的原型,并与asio作对比。
作业的答案必须包含一点:libcurl所需回调用的函数带有数据,比如当读到网页数据时,libcurl回调我们设定的函数是:
size_t write_html(char* data, size_t size, size_t nmemb, void*);
第一个入参就是“char* data”就是libcurl读到的数据,通过回到交给我们处理,上例中我们将它写成一个磁盘文件;
但asio版本的回调,两个入参,一个出错时才有用,另一个只是告诉我们数据的大小。可是我们更关心的是数据呀,特别是读操作。
异步读操作:
让我们从最关心的读操作看起。成员函数async_read_some()的原型如下:
template <typename MutableBufferSequence, typename ReadHandler>
void async_read_some(const MutableBufferSequence& buffers
, ReadHandler handler);
忽略模版,先看简化版:
void async_read_some(buffers, handler);
第一个入参要一个“内存块”对象,第二个入参就是前面说的handler()回调操作,可以是函数指针、可以是……
buffers的类型虽然是模版,但类型模版名称MutableBuffer透露端倪,它暗示我们这块buffers应该是“Mutable可变的”。在asio中,“可变的”内存块对象既表示其内容可被修改,也表示万一空间不足,该内存块对象还应支持扩张容量。简单滴说就是类似std::vector类型的对象。这样的要求非常合理,因为read some正意味着事先不确定这次到底能读到多少字节的数据。
用于明确读取指定字节内容的*函数async_read()简化原型如下:
void async_read(ip::tcp::socket& socket,
, const MutableBufferSequence& buffers
, ReadHandler handler);
多出第一个入参,指定负责异步读的网络底层套接字socket;
重点是内部实现的读取数据过程,会反复地读取直到buffers填满或读操作出错为止。
异步写操作
async_write_some()原型如下:
template <typename ConstBufferSequequence, typename WriteHandler>
void async_write_some(const ConstBufferSequequence& buffers
, WriteHandler handler);
ConstBufferSequequence表明,这次要的buffer不会被修改。因为待写的数据肯定得事先准备好,
有多大,有什么内容一切都是定的。
可见对于网络读写操作所需的数据存储,asio要求用户方在发起异步操作前就自行准备好(上述入参buffers)。asio通常将直接使用该内存;libucurl则是由库创建内存,要求我们在回调操作时读出或写入。
asio的策略易用性较差,因为用户需要在异步操作发起到完成之间维护好这块内存;但性能较好,
因为减少内存复制或内存申请的次数。
用于明确写出指定字节内容的*函数async_write()简化原型如下:
void async_write(ip::tcp::socket& socket
, const ConstBufferSequequence& buffers
, WriteHandler handler);
第一个入参用于指定负责异步读的网络底层套接字。内部实现的写数据过程,会负责将buffers内部的数据全部写出或操作出错为止。
异步发起连接。
async_connect()的原型为
template <typename ConnectHandler>
void async_connect(const endpoint_type& peer_endpoint
, ConnectHandler handler);
入参peer_endpoint是待连接的目标地址,其数据结构留到下一小节讲解。
入参handler是连接操作完成(失败或成功)后续回调的操作。连接操作不需要显式数据传递,
因此和定时器回调一样,只有error入参:
void handler(const boost::system::error_code& error);
拥有async_connect异步连接,async_read_some异步读,和async_write_some异步写的方法,
如果我们有一个ip::tcp::socket对象,就可以将连接,读,写串成异步操作链。
应用代码,io_service以及操作系统(OS)三者共同串成的,异步操作链示意图如图13-20所示:
除连接操作只需一次之外,后续的读写曹组可以根据需要各种组合。比如图中示意一写一读,实际应用也有可能是“写,写,读,读”或“读,读,写,写”。在后面“echo通信示例”,我们给出链式异步操作的实现代码。