Boost.Asio

Most programs interact with the outside world in some way, whether it be via a file, a network, a serial cable, or the console. Sometimes, as is the case with networking, individual I/O operations can take a long time to complete. This poses particular challenges to application development.

Boost.Asio provides the tools to manage these long running operations, without requiring programs to use concurrency models based on threads and explicit locking.

The Boost.Asio library is intended for programmers using C++ for systems programming, where access to operating system functionality such as networking is often required. In particular, Boost.Asio addresses the following goals:
Portability, Scalability, Efficiency, Model concepts from established APIs, such as BSD sockets., Ease of use., Basis for further abstraction.

Although Boost.Asio started life focused primarily on networking, its concepts of asynchronous I/O have been extended to include other operating system resources such as serial ports, file descriptors, and so on.

1、网络IO模型

要想明白Boost::Asio到底有何用途,首先需要对IO模型有一定的了解。在W. Richard Stevens 的 Unix Network Programming 中,谈到了5种IO模型:

  • 阻塞 blocking,当前进程发出IO请求后阻塞,等待IO就绪后进程再继续执行;
  • 非阻塞 non-blocking,不停的调用recv_some 或send_some,每次都能progress一点,最后仍然会在data copy这里阻塞在系统调用上;
  • IO多路复用 IO multiplexing,基本类似于blocking,只不过一个线程可以同时处理多个socket的请求,也就是所谓的线程复用了,在Linux上有select、poll、epoll机制提供IO多路复用;
  • 异步 asynchronouse,线程提交IO请求之后直接返回,系统在执行完IO请求并复制到用户提供的数据区之后再通知完成;
  • 信号驱动 singal-driven,很少使用。

在Linux中,提供的epoll方法可以实现IO多路复用,且拥有不错的效率,想要了解epoll机制可以查看这篇文章

2、ASIO异步模型

boost::asio使用的是proactor模型,从官方文档中,可以找到其模型图如下:

proactor

对上述模型的说明可以查看官方文档

3、io_service

在boost的1.72版本中,似乎选择用io_context类来代替io_service,但是为了向下兼容,最新的boost lib并没有完全废置io_service,但是在官方文档中,都以io_context代替io_service。这里仍以io_service讲解。

io_service是这个库里面最重要的类;它负责和操作系统打交道,等待所有异步操作的结束,然后为每一个异步操作调用其完成处理程序

你有多种不同的方式来使用io_service。在下面的例子中,我们有3个异步操作,2个socket连接操作和一个计时器等待操作:

  • 有一个io_service实例和一个处理线程的单线程例子:
io_service service; // 所有socket操作都由service来处理
ip::tcp::socket sock1(service); // all the socket perations 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();
  • 有一个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();
}
  • 有多个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();
}

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

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

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

下面是你需要从前面的例子中学到的:

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

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

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

3、网络API

这里不再细讲常用API(包括IP地址的获取,endpoint,socket,read,write等等)的用法,重点讲述一些需要着重注意的事情:

  • 套接字实例不能被拷贝,拷贝构造方法和=操作符是不可访问的。因为每一个实例都拥有并管理着一个资源(原生套接字本身)。如果我们允许拷贝构造,结果是我们会有两个实例拥有同样的原生套接字;这样我们就需要去处理所有者的问题。
ip::tcp::socket s1(service), s2(service);
s1 = s2; // 编译时报错
ip::tcp::socket s3(s1); // 编译时
  • 当从一个套接字读写内容时,你需要一个缓冲区,用来保存读取和写入的数据。缓冲区内存的有效时间必须比I/O操作的时间要长;你需要保证它们在I/O操作结束之前不被释放。(这点很重要,尤其实在异步操作的时候)
// 非常差劲的代码 ...
void on_read(const boost::system::error_code & err, std::size_t read_bytes) { ... }

void func() {
    char buff[512];
    sock.async_receive(buffer(buff), on_read);
}

在我们调用async_receive()之后,buff就已经超出有效范围,它的内存当然会被释放。当我们开始从套接字接收一些数据时,我们会把它们拷贝到一片已经不属于我们的内存中;它可能会被释放,或者被其他代码重新开辟来存入其他的数据,结果就是:内存冲突。

当我们需要对一个buffer进行读写操作时,代码会把实际的缓冲区对象封装在一个buffer()方法中,然后再把它传递给方法调用:

char buff[512];
sock.async_receive(buffer(buff), on_read);

基本上我们都会把缓冲区包含在一个类中以便Boost.Asio的方法能遍历这个缓冲区,比方说,使用下面的代码:

sock.async_receive(some_buffer, on_read);

实例some_buffer需要满足一些需求,叫做ConstBufferSequence或
者MutableBufferSequence(你可以在Boost.Asio的文档中查看它们)。创建你自己的类去处理这些需求的细节是非常复杂的,但是Boost.Asio已经提供了一些类用来处理这些需求。所以你不用直接访问这些缓冲区,而可以使用buffer()方法。自信地讲,你可以把下面列出来的类型都包装到一个buffer()方法中:
一个char[] const 数组
一个字节大小的void *指针
一个std::string类型的字符串
一个POD const数组(POD代表纯数据,这意味着构造器和释放器不做任何操
作)
一个pod数据的std::vector
一个包含pod数据的boost::array
一个包含pod数据的std::array

下面的代码都是有效的:

struct pod_sample { int i; long l; char c; };
char b1[512];
void * b2 = new char[512];
std::string b3; b3.resize(128);
pod_sample b4[16];
std::vector<pod_sample> b5; b5.resize(16);
boost::array<pod_sample,16> b6;
std::array<pod_sample,16> b7;
sock.async_send(buffer(b1), on_read);
sock.async_send(buffer(b2,512), on_read);
sock.async_send(buffer(b3), on_read);
sock.async_send(buffer(b4), on_read);
sock.async_send(buffer(b5), on_read);
sock.async_send(buffer(b6), on_read);
sock.async_send(buffer(b7), on_read);
  • 有时候你会想让一些异步处理方法顺序执行。比如,你去一个餐馆(go_to_restaurant),下单(order),然后吃(eat)。你需要先去餐馆,然后下单,最后吃。这样的话,你需要用到io_service::strand,这个方法会让你的异步方法被顺序调用。看下面的例子:
using namespace boost::asio;
io_service service;
void func(int i) {
    std::cout << "func called, i= " << i << "/" << boost::this_t
    hread::get_id() << std::endl;
}

void worker_thread() {
    service.run();
}

int main(int argc, char* argv[]) {
    io_service::strand strand_one(service), strand_two(service);
    for ( int i = 0; i < 5; ++i)
        service.post( strand_one.wrap( boost::bind(func, i)));
    for ( int i = 5; i < 10; ++i)
        service.post( strand_two.wrap( boost::bind(func, i)));
    boost::thread_group threads;
    for ( int i = 0; i < 3; ++i)
        hreads.create_thread(worker_thread);
    // 等待所有线程被创建完
    boost::this_thread::sleep( boost::posix_time::millisec(500));
    threads.join_all();
}

在上述代码中,我们保证前面的5个线程和后面的5个线程是顺序执行的

4、实战例子

下面的例子是我根据官方文档稍作更改,能够做到在服务器中用一个vector容器保存所有客户端的连接socket,同时也能对断开连接做出检查。

#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
#include <vector>

using boost::asio::ip::tcp;
class session;
std::vector<std::shared_ptr<session>> client_info;
class session
        : public std::enable_shared_from_this<session>
{   // 为了保证该类的实例在异步函数执期间一直有效,我们可以传递一个指向自身的share_ptr给异步函数,
    // 这样在异步函数执行期间share_ptr所管理的对象就不会析构,所使用的变量也会一直有效了(保活)
    // 继承于enable_shared_frome_this类则会为派生类提供成员函数: shared_from_this。
    // 调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr<T> 对象,
    // 返回的指针将和自身共享所有权
public:
    session(tcp::socket socket)
        : socket_(std::move(socket))        // move强制转换socket为右值
    {
    }

    void start()
    {
        do_read();
    }

private:
    void do_read()
    {
        auto self = shared_from_this();
        socket_.async_read_some(boost::asio::buffer(data_, max_length),
            [this, self](const boost::system::error_code & ec, std::size_t length) {
                // 捕获`self`使shared_ptr<session>的引用计数增加1,在该例中避免了async_read()退出时其引用计数变为0
            this->read_handler(ec, length);
        });
    }

    void read_handler(const boost::system::error_code & ec, std::size_t length) {
        if(ec) {
            // 没有判断end of file 即断开连接!
            if(ec.value() == boost::asio::error::eof){
                std::cout << "[client exit!]" << std::endl;
                auto it = std::find(client_info.begin(), client_info.end(), shared_from_this());
                client_info.erase(it);
                std::cout << "client num is " << client_info.size() << "!\n";
                return;
            }
            std::cout << "[read error]" << ec.message() << std::endl;
            return;
        }
        data_[length] = '\0';
        std::cout << "received:" << data_ << std::endl;
        do_write(length);
    }

    void do_write(std::size_t length)
    {
        auto self = shared_from_this();
        boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
            [this, self](const boost::system::error_code & ec, std::size_t length) {
            this->write_handler(ec, length);
        });
    }

    void write_handler(const boost::system::error_code & ec, std::size_t length) {
        if(ec) {
            std::cout << "[write error]" << ec.message() << std::endl;
        }
        std::cout << "msg send!\n";
        do_read();
    };

    tcp::socket socket_;
    enum { max_length = 1024 };
    char data_[max_length];
};

class server
{
public:

    server(boost::asio::io_context& io_context, short port)
            : acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
    {
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(
                [this](boost::system::error_code ec, tcp::socket socket)
                {
                    if (!ec)
                    {
                        auto ptr = std::make_shared<session>(std::move(socket));
                        ptr->start(); // 开始接受客户端的消息
                        client_info.push_back(ptr);
                    }

                    std::cout << "client number is " << client_info.size() << "!\n";

                    do_accept();    // 继续接受连接请求
                });
    }

    tcp::acceptor acceptor_;
};

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 2)
        {
            std::cerr << "Usage: async_tcp_echo_server <port>\n";
            return 1;
        }

        boost::asio::io_context io_context;

        server s(io_context, std::atoi(argv[1]));

        io_context.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

5、参考文献

官方文档:https://www.boost.org/doc/libs/1_72_0/doc/html/boost_asio/overview.html

知乎的文章:https://zhuanlan.zhihu.com/p/55503053

第七章是讲ASIO库的:http://zh.highscore.de/cpp/boost/

《Boost.Asio C++ 网络编程》这本书的语法有些老旧,但是讲解很全面很细致,适合第一次学Asio看:https://mmoaay.gitbooks.io/boost-asio-cpp-network-programming-chinese/content/Chapter1.htmlhttps://github.com/mmoaay/boost-asio-cpp-network-programming-in-chinese/blob/master/README.md

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容