最終更新日時:
が更新

履歴 編集

ネットワーク - TCP

インデックス

接続

同期バージョン

同期バージョンの接続には、boost::asio::ip::tcp::socketクラスのconnect()メンバ関数を使用する。

接続先の情報はtcp::endpointに、IPアドレス文字列と、ポート番号の2つを指定する。

connect()の第2引数としてerror_codeを渡した場合には、接続失敗時にエラー情報がerror_code変数に格納される。

error_codeを渡さなかった場合には、接続失敗時にboost::system::system_errorが例外として投げられる。

#include <iostream>
#include <boost/asio.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

int main()
{
    asio::io_service io_service;
    tcp::socket socket(io_service);

    boost::system::error_code error;
    socket.connect(tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), 31400), error);

    if (error) {
        std::cout << "connect failed : " << error.message() << std::endl;
    }
    else {
        std::cout << "connected" << std::endl;
    }
}

非同期バージョン

非同期バージョンの接続には、boost::asio::ip::tcp::socketクラスのasync_connect()メンバ関数を使用する。

第1引数として、接続先情報のIPアドレス文字列と、ポート番号を指定する。

第2引数として、接続成功もしくは接続失敗時に呼ばれる関数を指定する。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class Client {
    asio::io_service& io_service_;
    tcp::socket socket_;

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service)
    {}

    void connect()
    {
        socket_.async_connect(
                tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), 31400),
                boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

    void on_connect(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "connect failed : " << error.message() << std::endl;
        }
        else {
            std::cout << "connected" << std::endl;
        }
    }
};

int main()
{
    asio::io_service io_service;
    Client client(io_service);

    client.connect();

    io_service.run();
}

接続待機

接続待機には、boost::asio::ip::tcp::acceptorクラスを使用する。

acceptorクラスのコンストラクタには、IPのバージョン(tcp::v4() or tcp::v6())とポート番号を設定する。

同期バージョン

同期バージョンの接続待機には、acceptorクラスのaccept()メンバ関数を使用する。

引数として、バインディングするsocketクラスオブジェクトへの参照を渡す。

#include <iostream>
#include <boost/asio.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

int main()
{
    asio::io_service io_service;

    tcp::acceptor acc(io_service, tcp::endpoint(tcp::v4(), 31400));
    tcp::socket socket(io_service);

    boost::system::error_code error;
    acc.accept(socket, error);

    if (error) {
        std::cout << "accept failed: " << error.message() << std::endl;
    }
    else {
        std::cout << "accept correct!" << std::endl;
    }
}

非同期バージョン

非同期バージョンの接続待機には、acceptorクラスのasync_accept()メンバ関数を使用する。

第1引数としてバインディングするsocketオブジェクトへの参照をとり、第2引数として接続成功もしくは接続失敗時に呼ばれる関数を指定する。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class Server {
    asio::io_service& io_service_;
    tcp::acceptor acceptor_;
    tcp::socket socket_;

public:
    Server(asio::io_service& io_service)
        : io_service_(io_service),
          acceptor_(io_service, tcp::endpoint(tcp::v4(), 31400)),
          socket_(io_service) {}

    void start_accept()
    {
        acceptor_.async_accept(
            socket_,
            boost::bind(&Server::on_accept, this, asio::placeholders::error));
    }

private:
    void on_accept(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "accept failed: " << error.message() << std::endl;
        }
        else {
            std::cout << "accept correct!" << std::endl;
        }
    }
};

int main()
{
    asio::io_service io_service;
    Server server(io_service);

    server.start_accept();

    io_service.run();
}

メッセージ送信

ここでは、TCPソケットでのメッセージ送信方法を解説する。

同期バージョン

同期バージョンのメッセージ送信には、boost::asio::write()フリー関数を使用する。

この関数には、多様なバージョンが提供されているが、ここでは基本的なものを紹介する。

  • 第1引数 : socketオブジェクトへの参照
  • 第2引数 : 送信バッファ
  • 第3引数 : 送信結果を格納するエラー値への参照(省略可)

第3引数を省略し、エラーが発生した場合はboost::system::system_error例外が投げられる。

#include <iostream>
#include <boost/asio.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

int main()
{
    asio::io_service io_service;
    tcp::socket socket(io_service);

    // 接続
    socket.connect(tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), 31400));

    // メッセージ送信
    const std::string msg = "ping";
    boost::system::error_code error;
    asio::write(socket, asio::buffer(msg), error);

    if (error) {
        std::cout << "send failed: " << error.message() << std::endl;
    }
    else {
        std::cout << "send correct!" << std::endl;
    }
}

非同期バージョン

非同期バージョンのメッセージ送信には、boost::asio::async_write()フリー関数を使用する。

この関数もまた、いくつかのバージョンが提供されているが、ここでは基本的なものを紹介する。

  • 第1引数 : socketオブジェクトへの参照
  • 第2引数 : 送信バッファ
  • 第3引数 : 送信成功もしくは失敗時に呼ばれる関数

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class Client {
    asio::io_service& io_service_;
    tcp::socket socket_;
    std::string send_data_; // 送信データ

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service)
    {}

    void start()
    {
        connect();
    }

private:
    // 接続
    void connect()
    {
        socket_.async_connect(
                tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), 31400),
                boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

    // 接続完了
    void on_connect(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "connect failed : " << error.message() << std::endl;
            return;
        }

        send();
    }

    // メッセージ送信
    void send()
    {
        send_data_ = "ping";
        asio::async_write(
                socket_,
                asio::buffer(send_data_),
                boost::bind(&Client::on_send, this,
                            asio::placeholders::error,
                            asio::placeholders::bytes_transferred));
    }

    // 送信完了
    // error : エラー情報
    // bytes_transferred : 送信したバイト数
    void on_send(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error) {
            std::cout << "send failed: " << error.message() << std::endl;
        }
        else {
            std::cout << "send correct!" << std::endl;
        }
    }
};

int main()
{
    asio::io_service io_service;
    Client client(io_service);

    client.start();

    io_service.run();
}

メッセージ受信

ここでは、TCPソケットでのメッセージ受信の方法を解説する。

同期バージョン

同期バージョンのメッセージ受信には、以下のいずれかの関数を使用する。

ここでは、boost::asio::read()フリー関数を使用して解説する。

  • 第1引数 : sockeオブジェクトへの参照
  • 第2引数 : 受信バッファへの参照
  • 第3引数 : どれくらい受信するか。transfer_all()はバッファがいっぱいになるまで読む。transfer_at_least(size_t minimum)は最低でもNバイト読む。transfer_exactly(size_t size)は指定したサイズ読む。
  • 第4引数 : 受信結果を格納するエラー値への参照(省略可)

第4引数を省略し、エラーが発生した場合はboost::system::system_error例外が投げられる。

#include <iostream>
#include <boost/asio.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

int main()
{
    asio::io_service io_service;

    tcp::acceptor acc(io_service, tcp::endpoint(tcp::v4(), 31400));
    tcp::socket socket(io_service);

    // 接続待機
    acc.accept(socket);

    // メッセージ受信
    asio::streambuf receive_buffer;
    boost::system::error_code error;
    asio::read(socket, receive_buffer, asio::transfer_all(), error);

    if (error && error != asio::error::eof) {
        std::cout << "receive failed: " << error.message() << std::endl;
    }
    else {
        const char* data = asio::buffer_cast<const char*>(receive_buffer.data());
        std::cout << data << std::endl;
    }
}

非同期バージョン

非同期バージョンのメッセージ受信には、以下のいずれかの関数を使用する。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class Server {
    asio::io_service& io_service_;
    tcp::acceptor acceptor_;
    tcp::socket socket_;
    asio::streambuf receive_buff_;

public:
    Server(asio::io_service& io_service)
        : io_service_(io_service),
          acceptor_(io_service, tcp::endpoint(tcp::v4(), 31400)),
          socket_(io_service) {}

    void start()
    {
        start_accept();
    }

private:
    // 接続待機
    void start_accept()
    {
        acceptor_.async_accept(
            socket_,
            boost::bind(&Server::on_accept, this, asio::placeholders::error));
    }

    // 接続待機完了
    void on_accept(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "accept failed: " << error.message() << std::endl;
            return;
        }

        start_receive();
    }

    // メッセージ受信
    void start_receive()
    {
        boost::asio::async_read(
            socket_,
            receive_buff_,
            asio::transfer_all(),
            boost::bind(&Server::on_receive, this,
                        asio::placeholders::error, asio::placeholders::bytes_transferred));
    }

    // 受信完了
    // error : エラー情報
    // bytes_transferred : 受信したバイト数
    void on_receive(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error && error != boost::asio::error::eof) {
            std::cout << "receive failed: " << error.message() << std::endl;
        }
        else {
            const char* data = asio::buffer_cast<const char*>(receive_buff_.data());
            std::cout << data << std::endl;

            receive_buff_.consume(receive_buff_.size());
        }
    }
};

int main()
{
    asio::io_service io_service;
    Server server(io_service);

    server.start();

    io_service.run();
}

名前解決して接続

名前解決には、boost::asio::ip::tcp::resolverクラスとboost::asio::ip::tcp::resolver::queryクラスを組み合わせて使用する。

queryクラスのコンストラクタには、以下を指定する:

  • 第1引数 : ホスト名
  • 第2引数 : サービス名

同期バージョン

ホスト名等が設定されたqueryオブジェクトをresolverクラスのresolve()メンバ関数に渡し、その文字列を接続関数に渡すことで、同期バージョンでの名前解決しての接続ができる。

この関数の最後の引数としてboost::system::error_codeオブジェクトへの参照を渡した場合には、名前解決失敗時にエラー情報が格納される。error_codeを渡さなかった場合には、名前解決失敗時にboost::system::system_errorが例外として投げられる。

また、この関数は戻り値として、boost::asio::ip::tcp::resolver::iteratorオブジェクトを返す。このイテレータは、デフォルト構築されたイテレータを終端としてイテレートできる。このイテレータは間接参照によってendpointオブジェクトが取得できる。

#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;

class Client {
    asio::io_service& io_service_;
    tcp::socket socket_;

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service)
    {
    }

    void connect()
    {
        tcp::resolver resolver(io_service_);
        tcp::resolver::query query("google.com", "http");

        // 同期で名前解決
        // 非同期で接続
        asio::async_connect(
            socket,
            resolver_.resolve(query),
            boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

private:
    void on_connect(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "connect error : " << error.message() << std::endl;
        }
        else {
            std::cout << "connect!" << std::endl;
        }
    }
};

int main()
{
    asio::io_service io_service;

    Client client(io_service);
    client.connect();

    io_service.run();
}

非同期バージョン

非同期バージョンの名前解決には、boost::asio::ip::tcp::resolverクラスのasync_resolve()メンバ関数を使用する。

  • 第1引数 : queryオブジェクト
  • 第2引数 : 名前解決の成功もしくは失敗時に呼ばれる関数。iteratorプレースホルダを束縛することにより、完了時に呼ばれる関数に、endpointのイテレータが渡される。

#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;

class Client {
    asio::io_service& io_service_;
    tcp::socket socket_;
    tcp::resolver resolver_;

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service),
          resolver_(io_service)
    {
    }

    void connect()
    {
        tcp::resolver::query query("google.com", "http");

        // 非同期で名前解決
        resolver_.async_resolve(
            query,
            boost::bind(&Client::on_resolve, this,
                        asio::placeholders::error,
                        asio::placeholders::iterator));
    }

private:
    void on_resolve(const boost::system::error_code& error,
                    tcp::resolver::iterator endpoint_iterator)
    {
        if (error) {
            std::cout << "resolve failed: " << error.message() << std::endl;
            return;
        }

        // 非同期で接続
        asio::async_connect(
            socket_,
            endpoint_iterator,
            boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

    void on_connect(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "connect error : " << error.message() << std::endl;
        }
        else {
            std::cout << "connect!" << std::endl;
        }
    }
};

int main()
{
    asio::io_service io_service;

    Client client(io_service);
    client.connect();

    io_service.run();
}

タイムアウトを設定する

通信処理のタイムアウトには、ソケットに対してタイムアウトを指定するのではなく、タイマークラスの非同期イベントと組み合わせて行う。

同期通信でタイムアウトを指定する方法はないため、ここでは非同期バージョンのみ示す。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/bind.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class Client {
    asio::io_service& io_service_;
    tcp::socket socket_;
    asio::streambuf receive_buff_;

    asio::steady_timer timer_; // タイムアウト用のタイマー
    bool is_canceled_;

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service),
          timer_(io_service),
          is_canceled_(false)
    {}

    void start()
    {
        connect();
    }

private:
    // 接続
    void connect()
    {
        socket_.async_connect(
                tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), 31400),
                boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

    // 接続完了
    void on_connect(const boost::system::error_code& error)
    {
        if (error) {
            std::cout << "connect failed : " << error.message() << std::endl;
            return;
        }

        start_receive();
    }

    // メッセージ送信
    void start_receive()
    {
        boost::asio::async_read(
            socket_,
            receive_buff_,
            asio::transfer_all(),
            boost::bind(&Client::on_receive, this,
                        asio::placeholders::error, asio::placeholders::bytes_transferred));

        // 5秒でタイムアウト
        timer_.expires_from_now(boost::chrono::seconds(5));
        timer_.async_wait(boost::bind(&Client::on_timer, this, _1));
    }

    // 受信完了
    // error : エラー情報
    // bytes_transferred : 送信したバイト数
    void on_receive(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error == asio::error::operation_aborted) {
            std::cout << "タイムアウト" << std::endl;
        }
        else {
            // タイムアウトになる前に処理が正常終了したのでタイマーを切る
            // タイマーのハンドラにエラーが渡される
            timer_.cancel();
            is_canceled_ = true;

            if (error) {
                std::cout << "その他のエラー : " << error.message() << std::endl;
            }
            else {
                std::cout << "受信成功" << std::endl;
            }
        }
    }

    // タイマーのイベント受信
    void on_timer(const boost::system::error_code& error) {
        if (!error && !is_canceled_) {
            socket_.cancel(); // 通信処理をキャンセルする。受信ハンドラがエラーになる
        }
    }
};

int main()
{
    asio::io_service io_service;
    Client client(io_service);

    client.start();

    io_service.run();
}

タイムアウトにはいくつかのポイントがある。

1. タイマークラスの選択

タイマークラスには以下の選択肢がある:

タイマークラス 説明
boost::asio::deadline_timer Boost.DateTimeライブラリのposix_timeで時間指定を行う古いタイマー
boost::asio::high_resolution_timer 高分解能タイマー
boost::asio::steady_timer 時間が逆行しないことを保証するタイマー
boost::asio::system_timer time_tと互換性のあるタイマー

用途に応じて使い分ける必要があるが、基本的にはsteady_timerを推奨する。これは、タイマー処理中にOSの時間設定が変更されても時間が逆行しないタイマーであるため、外部要因によるバグを防ぐことができる。

2. タイムアウトの時間設定

タイムアウトの時間指定は、ここでは以下のように行なっている:

// 5秒でタイムアウト
timer_.expires_from_now(boost::chrono::seconds(5));
timer_.async_wait(boost::bind(&Client::on_timer, this, _1));

各タイマークラスのexpires_from_now()メンバ関数は、現在日時からの相対時間でタイムアウトを指定する関数である。特定の日時にタイムアウトを設定したい場合は、expires_at()メンバ関数を使用する。

3. タイムアウト方法

ここまではタイムアウトではなく、単にタイマーの使い方を見てきた。 実際のタイムアウトは以下のようにして行う:

  1. タイマーハンドラで通信処理をキャンセル or 失敗させる。

通信処理が正常終了するより前にタイマーハンドラが呼ばれたら、socketクラスのcancel()メンバ関数やclose()メンバ関数を使用して通信処理を異常終了させる。

// タイマーのイベント受信
void on_timer(const boost::system::error_code& error) {
    if (!error && !is_canceled_) {
        socket_.cancel(); // 通信処理をキャンセルする。受信ハンドラがエラーになる
    }
}

注意すべきポイントは、これらの異常終了させるための関数を呼び出しても、通信処理のイベントハンドラが呼び出されるということである。

  1. 通信処理のイベントハンドラでタイムアウトによる中断をハンドリングする

タイムアウトによって通信処理が異常終了した場合、通信処理のイベントハンドラにはboost::asio::error::operation_abortedというエラーが渡される。ハンドラは、タイムアウトによって失敗したのかどうかを正しくハンドリングする必要がある。

void on_receive(const boost::system::error_code& error, size_t bytes_transferred)
{
    if (error == asio::error::operation_aborted) {
        std::cout << "タイムアウト" << std::endl;
    }

    ...
}

  1. 通信処理がタイマーよりも早く正常終了したらタイマーをキャンセルする

通信処理がタイムアウトを待つことなく正常終了した場合は、タイマーを止める必要がある。これをしないと以降の通信処理が意図せずタイムアウトになってしまうだろう。

if (error == asio::error::operation_aborted) {
    std::cout << "タイムアウト" << std::endl;
}
else {
    // タイムアウトになる前に処理が正常終了したのでタイマーを切る
    // タイマーのハンドラにエラーが渡される
    timer_.cancel();
    is_canceled_ = true;
}

タイマークラスのcalcel()メンバ関数を呼ぶと、socketの場合と逆に、タイマーのハンドラにboost::asio::error::operation_abortedエラーが渡されることになる。

ただし、cancel()メンバ関数を呼ぶ直前ですでにタイムアウトになっている場合、boost::asio::error::operation_abortedエラーがハンドラに渡されない可能性がある。 この場合に備えてフラグ変数等でタイマーを止めたことを知らせる必要がある。

documented boost version is 1.51.0