はじめに
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で接続してみましょう
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_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;
}
###メインループ
boost::asio::io_service io_service;
server s(io_service, std::atoi(argv[1]));
io_service.run();
boostのお約束部分
io_serviceを作成し runで、queueにあるio命令の実行を行う
ループ処理をします
###Accept部
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部
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で、最終引数にラムダで完了関数オブジェクトを設定してます
(今回な何もしない)
その前に
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を使わない場合の動作
- async_acceptを実行し、acceptハンドラを登録
- acceptがシグナルになったのでラムダ関数が実行され 自動領域にsessionオブジェクトを作成しsession::startが呼ばれる
- session::do_writeが呼ばれ async_writeが実行され、writeハンドラを登録する
- session::do_writeを抜ける、session::startを抜ける
- acceptハンドラ を抜けると 自動領域のsessionクラスが消滅
- writeがシグナルになった時 sessionクラスが消滅している!!
write時に sessionクラスが消滅している可能性があるわけですね
ところが shared_ptrを使うと
- の部分が make_sharedで shared_ptr<session>が作成され(参照カウンタ 1)
- のwriteハンドラにキャプチャさせ shared_ptr<session>の参照カウンタincrement(参照カウンタ 2)
- でacceptハンドラを抜けた際に shared_ptr<session>の参照カウンタdecrement(参照カウンタ 1)
- writeシグナル時に 参照カウンタが残ってるのでsessionクラスを使う事ができる!
- writeハンドラ終了時にキャプチャ解除され 参照カウンタdecrement(参照カウンタ 0) session消滅
と、正常な動作になりました!!
結論
asioで 非同期処理をする場合はオブジェクトの生成、解放タイミングに気をつける
shared_ptrを使うと比較的楽になる
メモ
asio::const_buffer については後日調べる必要がありそうだ。