C++——boost:asio的使用

背景知识

高效网络编程一般都要依赖于IO复用,IO复用是指同时发送并监听处理很多socket或者文件读写的事件。IO复用的高效方式目前常用的有两种:Reactor和Proactor。这两种方式在操作系统级都是异步和非阻塞的,也就是说用户提交了一个请求后都可以直接返回。但是Reactor在用户层级看来是同步的,就是在提交了一系列的操作给操作系统后,需要阻塞监听等待事件的发生,如果有事件发生则手动调用相关的函数进行处理,其具体在操作系统级利用的是操作系统的事件通知接口。而Proactor在用户看来是异步的,他是在调用的时候同时注册一个回调函数,如果请求在操作系统级有结果了,其注册的回调函数就会自动调用。这个在操作系统级使用的aio异步调用接口。

http://www.artima.com/articles/io_design_patterns2.html

最显著的不同时,以TCP距离,Reactor会在内核收到TCP数据的时候通知上层应用程序,后面从内核中取出数据并调用处理函数处理用用户完成(什么时候,怎么处理)。Proactor会在TCP收到数据后由内核将数据拷贝到用户指定的空间,然后立即调用注册的回调函数进行处理。

看起来Proactor会明显比Reactor简单和快速,但是由于工程原因,这个也是不一定的。

介绍

将整个异步平台抽象成boost::asio::io_service,想要使用asio都要先建立这个对象。异步平台上可以使用很多组件,比如boost::asio::ip::tcp::socket,这些组件又有各自的方法。但是过程是统一的:(asio可以执行同步和异步两种调用)

对于同步的调用。调用socket.connect(server_endpoint),或者其他队远端交互的方法。请求会首先发送给io_service,io_service会调用操作系统的具体方法,然后返回结果到io_service,io_service会通知到上层用户组件。错误用异常通知(可以阻止),正确用返回值。

对于异步调用。在组件调用io_service执行命令的同时要提供一个回调函数,可以同时发布多个异步请求,所有的返回结果都会放在io_service的队列里存储。进程调用io_service::run()会逐个的拿出存储在队列里的请求调用提前传入的回调函数进行处理。

I/O对象是用来完成实际功能的组件,有多种对象 :

boost::asio::ip::tcp::socket

boost::asio::ip::tcp::resolver

boost::asio::ip::tcp::acceptor

boost::asio::local::stream_protocol::socket本地连接

boost::asio::posix::stream_descriptor 面向流的文件描述符,比如stdout,stdin

boost::asio::deadline_timer 定时器

boost::asio::signal_set 信号处理

这些对象大部分需要io_service来初始化。还有一个用于控制io_service生命周期的work类,和用来存储数据的buffer类。

io_service

run() vs poll()

run()和poll()都循环执行I/O对象的事件,区别在于如果事件没有被触发(ready),run()会等待,但是poll()会立即返回。也就是说poll()只会执行已经触发的I/O事件。

比如I/O对象socket1,socket2, socket3都绑定了socket.async_read_some()事件,而此时socket1、socket3有数据过来。则调用poll()会执行socket1、socket3相应的handler,然后返回;而调用run()也会执行socket1和socket3的相应的handler,但会继续等待socket2的读事件。

stop()

调用 io_service.stop() 会中止 run loop,一般在多线程中使用。

post() vs dispatch()

post()和dispatch()都是要求io_service执行一个handler,但是dispatch()要求立即执行,而post()总是先把该handler加入事件队列。

什么时候需要使用post()?当不希望立即调用一个handler,而是异步调用该handler,则应该调用post()把该handler交由io_service放到事件队列里去执行。比如,Boost.Asio自带的聊天室示例,其中实现了一个支持异步IO的聊天室客户端,是个很好的例子。

chat_client.cpp 的write()函数之所以要使用post(),是为了避免临界区同步问题。write()调用和do_write()里async_write()的执行分别属于两个线程,前者会往write_msgs_里写数据,而后者会从write_msgs_里读数据,如果不使用post(),而直接调用do_write(),显然需要使用锁来同步write_msgs_。但是使用post()相当于由io_service来调度write_msgs_的读写,这就在一个线程内完成,无需额外的锁机制。

work类

work类用于通知io_service是否可以结束,只要对象work(io_service)存在,io_service就不会结束。所以work类用起来更像是一个标识,比如:

boost::asio::io_serviceio_service;

boost::asio::io_service::work*work = new boost::asio::io_service::work( io_service );

// deletework; // 如果不注释掉这一句,则run loop不会退出;一般用shared_ptr维护work对象,使用work.reset()来结束其生命周期。

io_service.run()

buffer类

buffer类分mutable_buffer和const_buffer两个类,buffer类特别简单,仅有两个成员变量:指向数据的指针 和 相应的数据长度。buffer类本身并不申请内存,只是提供了一个对现有内存的封装。

需要注意的是,所有async_write()、async_read()之类函数接受的buffer类型是MutableBufferSequence / ConstBufferSequence,这意味着它们既可以接受boost::asio::buffer,也可以接受std::vector<boost::asio::buffer> 这样的类型。

缓冲区管理

缓冲区的生命期是使用asio最需要重视的两件事之一,缓冲区之所以需要重视的原因在于Asio异步调用Reference里的这段描述:

Althoughthe buffers object may be copied as necessary, ownership of the underlyingmemory blocks is retained by the caller, which must guarantee that they remainvalid until the handler is called.

这意味着缓冲区从发起异步调用到handler被执行,这段时间内需要交由io_service控制,这个限制常常导致asio的某些代码变得可能比Reactor相应代码还要麻烦一些。

还是举上面聊天室的那个例子。chat_client.cpp的do_write()函数收到用户输入数据后,之所以把该数据保存到std::deque<std::string> write_msgs_ 队列,而不是存到类似chardata[]的数组里,然后去调用async_write(..data..)发送数据,是为了避免这种情况:输入数据速度过快,当上一次async_write()调用的handler还没有来得及处理,又收到一份新的数据,如果直接保存到data,会导致覆盖上一次async_write()的缓冲区。async_write()要求这个缓冲区从调用async_write()开始,直到handler处理这个时间段是不变的。

同样的,在do_write()函数里调用async_write()函数之前,先判断write_msgs_队列是否为空,也是为了保证async_write()总是从write_msgs_队列头取得有效的数据,而在handle_write()里当数据发送完毕后,再pop_front()弹出已经发送的数据包。以此避免出现前一个async_write()的handler还没执行完毕,就把队列头弹出去,导致对应的缓冲区失效问题。

这里主要还是因为async_write()和async_read()的区别,前者是主动发起的,后者可以由io_service控制,所以后者不用担心这种缓冲区被覆盖问题。因为在同一个线程里,哪怕需要读取的事件触发得再快,也需要由io_service逐一处理。

在这个聊天室的例子里,如果不考虑把数据按用户输入顺序发送出去的话,可以使用更简单的办法来处理do_write()函数,例如:

:::c++

voiddo_write(chat_message msg)

{

chat_message* pmsg = new chat_message(msg);// implement copy ctor for chat_message firstly

boost::asio::async_write(socket_,

boost::asio::buffer(pmsg->data(), pmsg->length()),

boost::bind(&chat_client::handle_write,this,

boost::asio::placeholders::error, pmsg));

}

voidhandle_write(const boost::system::error_code& error, chat_message* pmsg)

{

if (!error) {

}else{

do_close();

}

delete pmsg;

}

这里相当于给每个异步调用分配一块属于自己的内存,异步调用完成即自动释放掉,有些类似于闭包了。如果不希望频繁new/delete内存,也可以考虑使用boost::circular_buffer一次性分配内存后逐项使用。

I/O对象

socket

Boost.Asio最常用的对象应该就是socket了,常用的函数一般有这几个:

读写TCP socket的时候,一般使用read(),async_read(), write(), async_write(),为了避免所谓的short readsand writes,一般不使用receive(), async_receive(), send(), async_send()。

读写有连接的UDP socket的时候,一般使用receive(),async_receive(), send(), async_send()。

读写无连接的UDP socket的时候,一般使用receive_from(),async_receive_from(), send_to(), async_send_to()。

而自由函数boost::asio::async_write()和类成员函数socket.async_write_some()的有什么区别呢(boost::asio::async_read()和socket.async_read_some()类似):

boost::asio::async_write()异步写,立即返回。但它可以保证写完整个缓冲区的内容,否则将报错。boost::asio::async_write() 是通过调用n次socket.async_write_some()来实现的,所以代码必须确保在boost::asio::async_write()执行的时候,没有其他的写操作在同一socket上执行。在调用boost::asio::async_write()的时候,如果指定buffer的length没有写完或出错,是不会回调相应的handler的,它将一直在run loop中执行;直到buffer里所有的数据都写完或出错(此时handler里返回的长度肯定会小于buffer length),才会调用handler继续处理;而socket.async_write_some()不会有这样的问题,它只会尝试写一次,写完的长度会在handler的参数里返回。

所以,这里强调使用asio时第二件需要重视的事情,就是handler的返回值(一般可能声明为boost::asio::placeholders::error)。因为asio里所有的任务都由io_service异步执行,只有执行成功或者失败之后才会回调handler,所以返回值是你了解当前异步操作状况的唯一办法,记住不要忽略任何handler的返回值处理。

信号处理

Boost.Asio的信号处理非常简单,声明一个信号集合,然后把相应的异步handler绑上就可以了。如果你希望在一个信号集里处理所有的信号,那么你可以根据handler的第二个参数,来获取当前触发的是那个信号。比如:

boost::asio::signal_set signals(io_service,SIGINT, SIGTERM);

signals.add(SIGUSR1); // 也可以直接用add函数添加信号

signals.async_wait(boost::bind(handler, _1,_2));

void handler(

constboost::system::error_code& error,

intsignal_number // 通过这个参数获取当前触发的信号值

);

定时器

Boost.Asio的定时器用起来根信号集一样简单,但由于它太过简单,也有不方便的地方。比如,在一个UDP伺服器里,一般收到的每个UDP包中都会包含一个sequence number,用于标识该UDP,以应对包处理超时情况。假设每个UDP包处理时间只有100ms,如果超时则直接给客户端返回超时标记。这种最简单的定时器常用的一些Reactor框架都有很完美的解决方案,一般是建一个定时器链表来实现,但是Asio中的定时器没法单独完成这个工作。

boost::asio::deadline_timer只有两种状态:超时和未超时。所以,只能很土的对每个UDP包创建一个定时器,然后借助std::map和boost::shared_ptr保存sequence number到定时器的映射,根据定时器handler的返回值判断该定时器是超时,还是被主动cancel。

strand

在多线程中,多个I/O对象的handler要访问同一块临界区,此时可以使用strand来保证这些handler之间的同步。

示例:

我们向定时器注册 func1 和 func2,它们可能会同时访问全局的对象(比如 std::cout )。这时我们希望对 func1 和 func2 的调用是同步的,即执行其中一个的时候,另一个要等待。

这时就可以用到boost::asio::strand 类,它可以把几个cmd包装成同步执行的。例如,我们向定时器注册 func1 和 func2 时,可以改为:

boost::asio::strand  the_strand;

t1.async_wait(the_strand.wrap(func1));      //包装为同步执行的

t2.async_wait(the_strand.wrap(func2));

这样就保证了在任何时刻,func1 和 func2 都不会同时在执行。

还有就是如果你希望把一个io_service对象绑定到多个线程。此时需要boost::asio::strand来确保handler不会被同时执行,因为异步操作,比如async_write、async_receive_from之类会影响到临界区buffer。

具体可参考asio examples里的示例:HTTPServer 2和HTTP Server 3的connection.hpp设计。

时间: 2024-10-03 14:00:49

C++——boost:asio的使用的相关文章

10 C++ Boost ASIO网路通信库 TCP/UDP,HTTP

  tcp 同步服务器,显示服务器端时间 tcp 同步服务器,提供多种选择 多线程的tcp 同步服务器 tcp 同步客户端 boost 域名地址解析 tcp异步服务器 tcp 异步客户端 UDP同步服务器 UDP同步客户端 UDP异步服务器 UDP异步客户端 HTTP同步客户端 HTTP异步客户端 同步实验: 异步实验 多线程异步实验 tcp 同步服务器,显示服务器端时间 [email protected]:~/boost$ cat main.cpp  #include <ctime> #in

C/C++利用Boost::Asio网络库建立自己的Socket服务器

引言 寸光阴,当下我们或许更需要利用现有的知识,应用现有的技术.网络是当前互联网的根本,了解网络便开始显得极其重要.今天我们利用Boost库中Asio部分,浅尝网络服务器.此处不做过于深入的开展,为达成学习目的,只做简单的异步并发服务器. 注意:本篇代码没有直接引用boost等命名空间,为的是新入门Boost的同学能够更好的了解每个参数在boost的具体命名空间位置,有助于更好的理解boost的布局. 版权所有:_OE_,转载请注明出处:http://blog.csdn.net/csnd_ayo

Boost.ASIO简要分析-1 初窥

Boost.Asio是一个主要用于网络及底层I/O编程的跨平台C++库. 1. 初窥 Boost.Asio支持对I/O对象进行同步及异步操作. 1.1 同步操作 同步操作的事件顺序如下图所示: 1) 调用者调用I/O对象的connect函数开始连接操作,socket.connect(server_endpoint): 2) I/O对象将连接请求传递给io_service: 3) io_service调用操作系统函数: 4) 操作系统返回结果给io_service: 5) io_service将结

boost asio异步读写网络聊天程序客户端 实例详解

// // chat_client.cpp // ~~~~~~~~~~~~~~~ // // Copyright (c) 2003-2013 Christopher M. Kohlhoff (chris at kohlhoff dot com) // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://ww

boost asio 接收数据异常 $/x1

说明 在发送PLAY指令之后,接收到的数据是$/x1,实际上通过调试服务器端,发现服务器端实际上已经了200 OK过来,因此猜测是接收超时,但是在前面的指令收发都没有问题,尝试在PLAY指令发送之后,接收之前调用Sleep函数睡眠500ms,没有任何的效果,查看如何设置socket超时,也没有相关资料,使用的都是同步的收发 测试代码 #include <iostream> #include <fstream> #include <string> #include <

使用Boost asio实现同步的TCP/IP通信

可以先了解一下Boost asio基本概念,以下是Boost asio实现的同步TCP/IP通信: 服务器程序部分,如果想保留套接字之后继续通信,可以动态申请socket_type,保存指针,因为socket_type貌似不能拷贝: #include "stdafx.h" #include <iostream> #include <boost/asio.hpp> using namespace boost::asio; using namespace std;

BOOST中read_some和 boost::asio::error::eof(2)错误

当socket读写完成调用回调函数时候一定要检查 是不是有EOF错误,如果有那么好了,另一方已经断开连接了别无选择,你也断开把. for (;;) { boost::array < char, 128 > buf; boost::system::error_code error; size_t len = socket.read_some(boost::asio::buffer(buf), boost::asio::assign_error(error)); //当服务器关闭连接时,boost

Boost::Asio入门剖析

Boost::Asio可以在socket等I/O对象上执行同步或异步操作,使用Boost::Asio前很有必要了解Boost::Asio.你的程序以及它们交互的过程. 作为一个引导的例子,我们思考一个当一个socket执行连接操作时发生了什么,我们首先开始一个同步的例子 你的程序需要一个io_service对象,io_service把你的程序和操作系统I/O设备链接起来. boost::asio::io_service io_service; 你的程序需要一个I/O对象来执行I/O操作,比如tc

跨平台c++/boost/asio 简单的HTTP POST请求 客户端模型

作为一个呼应,写一个c++版本的同步http post客户端功能,如果你需要纯C版本,移步这里 linux下纯C简单的HTTP POST请求 客户端模型 讲解一下基本的的http post协议 通过\r\n,实现tcp的消息边界 每个请求的第一段 POST /a.b HTTP/1.1 POST http的方法,还有最常用的GET,当然还有其他的几种,略过 /a.b 请求的网页路径,比如如果是首页,最经常的就是/ HTTP/1.1 http协议的版本号,传说中已经出了2了,还有神奇的谷歌出的用来替

boost.asio包装类st_asio_wrapper开发教程(一)

一:什么是st_asio_wrapper它是一个c/s网络编程框架,基于对boost.asio的包装(最低在boost-1.49.0上调试过),目的是快速的构建一个c/s系统: 二:st_asio_wrapper的特点效率高.跨平台.完全异步,当然这是从boost.asio继承而来:自动重连,数据透明传输,自动解决分包粘包问题(必须使用默认的打包解包器,这一特性表现得与udp一样):只支持tcp和udp协议: 三:st_asio_wrapper的大体结构st_asio_wrapper.h:编译器