LoginSignup
17
15

More than 5 years have passed since last update.

【boost::asio buffers】 boost::asioでセッション管理にはshared_ptrが便利だ

Last updated at Posted at 2015-02-14

はじめに

C++11の素晴らしいサンプルコードを勉強したいので
boostのサンプルコードを参考書にします
他にもすばらしいコードがあれば教えて下さい!

今回のお題

boost1_57_0 の buffersというサンプルです
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/example/cpp11/buffers/

いちおう、上記サンプルを CMake(CLion)プロジェクトにしたものを
下記リポジトリに置いておきます
https://github.com/MurasameOpen/CPPSample/tree/master/boostSample/boost1_57_0/asio/cpp11/buffers

解析

動作

まずこのサンプルは、参照カウンタ(shared_ptr)を使い
安全にメモリ管理をするサンプルです
ポート番号を第一引数に実行し
telnetで接続してみましょう

telnet
miyatake_y$ telnet -4 localhost 8080 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Fri Feb 13 17:40:58 2015
Connection closed by foreign host.

このように、現在時刻を返してコネクションをCloseするだけのものです
ただし この後何度接続してもメモリリークをしない&必要な時に解放されない
その管理に shared_ptr を使ってます

ソース解析

全体

reference_counterd.cpp
//
// reference_counted.cpp
// ~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2014 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://www.boost.org/LICENSE_1_0.txt)
//

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

using boost::asio::ip::tcp;

// A reference-counted non-modifiable buffer class.
class shared_const_buffer
{
public:
    // Construct from a std::string.
    explicit shared_const_buffer(const std::string& data)
            : data_(new std::vector<char>(data.begin(), data.end())),
              buffer_(boost::asio::buffer(*data_))
    {
    }

    // Implement the ConstBufferSequence requirements.
    typedef boost::asio::const_buffer value_type;
    typedef const boost::asio::const_buffer* const_iterator;
    const boost::asio::const_buffer* begin() const { return &buffer_; }
    const boost::asio::const_buffer* end() const { return &buffer_ + 1; }

private:
    std::shared_ptr<std::vector<char> > data_;
    boost::asio::const_buffer buffer_;
};

class session
        : public std::enable_shared_from_this<session>
{
public:
    session(tcp::socket socket)
            : socket_(std::move(socket))
    {
    }

    void start()
    {
        do_write();
    }

private:
    void do_write()
    {
        std::time_t now = std::time(0);
        shared_const_buffer buffer(std::ctime(&now));

        auto self(shared_from_this());
        boost::asio::async_write(socket_, buffer,
                [this, self](boost::system::error_code /*ec*/, std::size_t /*length*/)
                {
                });
    }

    // The socket used to communicate with the client.
    tcp::socket socket_;
};

class server
{
public:
    server(boost::asio::io_service& io_service, short port)
            : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
              socket_(io_service)
    {
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(socket_,
                [this](boost::system::error_code ec)
                {
                    if (!ec)
                    {
                        std::make_shared<session>(std::move(socket_))->start();
                    }

                    do_accept();
                });
    }

    tcp::acceptor acceptor_;
    tcp::socket socket_;
};

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

        boost::asio::io_service io_service;

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

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

    return 0;
}

メインループ

main
        boost::asio::io_service io_service;
        server s(io_service, std::atoi(argv[1]));
        io_service.run();

boostのお約束部分
io_serviceを作成し runで、queueにあるio命令の実行を行う
ループ処理をします

Accept部

server
class server
{
public:
    server(boost::asio::io_service& io_service, short port)
            : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
              socket_(io_service)
    {
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(socket_,
                [this](boost::system::error_code ec)
                {
                    if (!ec)
                    {
                        std::make_shared<session>(std::move(socket_))->start();
                    }

                    do_accept();
                });
    }

    tcp::acceptor acceptor_;
    tcp::socket socket_;
};

do_accept内の async_acceptは、io_serviceに対して
acceptのイベント待機を行い、イベントがシグナルになると最終引数の
関数オブジェクトを実行するよう コールバック登録し終了します
asioにおける async_xxx系の動作は全て同じになってます

そして、acceptがシグナルになった時に第三引数の関数オブジェクト(ラムダ関数)が実行されます。
ラムダ関数の最後に do_acceptを呼び出してますが、これは
acceptを受け、処理が終了した後に次のconnectを待ち受けるという
独特の書き方です
決して 再帰呼び出し(スタックが増える)していません

ラムダの中身で気になるのは2つあります

std::move(socket_)

moveセマンティクスですね。本格的に解析すると右辺値参照とか必要になるので
そういうのは外部に任せて
本の虫
class serverではもう socket_を使わないため、class sessionへ所有権ごと移動します

std::make_shared<session>

sessionを std::shared_ptrで管理します
shared_ptrとは参照カウンタ方式のスマートポインタで、参照個数が記録されていて
参照が無くなると自動的にdeleteされます
今回のサンプルのキモですね!

なぜshared_ptrが必要なのかは session部を見ましょう

session部

session
    void do_write()
    {
        std::time_t now = std::time(0);
        shared_const_buffer buffer(std::ctime(&now));

        auto self(shared_from_this());
        boost::asio::async_write(socket_, buffer,
                [this, self](boost::system::error_code /*ec*/, std::size_t /*length*/)
                {
                });
    }

ここも先ほどと同じように asio::async_writeで、最終引数にラムダで完了関数オブジェクトを設定してます
(今回な何もしない)
その前に

shared_from_this()
        auto self(shared_from_this());
        boost::asio::async_write(socket_, buffer,
                [this, self](boost::system::error_code /*ec*/, std::size_t /*length*/)
                {
                });

この部分の処理
shared_from_this() は std::enable_shared_from_this <T>を継承しているclass かつ、shared_ptrで管理されている場合にのみ有効です
class sessionは enable_shared_from_thisを継承かつ、accept時に make_sharedで管理されてるので
有効です
shared_from_this()は 自分(*this)を管理している shared_ptrを取得します
そして今回は selfという名前で ラムダのキャプチャに入れてます

そこで注目なのは、ラムダのキャプチャにselfを指定しているが実際には使われていません!
何故使わないのにキャプチャするのかというと
参照をラムダ関数オブジェクトに残すためだと思われます

ここ、間違ってたら指摘して欲しいです。最初何をやってるか理解できませんでしたが
shared_ptrを使わず同じ実装をした時の動きを順に

shared_ptrを使わない場合の動作

  1. async_acceptを実行し、acceptハンドラを登録
  2. acceptがシグナルになったのでラムダ関数が実行され 自動領域にsessionオブジェクトを作成しsession::startが呼ばれる
  3. session::do_writeが呼ばれ async_writeが実行され、writeハンドラを登録する
  4. session::do_writeを抜ける、session::startを抜ける
  5. acceptハンドラ を抜けると 自動領域のsessionクラスが消滅
  6. writeがシグナルになった時 sessionクラスが消滅している!!

write時に sessionクラスが消滅している可能性があるわけですね
ところが shared_ptrを使うと

  1. の部分が make_sharedで shared_ptr<session>が作成され(参照カウンタ 1)
  2. のwriteハンドラにキャプチャさせ shared_ptr<session>の参照カウンタincrement(参照カウンタ 2)
  3. でacceptハンドラを抜けた際に shared_ptr<session>の参照カウンタdecrement(参照カウンタ 1)
  4. writeシグナル時に 参照カウンタが残ってるのでsessionクラスを使う事ができる!
  5. writeハンドラ終了時にキャプチャ解除され 参照カウンタdecrement(参照カウンタ 0) session消滅

と、正常な動作になりました!!

結論

asioで 非同期処理をする場合はオブジェクトの生成、解放タイミングに気をつける
shared_ptrを使うと比較的楽になる

メモ

asio::const_buffer については後日調べる必要がありそうだ。

17
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
15