简介

关于Boost的安装,请参考 Linux便捷安装boost

总括地说,Boost Asio是一个跨平台的、主要用于网络和其他一些底层输入/输出编程的C++库。

Boost.Asio在2003被开发出来,然后于2005年的12月引入到Boost 1.35版本中。依赖于如下的库:

  • Boost.System:这个库为Boost库提供操作系统支持。
  • Boost.Regex:使用这个库(可选的)以便你重载read_until()或者async_read_until()时使用boost::regex参数。
  • Boost.DateTime:使用这个库(可选的)以便你使用Boost.Asio中的计时器
  • OpenSSL:使用这个库(可选的)以便你使用Boost.Asio提供的SSL支持。

作为一个跨平台的库,Boost.Asio可以在大多数操作系统上使用。能同时支持数千个并发的连接。它提供了一套可以支持传输控制协议(TCP)socket、用户数据报协议(UDP)socket和Internet控制消息协议(IMCP)socket的API,而且如果有需要,你可以对其进行扩展以支持你自己的协议。

同步与异步

在学习asio之前,有必要搞清楚同步和异步之间的区别。同步编程和异步编程是有很大不同的。

在我们刚学习编程的时候,大部分的练习都是同步的,即所有的程序操作都是顺序执行的。

如从socket中读取数据,然后写入响应到socket中。每个操作都是阻塞的,在一个操作未完成前,不会执行下一个操作。

为了不影响主线程的运行,当执行可能阻塞的操作时,通常会创建线程来处理。所以在同步编程中,多线程编程的模式往往必不可少。

相反,异步编程是事件驱动的。对于socket读取来说(或者其他可能阻塞的操作),应用程序不知道该操作何时会结束,但它会提供一个回调给应用,当操作完成时,它会调用这个API,并返回操作结果。

因为是事件驱动的,不需要阻塞等待执行结果,所以只需要一个线程。

从同步转变到异步或者相反的转变,改动较大且容易出错。所以最好在项目初期就确定何种编程模式。

总之:

  • 同步编程和异步编程的API区别较大,确定后不要随意切换
  • 同步编程往往多线程,但容易理解和调试
  • 异步编程往往单线程,但较复杂且难以调试
简单示例

在进一步讲解asio的功能之前,先来看一下使用asio进行网络编程的简单示例,从概念和代码上加深理解。

本示例中包含一个同步版本的客户端和服务端,以及一个异步版本的客户端和服务端。仅包含代码片断。

同步版本:
// 客户端
using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.connect(ep);

// 服务端
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001
ip::tcp::acceptor acc(service, ep);
while ( true) {
    socket_ptr sock(new ip::tcp::socket(service));
    acc.accept(*sock);
    boost::thread( boost::bind(client_session, sock));
}
void client_session(socket_ptr sock) {
    while ( true) {
        char data[512];
        size_t len = sock->read_some(buffer(data));
        if ( len > 0)
            write(*sock, buffer("ok", 2));
    }
}

分析:

  1. 客户端
  • 至少需要一个io_service实例。Boost.Asio使用io_service同操作系统的输入/输出服务进行交互。
  • 通常一个io_service的实例就足够了。然后,创建连接的地址和端口,再建立socket。把socket连接到创建的地址和端口。
  1. 服务端
  • 至少需要一个io_service实例。
  • 指定监听的端口,再创建一个接收器——一个用来接收客户端连接的对象。
  • 在接下来的循环中,创建一个虚拟的socket来等待客户端的连接。然后当一个连接被建立时,创建一个线程来处理这个连接。
  • 在client_session线程中来读取一个客户端的请求,进行解析,然后返回结果。
异步版本
// client
using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.async_connect(ep, connect_handler);
service.run();
void connect_handler(const boost::system::error_code & ec) {
    // 如果ec返回成功我们就可以知道连接成功了
}

// server
using boost::asio;
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // 监听端口2001
ip::tcp::acceptor acc(service, ep);
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
service.run();
void start_accept(socket_ptr sock) {
    acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) );
}
void handle_accept(socket_ptr sock, const boost::system::error_code &err) {
    if ( err) return;
    // 从这里开始, 你可以从socket读取或者写入
    socket_ptr sock(new ip::tcp::socket(service));
    start_accept(sock);
}

分析:

  1. 客户端
  • 至少需要一个io_service实例。需要指定连接的地址及创建socket。
  • async_connect异步连接会立即返回,当连接真正完成时,会调用注册好的connect_handler函数。
  • 特别注意,真正的连接会在service.run();之后进行,只要还有待处理的异步操作,循环就不会退出。上例会在连接执行成功后退出。
  • 当connect_handler函数被调用时,检查错误码(ec),如果成功,就可以写入数据。
  • 记住:每一个异步操作都有一个完成处理程序——一个操作完成之后被调用的函数。
  1. 服务端
  • 首先创建一个io_service实例。指定监听的端口,再创建一个接收器acc,创建socket,异步等待客户端连接到来。
  • 运行异步service.run()循环。
  • 当接收到客户端连接时,handle_accept被调用(调用async_accept的完成处理程序)。如果没有错误,这个socket就可以用来做读写操作。
  • 在使用这个socket之后,你创建了一个新的socket,然后再次调用start_accept(),用来创建另外一个“等待客户端连接”的异步操作。
  • 以上操作使service.run()循环一直保持忙碌状态,而不会退出。
异常处理和错误代码

在上面的示例中,使用了ec表示errorcode错误码。

Boost.Asio允许同时使用异常处理或者错误代码,所有的异步函数都有抛出错误和返回错误码两种方式的重载。当函数抛出错误时,它通常抛出boost::system::system_error的错误。

例如,下面的代码片断:

try {
    sock.connect(ep);
} catch(boost::system::system_error e) {
    std::cout << e.code() << std::endl;
}

和下面的代码片断相同:

boost::system::error_code err;
sock.connect(ep, err);
if ( err)
    std::cout << err << std::endl;

在异步函数中,可以使用异常处理或者错误码(随心所欲),但要保持一致性。

同时使用这两种方式会导致问题,大部分时候是崩溃(当你不小心出错,忘记去处理一个抛出来的异常时)。

如果你的代码很复杂(调用很多socket读写函数),你最好选择异常处理的方式,把你的读写包含在一个函数try {} catch块里面。

Boost.Asio的所有错误码都包含在?的命名空间中(以便你创造一个大型的switch来检查错误的原因)。如果想要了解更多的细节,请参照boost/asio/error.hpp头文件。

Boost.Asio中的线程

关于Boost.Asio的线程,主要有以下几点需要说明:

  1. io_service
  • io_service是线程安全的。
  • 几个线程可以同时调用io_service::run()。
  • 如果在一个单线程函数中调用io_service::run(),这个函数必须等待所有异步操作完成之后才能继续执行(即,线程可能会阻塞)。
  • 一般会在多个线程中调用io_service::run()。这会阻塞所有调用io_service::run()的线程。
  • 只要任何一个线程调用了io_service::run(),所有的回调都会同时被调用;这也就意味着,当你在一个线程中调用io_service::run()时,所有的回调都被调用了。
  1. socket
  • socket类不是线程安全的。
  • 要避免在某个线程里读一个socket时,同时在另外一个线程里面对其进行写入操作。(通常来说这种操作都是不推荐的,更别说Boost.Asio)。
  1. utility
  • 不是线程安全的,所以通常也不提倡在多个线程里面同时使用。
  • 里面的方法经常只是在很短的时间里面使用一下,然后就释放了。

最后一点要说明的是,除了自己创建的线程,Boost.Asio本身也包含几个线程。但是可以保证的是那些线程不会调用你的代码。这也意味着,只有调用了io_service::run()方法的线程才会调用回调函数。

网络通信之外

除了网络通信,Boost.Asio还包含了其他的I/O功能。

  1. 信号量

Boost.Asio支持信号量,比如SIGTERM(软件终止)、SIGINT(中断信号)、SIGSEGV(段错误)等,如下:

// 捕捉并处理SIGINT
void signal_handler(const boost::system::error_code & err, int signal)
{
    // 纪录日志,然后退出应用
}
boost::asio::signal_set sig(service, SIGINT, SIGTERM);
sig.async_wait(signal_handler);
  1. 串口

使用Boost.Asio轻松地连接到一个串行端口。在Windows上端口名称是COM7,在POSIX平台上是/dev/ttyS0。如下:

// 打开端口
io_service service;
serial_port sp(service, "COM7");

// 设置端口选项, 如端口的波特率、奇偶校验和停止位
serial_port::baud_rate rate(9600);
sp.set_option(rate);

// 读写
char data[512];
read(sp, buffer(data, 512));
  1. 操作文件

Boost.Asio也可以连接到Windows的文件,然后同样使用自由函数,比如read(), asyn_read()等,如下:

HANDLE h = ::OpenFile(...);
windows::stream_handle sh(service, h);
char data[512];
read(h, buffer(data, 512));

对于POXIS文件描述符,比如管道,标准I/O和各种设备(但不包括普通文件)你也可以这样做,就像下面的代码所做的一样:

posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO));
char data[512];
read(sd_in, buffer(data, 512));
  1. 定时器

其实,在网络编程中,也经常需要使用定时器,如某操作的超时、定时任务等。使用方式如下:

// 使用boost asio
deadline_timer t(service, boost::posix_time::milliseconds(500));
t.wait();

// 以上代码等价于:
boost::this_thread::sleep(boost::posix_time::milliseconds(500));

也可用于定时任务,使用方式:

boost::asio::deadline_timer m_timer;

m_timer.expires_from_now(boost::posix_time::seconds(1));
m_timer.async_wait([]() { cout << "here again" << endl; });
io_service类

前面已经看到了io_service类的使用,它是asio库里最重要的类,主要功能如下:

  • 负责和操作系统打交道
  • 等待所有异步操作的结束
  • 为每一个异步操作调用其完成处理程序

本节主要讨论io_service类在异步操作中的应用。主要有以下几种应用模式:

  1. 一个io_service实例和一个处理线程的单线程
io_service service; // 所有socket操作都由service来处理 
ip::tcp::socket sock1(service); // all the socket operations are handled by service 
ip::tcp::socket sock2(service); 
sock1.asyncconnect( ep, connect_handler); 
sock2.async_connect( ep, connect_handler); 
deadline_timer t(service, boost::posixtime::seconds(5));
t.async_wait(timeout_handler); 
service.run();
  1. 一个io_service实例和多个处理线程的多线程
io_service service;
ip::tcp::socket sock1(service);
ip::tcp::socket sock2(service);
sock1.asyncconnect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service, boost::posixtime::seconds(5));
t.async_wait(timeout_handler);
for ( int i = 0; i < 5; ++i)
    boost::thread( run_service);
void run_service()
{
    service.run();
}
  1. 多个io_service实例和多个处理线程的多线程
io_service service[2];
ip::tcp::socket sock1(service[0]);
ip::tcp::socket sock2(service[1]);
sock1.asyncconnect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service[0], boost::posixtime::seconds(5));
t.async_wait(timeout_handler);
for ( int i = 0; i < 2; ++i)
    boost::thread( boost::bind(run_service, i));
void run_service(int idx)
{
    service[idx].run();
}
  • 第一种情况是非常基础的应用程序。

    • 因为是串行的方式,所以当几个处理程序需要被同时调用时,通常会遇到瓶颈。
    • 如果一个处理程序需要花费很长的时间来执行,所有随后的处理程序都不得不等待。
  • 第二种情况是比较适用的应用程序。

    • 这是非常强壮的————如果几个处理程序被同时调用了(这是有可能的),它们会在各自的线程里面被调用。
    • 唯一的瓶颈就是所有的处理线程都很忙的同时又有新的处理程序被调用。
    • 然而,这是有快速的解决方式的,增加处理线程的数目即可。
  • 第三种情况是最复杂和最难理解的。

    • 只有在第二种情况不能满足需求时才使用它。
    • 这种情况一般就是当有成千上万实时(socket)连接时。
    • 可以认为每一个处理线程(运行io_service::run()的线程)有它自己的select/epoll循环;它等待任意一个socket连接,然后等待一个读写操作,当它发现这种操作时,就执行。
    • 唯一需要注意的就是当监控的socket数目以指数级的方式增长时(超过1000个的socket)。此时,有多个select/epoll循环会增加应用的响应时间。

如果你觉得你的应用程序可能需要转换到第三种模式,请确保监听操作的这段代码(调用io_service::run()的代码)和应用程序其他部分是隔离的,这样你就可以很轻松地对其进行更改。

注意,不能拥有多个io_service实例却只有一个线程。下面的代码片段没有任何意义:

for ( int i = 0; i < 2; ++i)
    service[i].run();

因为service[1].run()需要service[0].run()先结束。因此,所有由service[1]处理的异步操作都需要等待,这显然不是一个好主意。

最后,需要一直记住的是如果没有其他需要监控的操作,.run()就会结束,就像下面的代码片段:

io_service service; 
tcp::socket sock(service); 
sock.async_connect( ep, connect_handler); 
service.run();

在上面的例子中,只要sock建立了一个连接,connect_handler就会被调用,然后接着service.run()就会完成执行。

要想让service不退出,只需要一直给它任务。更简单的方式是使用模拟任务:

typedef boost::shared_ptr work_ptr;
work_ptr dummy_work(new io_service::work(service));

上面的代码可以保证service.run()一直运行直到你调用useservice.stop()或者 dummy_work.reset(0);// 销毁 dummy_work.

总结

  • 总之,Boost.Asio让网络编程变得异常简单。

  • Boost.Asio支持同步和异步编程。他们有很大不同;你需要在项目早期就选择其中的一种来实现,因为它们之间的转换是非常复杂而且易错的。

  • 如果你选择同步,你可以选择异常处理或者错误码,从异常处理转到错误码;只需要在call函数中增加一个参数即可(错误码)。

  • Boost.Asio不仅仅可以用来做网络编程。它还有其他更多的特性,这让它显得更有价值,比如信号量,计时器等等。

参考资料

Boost.Asio入门

Logo

更多推荐