12.2 Boost-Asio(笔记)


asio 库基于操作系统提供的异步机制,采用前摄器设计模式( Proactor)实现了可移植的异步(或者同步)IO操作,而且并不要求使用多线程和锁定,有效地避免了多线程编程带来的诸多弊端(如条件竞争、死锁等)。

目前asio主要关注与网络通信方面,使用大量的类和函数封装了socket API,提供了一个现代c++风格的网络编程接口,支持TCP、ICMP、UDP等网络通信协议。但asio的异步操作并不局限于网络编程,它还支持串口读写、定时器、SSL等功能,而且asio是一个很好的富有弹性的框架,可以扩展到其他有异步操作的需要的领域。

使用asio不需要编译,但它依赖于其他一些Boost库组件,最基本的是boost.systemboost.datetime 库,用来提供系统错误和时间支持。其他可选库有regexthreadserialization,如果支持SSL,还要额外安装OpenSSL

接下来对asio的介绍将以基本功能为主,即不使用systemdatetime以外的Boost组件。asio位于名字空间boost::asio,因此需要包含的头文件形式如下:

#ifdef _MSC_VER
#define _WIN32_WINNT 0x0501
#endif
#define BOOST_REGEX_NO_LIB
#define BOOST_DATE_TIME_SOURCE
#define BOOST_SYSTEM_NO_LIB
#include <boost/asio.hpp>
using namespace boost::asio;

1 概述

asio库基于前摄器模式(Proactor) 封装了操作系统的selectpoll/epollkqueueoverlapped I/O等机制,实现了异步IO模型。它的核心类是io_service,相当于前摄器模式中的Proactor角色,asio的任何操作都需要有io_service的参与。

在同步模式下,程序发起一个IO操作,向io_service提交请求,io_service把操作转交操作系统,同步地等待。当IO操作完成时,操作系统通知io_service,然后io_service再把结果发回给程序,完成整个同步流程。这个处理流程与多线程的join()等待方式很相似。

在异步模式下,程序除了要发起的IO操作,还要定义一个用于回调的完成处理函数。io_service 同样把IO操作转交给操作系统执行,但它不同步等待,而是立即返回。调用io_servicerun()成员函数可以等待异步操作完成,当异步操作完成时io_service从操作系统获取执行结果,调用完成处理函数。

asio不直接使用操作系统提供的线程,而且定义了一个自己的线程概念:strand,它保证在多线程的环境下代码可以正确地执行,而无需使用互斥量。io_service::strand::wrap()函数可以包装一个函数在strand中执行。

IO操作会经常使用到缓冲区,asio库专门用两个类 mutable_bufferconst_buffer 来封装这个概念,它们可以被安全地应用在异步的读写操作中,使用自由函数buffer()能够包装常用的c++容器类型,如数组、array、vector、string等,用read()write()函数来读取缓冲区。

asio库使用system库的error_codesystem_error来表示程序运行的错误。基本上所有的函数都有两种重载形式,一种形式是有一个error_code的输出参数,调用后必须检查这个参数验证是否发生了错误;另一种形式没有error_code参数,如果发生了错误会抛出system_error异常,调用代码必须使用try-catch块来捕获错误。在实际开发过程中,这两种形式都可以使用,各有利弊。

2 定时器

定时器是asio库里最简单的一个IO模型示范,提供等候时间终止的功能,通过它我们可以快速熟悉asio的基本使用方法。

定时器功能的主要类是deadline_timer,它的类摘要如下:

class deadline_timer
{
public:
    explicit deadline_timer(io_service& io_service);
    deadline_timer(io_service& io_service, const time_type& expiry_time);
    
    void wait();
    template<typename WaitHandler>
    void async_wait(WaitHandler handler);
    std::size_t cancel();
    
    time_type expires_at();
    std::size_t expires_at(const time_type& expiry_time);
    duration_type expires_from_now();
    std::size_t expires_from_now(const duration_type& expiry_time);
};

定时器deadline_timer 有两种形式的构造函数,都要求有一个 io_service 对象,用于提交IO请求,第二个参数是定时器的终止时间,可以是posix_time的绝对时间点或者是自当前时间开始的一个时间长度。

一旦定时器对象创建,它就会立即开始计时,可以使用成员函数wait()来同步等待定时器终止,或者使用async_wait()异步等待,当定时器终止时会调用handler函数。

如果创建定时器不指定终止时间,那么定时器不会工作,可以用成员函数expires_at()expires_from_now()分别设置定时器终止的绝对时间和相对时间,然后再调用wait()async_wait()等待。这两个函数也可以用于获得定时器的终止时间,只需要使用它们的无参重载形式。

定时器还有一个cancel()函数,它的功能是通知所有异步操作取消,转而等待定时器终止。

3 定时器用法

3.1 同步定时器

下面的代码示范了deadline_timer的用法:

#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::asio;		//打开asio名字空间
int main()
{
    io_service ios;						//所有asio程序必须要有一个io_service对象
    deadline_timer t(ios,				//定时器,io_service作为构造函数的参数
       posix_time::seconds(2));			//两秒钟后定时器终止
    cout << t.expires_at() << endl;		//查看终止的绝对时间
    
    t.wait();							//调用 wait()同步等待
    cout << "hello asio" << endl;		//输出一条消息
}

定时器同步等待两秒钟,当等待结束后输出一条信息,然后程序结束。

可以把它与thread库的sleep()函数对比研究,两者虽然都是等待,但是内部机制完全不同:thread库的sleep()使用了互斥量和条件变量,在线程中等待,而asio则是调用了操作系统的异步机制,如selectepoll等完成的。

同步定时器的用法很简单,但它演示了asio程序的基本结构和流程:

  • 一个asio程序首先要定义一个 io_service对象,它是前摄器模式中最重要的proactor角色
  • 声明一个IO操作(在这里是定时器),并把它挂接在io_service上,
  • 然后就可以执行后续的同步或异步操作。

3.2 异步定时器

接下来研究异步定时器,代码大致与同步定时器相等,增加了回调函数,并使用io_service.run()和定时器的async_wait()方法。

我们要定义回调函数,asio库要求回调函数只能有一个参数,而且这个参数必须是const asio::error_code &类型:

void print(system::error_code& /*e*/)
{
    cout << "hello asio" << endl;
}

随后的异步定时器代码也同样很简单:

int main()
{
    io_service ios;									//io_service 对象
    deadline_timer t(ios, posix_time::seconds(2));	//定时器
    
    t.async_wait(print);							//异步等待,传入回调函数,立即返回
    
    cout << "it show before t expired." << endl;
    ios.run();										//很重要!异步IO必须!
}

异步定时器的代码虽然变化不多,但实现的功能却有本质的差别。

代码的前两行与同步定时器相同,这是所有asio程序基本的部分。重要的是异步等待async_wait(),它通知io_service异步地执行IO操作,并且注册了回调函数,用于在IO操作完成时由事件多路分离器分派返回值(error_code)调用。

必须调用io_servicerun()成员函数,它启动前摄器的事件处理循环,阻塞等待所有的操作完成并分派事件。如果不调用run()那么虽然操作被异步执行了,但没有一个等待它完成的机制,回调函数将得不到执行机会。

当定时器时间到终止时 io_service 将调用被注册的print(),输出一条消息,然后程序结束。

3.3 异步定时器使用 bind

异步定时器中由于引入了回调函数,因此产生了很多的变化,我们可以增加回调函数的参数,使它能够做更多的事情。但async_wait()接受的回调函数类型是固定的,必须使用bind库来绑定参数以适配它的接口。

实现一个可以定时执行任意函数的定时器 a_timer(asyc timer),它持有一个asio定时器对象和一个计数器,还有一个function对象用来保存回调函数:

class a_timer
{
private:
    int count, count_max;			//计数器成员变量
    function<void()> f;				//function对象,持有无参无返回的可调用物
    deadline_timer t;				//asio 定时器对象

a_timer 的构造函数初始化成员变量,将计数器清零,设置计数器的上限,拷贝存储回调函数,并立即启动定时器。

之所以要"立即"启动,是因为我们必须保证在io_service.run()之前至少有一个异步操作在执行,否则io_service.run()会因为没有事件处理而立即不等待返回:

public:
	template<typename F>						//模板类型,可以接受任意可调用物
	a_timer(io_service& ios,int x,F func):
	f(func),count_max(x),count(0),				//初始化回调函数和计数器
	t(ios, posix_time::millisec(500))			//启动计时器
    {
        t.async_wait(bind(&a_timer::call_func,	//异步等待计数器
                   this,placeholders::error));	//注册回调函数
    }

注意在async_wait()bind的用法,call_funca_timer的一个成员函数,因此我们需要绑定this指针,同时我们还使用了asio下子名字空间placeholders下的一个占位符error,它的作用类似于bind库的占位符_1、_2,用于传递error_code值。

接下来是a_timer的主要功能函数call_func(),它符合async_wait()对回调函数的要求,有一个error_code参数,当定时器终止时它将被调用执行。

call_func函数内部累加计数器,如果计数器未达到上限,则调用function对象f,然后重新设置定时器的终止时间,再次异步等待被调用,从而达到反复执行的目的:

void call_func(const system::error_code&)
{
    if(count >= count_max)			//如果计数器达到上限则退出
    {
        return;
    }
    ++count;
    f();							//调用function对象
    //设置定时器的终止时间为0.5秒之后
    t.expires_at(t.expires_at() + posix_time::millisec(500));		
    //再次启动定时器,异步等待
    t.async_wait(bind(&a_timer::call_func,this,placeholders::error));
}

a_timer的调用代码非常简单,只需要一个io_service对象即可:

//第一个回调函数
void print1()
{
    cout << "hello asio" << endl;
}
//第二个回调函数
void print2()
{
    cout << "hello boost" << endl;
}
int main()
{
    io_service ios;					//io_service 对象
    a_timer at1(ios, 5, print1);	//启动第一个定时器
    a_timer at2(ios, 5, print2);	//启动第二个定时器
    ios.run();						//io_service等待异步调用结束
}

4 网络通信简述

asio库支持TCPUDPICMP通信协议,它在名字空间boost::asio::ip里提供了大量的网络通信方面的函数和类,很好地封装了原始的Berkeley Socket API,展现给asio用户一个方便易用且健壮的网络通信库,下面的论述主要针对使用最广泛的TCP协议。

ip::tcp类是asio网络通讯(TCP)部分主要的类,但它本身并没有太多的功能,而是定义了数个用于TCP通讯的typedef类型,用来协作完成网络通信。这些typedef包括端点类endpoint、套接字类socket、流类iostream,以及接受器acceptor、解析器resolver等等。从某种程度上来看,ip::tcp类更像一个名字空间。

ip::tcp的类摘要如下:

class tcp
{
public:
	//The type of a TCP endpoint
    typedef basic_endpoint<tcp> endpoint;
    //The type of resolver query
    typedef basic_resolver_query<tcp> resolver_query;
    //The type of a resolver iterator
    typedef basic_resolver_iterator<tcp> resolver_iterator;
    //The TCP socket type
    typedef basic_stream_socket<tcp> socket;
    //The TCP acceptor type
    typedef basic_socket_acceptor<tcp> acceptor;
    //The TCP resolver type
    typedef basic_resolver<tcp> resolver;
    //The TCP iostream type
    typedef basic_socket_iostream<tcp> resolver;
    //Obtain an identifier for the type of the protocol
    int type() const;
    //Obtain an identifier for the protocol
    int protocol() const;
    //Obtain an identifier for the protocol family
    int family() const;
    
    //Construct to represent the IPv4 TCP protocol
    static tcp v4();
    //Construct to represent the IPv6 TCP protocol
    static tcp v6();
};

5 IP地址和端点

IP地址独立于TCPUDP等通信协议,asio库使用类ip::address来表示IP地址,可以同时支持ipv4ipv6两种地址,它的类摘要如下:

class address
{
public:
	address();
    address(const address& other);
    
    bool is_v4() const;
    bool is_v6() const;
    
    ip::address_v4 to_v4() const;
    ip::address_v6 to_v6() const;
    
    string to_string() const;
    
    static address (const char* str);
    static address from_string(const string& str);
    
    friend bool operator==(const address& a1,const address& a2);
    ...						//其他比较操作和流输出操作
};

address类最重要的方法是静态成员函数from_string(),它是一个工厂函数,可以从字符串产生ip地址,地址的版本则可以用is_v4()is_v6()来检测。相应地,address也有一个to_string()函数,可以把ip地址转换成字符串。

address类的用法示例:

ip::address addr;							//声明一个ip地址对象
addr = addr.from_string("127.0.0.1");		//从字符串产生ip地址
assert(addr.is_v4());						//ipv4的地址
cout << addr.to_string() << endl;			//转换成字符串输出
addr = addr.from_string("ab::12::34::56");	//ipv6的地址字符串
assert(addr.is_v6());

有了ip地址,再加上通信用的端口号就构成了一个socket端点,在asio库中用ip::tcp::endpoint类来表示。它的主要用法就是通过构造函数创建一个可用于socket通信的端点对象,端点的地址和端口号可以用address()port()获得,例如:

ip::address addr;							//ip地址对象
addr = addr.from_string("127.0.0.1");		//一个ipv4的地址
ip::tcp::endpoint ep(addr,6688);			//创建端点对象,端口为6688
assert(ep.address() == addr);
assert(ep.port() == 6688);

6 同步socket处理

ip::tcp的内部类型socketacceptorresolverasioTCP通信中最核心的一组类,它们封装了socket的连接、断开和数据收发功能,使用它们可以很容易地编写出socket程序。

socket类是TCP通信的基本类:

  • 调用成员函数connect()可以连接到一个指定的通信端点
  • 连接成功后用local_endpoint()remote_endpoint()获得连接两端的端点信息
  • read_some()write_some()阻塞读写数据
  • 当操作完成后使用close()函数关闭socket。如果不关闭socket,那么在socket对象析构时也会自动调用close()关闭。

acceptor类对应socket APIaccept()函数功能,它用于服务器端,在指定的端口号接受连接,必须配合socket类才能完成通信。

resolver类对应的socket APIgetaddrinfo()系列函数,用于客户端解析网址获得可用的IP地址,解析得到的IP地址可以使用socket对象连接。

下面使用socket类和acceptor类来实现一对同步通信的服务器和客户端程序。

6.1 服务器端

服务器端程序,它使用一个acceptor对象在6688端口接受连接,当有连接时使用一个socket对象发送一个字符串。

int main()
{
    try											//function-try块
    {
		cout << "server start." << endl;
        io_service ios;							//asio程序必需的io_service对象
        
        ip::tcp::acceptor acceptor(ios,			//创建acceptor对象,ipv4
        ip::tcp::endpoint(ip::tcp::v4(),6688));	//接受6688端口
        cout << acceptor.local_endpoint().address() << endl;
        
        while(true)								//循环执行服务
        {
			ip::tcp::socket sock(ios);			//一个socket对象
            acceptor.accept(sock);				//阻塞等待socket连接
            
            cout << "client:";
            cout << sock.remote_endpoint().address() << endl;
            
            sock.write_some(buffer("hello asio"));	//发送数据
        }
    }
    catch(std::exception& e)			//捕获可能发生的异常
    {
		cout << e.what() << endl;
    }
}

服务器端程序里要注意的是自由函数buffer(),它可以包装很多种类的容器成为asio组件可用的缓冲区类型。通常我们不能直接把数组、vector等容器用作asio的读写参数,必须使用buffer()函数包装。

6.2 客户端

客户端程序,使用上面定义的a_timer类来实现定时调用功能,为此需要一个可被调用的函数client()

void client(io_service &ios)							//传入io_service对象
{
    try
    {
        cout << "client start." << endl;
        
        ip::tcp::socket sock(ios);				//创建socket对象
        //创建连接端点
        ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"),6688);
        
        sock.connnect(ep);						//socket连接到端点
        
        vector<char> str(100,0);				//定义一个vector缓冲区
        sock.read_some(buffer(str));			//使用buffer()包装缓冲区接收数据
        cout << "recive from" << sock.remote_endpoint().address();
        cout << &str[0] << endl;				//输出接收到的字符串
    }
    catch(std::exception& e)
    {
        cout << e.what() << endl;
    }
}

然后我们在main()函数中创建io_service对象,用定时器启动socket客户端:

int main()
{
    io_service ios;								//io_service对象
    a_timer at(ios,5,bind(client,ref(ios)));	//启动定时器
    ios.run();
}

7 异步socket处理

本小节我们把刚才的同步socket程序改为异步调用方式。异步程序的处理流程与同步程序基本相同,只需要把原有的同步调用函数换成前缀是async_的异步调用函数,并增加回调函数,在回调函数中再启动一个异步调用。

7.1 服务器端

首先实现异步的服务器端程序,定义一个server类,它实现异步服务的所有功能。

class server
{
private:
    io_service &ios;
    ip::tpc::acceptor acceptor;
    typedef shared_ptr<ip::tcp::socket> sock_pt;

server类必需的成员变量是io_service对象和一个acceptor对象,他们是TCP通信的必备要素,随后我们定义了一个智能指针的typedef,它指向socket对象,用来在回调函数中传递。

public:
	server(io_service& io): ios(io),
		acceptor(ios,ip::tcp::endpoint(ip::tcp::v4(),6688))
        {start();}

server的构造函数存储io_service对象,使用iostcp协议和端口号初始化acceptor对象,并用start()函数立即启动异步服务。

void start()
{
    sock_pt sock(new ip::tcp::socket(ios));				//智能指针
    acceptor.async_accept(*sock,
       bind(&server::accept_handler,this,placeholders::error,sock));
    													//异步侦听服务
}

start()函数用于启动异步接受连接,需要调用acceptorasync_accept()函数。为了能够让socket对象能够被异步调用后还能使用,我们必须使用shared_ptr来创建socket对象的智能指针,它可以在程序的整个声明周期中存在,直到没人使用它为止。

当有TCP连接发生时,server::accept_handler()函数将被调用,它使用socket对象发送数据。

void accept_handler(const system::error_code& ec,sock_pt sock)
{
    if(ec)												//检测错误码
    {
        return;
    }
    
    cout << "client:";									//输出连接的客户端信息
    cout << sock->remote_endpoint().address() << endl;
    sock->async_write_some(buffer("hello asio"),
         bind(&server::write_handler,this,placeholders::error));
    start();											//再次启动异步接受连接
}

accept_handler()对应同步处理中的发送数据部分,但因为是异步调用,所以代码略有不同。

首先它必须检测asio传递的error_code,保证没有错误发生。然后调用socket对象的async_write_some()异步发送数据。同样,我们必须再为这个异步调用编写回调函数write_handler()。当发送完数据后,不要忘记调用start()再次启动服务器接受连接,否则当完成数据发送后io_service将因为没有事件处理而结束运行。

发送数据的回调函数write_handler()很简单,因为我们不需要再做更多的工作,可以直接实现一个空函数,在这里我们简单地输出一条消息,表示异步发送数据完成。

	void write_handler(const system::error_code&)
    {
        cout << "send msg complete." << endl;
    }
};														//server类结束

最后在main()函数中创建io_service对象和server对象,调用run()方法开始异步等待:

int main()
{
    try
    {
        cout << "server start." << endl;
        io_service ios;								//io_service对象
        
        server serv(ios);							//构造server对象
        ios.run();									//启动异步调用事件处理循环
    }
    catch(std::exception& e)
    {
        cout << e.what() << endl;
    }
}

7.2 客户端

通常客户端不需要使用异步通信,但是处于演示目的,在这里也实现异步的客户端,进一步示范asio的异步用法。

与服务器对象,我们需要定义一个client类,它实现所有异步调用功能。

class client
{
private:
    io_service& ios;							//io_service对象
    ip::tcp::endpoint ep;						//TCP端点
    typedef shared_ptr<ip::tpc::socket> sock_pt;

clientserver类相似,都必须持有一个io_service的引用,也要有一个socketshare_ptr,不同的是client不需要acceptor,而是使用一个端点类直接与服务器建立连接。

public:
	client(io_service& io):ios(io),
		ep(ip::address::from_string("127.0.0.1"),6688)
        {
            start();							//启动异步连接
        }

client构造函数的主要作用是初始化IP端点对象,并调用start()函数启动TCP连接:

void start()
{
    sock_pt sock(new ip::tcp::socket(ios));		//创建socket对象
    sock->async_connect(ep,						//异步连接
        bind(&client::conn_handler,this,placeholders::error,sock));
}

start()创建一个socket对象的智能指针以便在异步调用过程中传递,然后使用async_connect()启动一个异步连接,指定连接的处理函数是conn_handler()

void conn_handler(const system::error_code& ec,sock_pt sock)
{
    if(ec)										//处理错误代码
    {
        return;
    }
    
    cout << "recive from" << sock->remote_endpoint.address();
    //建立接受数据的缓冲区
    shared_ptr<vector<char>> str(new vector<char>(100,0));
    //异步读取数据
    sock->async_read_some(buffer(*str),
          bind(&client::read_handler,this,placeholders::error,str));
    start();				//再次启动异步连接
}

当异步连接成功时,conn_hanlder()将被调用,它再用shared_ptr包装vector,用buffer()函数把vector作为接收数据的缓冲区,由async_read_some()异步读取,然后再启动一个异步连接。

当异步读取结束时,read_handler()被调用,它直接输出shared_ptr指向的缓冲区内容:

    void read_handler(const system::error_code& ec,shared_ptr<vector<char>> str)
    {
        if(ec)								//处理错误代码
        {
            return;
        }
		cout << &(*str)[0] << endl;			//输出接收到的数据
    }
};								//client类结束

客户端的main()函数代码与服务端的完全一致:

int main()
{
    try
    {
        cout << "client start." << endl;
        io_service ios;
        client cl(ios);
        ios.run();
    }
    catch(std::exception& e)				//捕获可能发生的异常
    {
        cout << e.what() << endl;
    }
}

8 查询网络地址

之前关于TCP通信的所有论述,我们都是使用直接的IP地址,但在实际生活中大多数时候,我们都不可能知道socket连接另一端的地址,而只有一个域名,这时候我们就需要使用resolver类来通过域名获得可用的IP,它可以实现与IP版本无关的网址解析。

resolver使用内部类queryiterator共同完成查询IP地址的工作:首先使用网址和服务名创建query对象,然后由resolve函数生成iterator对象,它代表了查询到的ip端点。之后就可以使用socket对象尝试连接,直到找到一个可用的为止。

在这里我们使用一个函数resolv_connect()来封装resolver的调用过程,代码如下:

#include <boost/lexical_cast.hpp>					//使用字符串转换功能
void resolv_connect(ip::tcp::socket &sock,			//socket对象
                   const char* name,int port)		//网址和端口号
{
    ip::tcp::resolver rlv(sock.get_io_service());	//resolver对象
    ip::tcp::resolver::query qry(name,lexical_cast<string>(port));		//创建一个query对象
    
    //使用resolve()开始迭代端点
    ip::tcp::resolver::iterator iter = rlv.resolve(qry);
    ip::tcp::resolver::iterator end;				//逾尾迭代器
    system::error_code ec = error::host_not_found;
    for(;ec && iter != end;++iter)
    {
        sock.close();
        sock.connect(*iter,ec);
    }
    if(ec)
    {
        count << "can't connect." << endl;
        throw system::system_error(ec);
    }
    cout << "connect success." << endl;
}

resolv_connect()函数中,使用了lexical_cast,这是因为query对象只接受字符串参数,所以我们需要把端口号由整数转换为字符串。

当开始resolver的迭代时,我们需要使用error_code和逾尾迭代器两个条件来控制循环,因为有可能迭代完成所有解析到的端点都无法连接,只有当error_code为0才表示连接成功。

有了resolv_connect()函数,我们就可以不受具体IP地址值的限制,以更直观更灵活的域名来连接服务器,例如:

int main()
{
    try
    {
        io_service ios;
        ip::tcp::socket sock(ios);
        resolv_connect(ios,sock,"www.boost.org",80);
        ...		//其他操作
        ios.run();
    }
    catch(std::exception& e)
    {
        cout << e.what() << endl;
    }
}

resolver不仅能够解析域名,也支持使用IP地址和服务名,例如:

ip::tcp::resolver::query qry("127.0.0.1","http");

9 高级议题

9.1 超时处理

使用定时器,我们可以在网络通信中实现超时处理,只需要在异步调用后声明一个deadline_timer对象,然后设定它的等待时间和回调函数就可以,用法很像thread库的time_join()的函数。

例如,下面的代码异步连接服务器,如果超过5秒钟还没有完成所有操作则强制关闭socket:

void conn_handler(const system::error_code&)					//连接处理函数
{...}
void time_expired(const system::error_code&,ip::tcp::socket *sock)		//超时处理函数
{
    cout << "time expired" << endl;
    sock->close();												//关闭socket
}
int main()
{
    io_service ios;
    ip::tcp::socket sock(ios);
    ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"),6688);
    
    sock.async_connect(ep,&conn_handler);				//异步连接
    deadline_timer t(ios,posix_time::seconds(5));		//开始计时
    t.async_wait(bind(time_expired,placeholders::error,&sock));		//异步等待超时
    ...						//其他操作
    ios.run();											//进入异步时间循环
}

9.2 流操作

对于有连接的TCP协议,asio库专门提供了一个ip::tcp::iostream类来简化socket通信。ip::tcp::iostreamstd::basic_iostream的子类,可以像标准流一样操作,它内部集成了resolver的域名解析功能和acceptor的接受连接功能,能够非常简单地完成TCP通信。

使用流操作的客户端可以是这样:

int main()				//客户端主函数
{
    for(int i=0;i<5;++i)		//循环连接5次
    {
        //连接到本机6688端口
        ip::tcp::iostream tcp_stream("127.0.0.1","6688");
        string str;
        getline(tcp_stream,str);						//从tcp流读取一行数据
        cout << str << endl;
    }
}

使用流操作的服务器端可以是这样:

int main()				//服务器端主函数
{
    io_service ios;
    ip::tcp::endpoint ep(ip::tcp::v4(),6688);
    ip::tcp::acceptor acceptor(ios,ep);
    
    while(true)
    {
        ip::tcp::iostream tcp_stream;
        acceptor.accept(*tcp_stream.rdbuf());
        tcp_stream << "hello tcp stream";
    }
}

9.3 UDP协议通信

asio中的udp协议通信与tcp处理流程类似,但因为udp协议是无连接的,故不需要建立连接,使用send_to()receive_from()就可以直接通过端点发送数据。

示范同步的udp客户端和服务器用法(省略了异常处理的try-catch块)的代码如下:

服务器端代码

int main()
{
    cout << "udp server start." << endl;
    io_service ios;
    
    //创建一个udp的socket对象
    //使用ipv4协议,端点6699
    ip::udp::socket sock(ios,ip::udp::endpoint(ip::udp::v4(),6699));
    //开启服务主循环
    while(true)
    {
        char buf[1];				//一个临时用缓冲区
        ip::udp::endpoint ep;		//要接受连接的远程端点
        
        system::error_code ec;
        //阻塞等待远程连接,连接的端点
        //信息保存在ep对象中
        sock.receive_from(buffer(buf),ep,0,ec);
        if(ec && ec != error::message_size){
            throw system::system_error(ec);
        }
        cout << "send to" << ep.address() << endl;		//得到远程端点
        sock.send_to(buffer("hello asio udp"),ep);		//发送数据
    }
}

客户端代码

int main()
{
    cout << "client start." << endl;
    io_service ios;
    //连接端点
    ip::udp::endpoint send_ep(ip::address:from_string("127.0.0.1"),6699);
    //创建udp socket 对象
    ip::udp::socket sock(ios);
    //使用ipv4打开socket
    sock.open(ip::udp::v4());
    //向连接端点发送连接数据
    char buf[1];
    sock.send_to(buffer(buf),send_ep);
    
    //接收数据
    vector<char> v(100,0);
    ip::udp::endpoint recv_ep;
    sock.receive_from(buffer(v),recv_ep);
    cout << "recv from" << recv_ep.address() << " ";
    cout << &v[0] << endl;
}

9.4 串口通信

asio除了"主营业务"网络通信外,也支持串口通信,需要使用serial_port类。串口通信的基本处理流程与网络通信类似,但要在通信前设置好波特率、奇偶校验位等串口通信参数。

示范串口通信基本用法的代码如下:

//异步处理函数
void time_exipred(system::error_code& ec,serial_port *sp)
{
    cout << "time_exipred" << endl;
    sp->close();
}
void read_handler(system::error_code& ec)
{
    cout << ec.message() << endl;
}
//主函数
int main()
{
    io_service ios;
    serial_port sp(ios,"COM1");				//打开串口 COM1
    
    //设置串口参数
    sp.set_option(serial_port::baud_rate(9600));
    sp.set_option(serial_port::flow_control(serial_port::flow_control::none));
    sp.set_option(serial_port::parity(serial_port::parity::none));
    sp.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
    sp.set_option(serial_port::character_size(8));
    
    size_t len = sp.write_some(buffer("hello serial"));			//向串口写数据
    cout << len << endl;
    
    //异步接收数据
    vector<char> v(100);
    sp.async_read_some(buffer(v),bind(read_handler,placeholders::error));
    
    deadline_timer t(ios,posix_time::seconds(2));				//处理超时
    t.async_wait(bind(time_exipred,placeholders::error,&sp));
    
    ios.run();		//启动事件等待循环
}

上面代码中定时器超时处理回调函数也可以直接调用serial_portcancel()close()方法,如:

t.async_wait(bind(&serial_port::cancel,ref(sp)));

或:

t.async_wait(bind(&serial_port::close,ref(sp)));

10 总结

Boost库介绍的并发编程章节,讨论了该方面的两个库,它们是threadasio,分别关注多线程和异步IO这两个领域。

thread库使用多个成熟的多线程范式为我们提供了可移植的多线程处理能力。c++历来缺乏操纵线程的能力,经常要求助于C API,而现在thread库填补了这个空白。它实现了目前多线程编程所需要的绝大部分概念,包括互斥锁、递归锁、读写锁、条件变量、线程、barrier、future等等,功能丝毫不逊色于POSIX API和其他多线程库,而且接口非常方便易用,并且能够配合refbindfunction等其他Boost程序库提供更灵活、优雅、坚固的解决方案。

拥有了thread库这个强有力的武器,c++程序员只需要在了解一些多线程开发基本规则,就能够轻而易举地构建出高效的多线程程序。

asio库目前还处于发展完善的过程中,它使用前摄器模式实现了同步或异步的IO操作,并提供了定时器、网络通信、串口通信等许多IO操作。

前摄器模式非常复杂,难以构建和应用,而asio基于操作系统的异步IO能力简洁而高效地实现了前摄器模式,并将细节封装在io_servicebasic_io_objectsocket等对象中,使库用户得到了一个高度可用的接口。asio还很好的封装了Barkeley Socket API,完整地支持TCP/UDP/ICMP等通信协议,有效地避免了误用复杂的原始Socket API的问题,很有可能纳入将来的c++标准,称为标准的网络底层库。

thread库和asio库都可以用于并发编程,但它们解决问题的途径确不一样。thread使用的是进程内部的线程机制,很少需要操作系统内核干预,只要掌握了线程的同步方法,多线程程序的结构很容易理解也很容易实现。而asio使用的是异步事件处理机制,与操作系统的内核密切相关,使用它需要对操作系统的底层机制有一定了解,比多线程程序更难于编写难于调试,但由于把异步操作的管理工作交由操作系统处理,因而能够获得更高的运行性能。

threadasio有竞争也有合作,可以在多线程环境中同步地使用asio,获得并发处理的能力又不涉及复杂的异步调用机制,也可以在asio异步调用中启动多线程,使异步操作可以运用于多个线程。

本章并没有覆盖Boost在并发编程领域的所有内容,没有介绍另一个很重要的库——处理进程间通信(IPC)的interprocessthreadasio这两个库也只是不完全的介绍,如果读者向更深入地了解它们的强大功能,请阅读Boost文档和源代码。

本章实现的使用函数和类如下:

  • basic_atom<>:它是一个原子操作的整数包装类,可以被安全地用在多线程环境中当做计数器;
  • a_timer:它基于asio库和function库提供了可定时调用任意函数的功能;
  • resolv_connect():它封装了域名解析类resolver的调用步骤,可以直接通过域名和端口号连接socket
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值