質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.07%

BoostライブラリのAsioで非同期通信を構築し,連続でデータの受け取りを行いたい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 4,117

Unity-chan

score 18

前提・実現したいこと

非同期で送信されるデータを,連続で受け取るプログラムの構築を目指しています.
具体的には「スレッドでデータ待機する→受け取る→表示する→データ待機する・・・」という処理を実現したいと考えています.
またMain関数からはAsioのライブラリが見えないように構築したいと考えています.

現状

BoostライブラリのAsioを用いて構築したコードを確認するために,送信したデータを表示して終了するプログラムを構築しました.(参考:https://boostjp.github.io/tips/network/tcp.html)

発生している問題・エラーメッセージ

実行すると以下のエラーメッセージが表示されます.(IPReceiveData.cppのOnReceive関数)

receive failed: 供給されたファイルハンドルが無効です.

受信ソース

IPReceiveData.h

#ifndef __IPConnect_h__
#define __IPConnect_h__


enum TransportProtocol {
    TP_TCP = 1,
    TP_UDP = 0,
};

class IPReceive {
public:
    // TCPかUDPか選択(最初に選択することで,TCPUDP区別なく初期化・通信の関数を使える)
    IPReceive(TransportProtocol isTcp);
    ~IPReceive();

    void iprInitialize();

    void iprReceiveMSG();

    char* GetLastData();

private:
    class IPReceive_impl* ipr_impl;
};

#endif

IPReceiveData.cpp

#define BOOST_COROUTINES_NO_DEPRECATION_WARNING    
#define PIMP(x) ipr_impl->x

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/asio/spawn.hpp>
#include "IPReceiveData.h"

namespace asio = boost::asio;
using asio::ip::tcp;
using asio::ip::udp;
using std::cout;
using std::endl;
using std::string;


// Pimplクラス
class IPReceive_impl {
private:
    tcp::acceptor    acceptor;
    tcp::socket      socketTCP;
    //udp::socket      socketUDP;
    //asio::strand     strand;
    asio::streambuf  receive_buff_;



    // 接続が完了したか(async_accept用で、引数は固定)
    void OnAccept(const boost::system::error_code& error)
    {
        if (error) 
            cout <<  "connect failed : " + error.message() << endl;
        else
            cout << "Success connection." << endl;
    }


    // 受信が成功したか(async_read用で,引数は固定)
    void OnReceive(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error && error != asio::error::eof) 
            cout << "receive failed: " << error.message() << endl;
        else {
            // 受信したデータをchar*に変換
            const char* data = asio::buffer_cast<const char*>(receive_buff_.data());
            lastData = const_cast<char*>(data);

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


public:
    asio::io_service&    ioService;
    const bool           isTCP;
    string               receiveData;
    char*                lastData;


    // TCPとUDPを指定して初期化するコンストラクタ
    IPReceive_impl(asio::io_service& ioService, TransportProtocol isTcp)
        :    ioService    (ioService),
            acceptor      (ioService, tcp::endpoint(tcp::v4(), 31400)),
            socketTCP     (ioService),
            strand        (ioService),
            socketUDP     (ioService),
            isTCP         (isTcp) {}


    // デストラクタ
    ~IPReceive_impl() {}


    // 指定IP,ポートでTCP接続
    void ConnectTCP(char *ipAddress, char *portNum)
    {
        // 接続後に呼ばれるコールバック関数を指定
        auto cbOnAccept(boost::bind(&IPReceive_impl::OnAccept, this, asio::placeholders::error));    

        // TCPソケットと上記で宣言したとコールバック関数を登録する
        acceptor.async_accept(socketTCP, cbOnAccept);
    }
    // 指定IP,ポートでUDP接続
    void ConnectUDP(char *ipAddress, char *portNum) {
    }



    // メッセージ送信
    void ReceiveTCP()
    {
        // 受信後に呼ばれるコールバック関数を指定
        auto cbOnReceive(
            boost::bind(&IPReceive_impl::OnReceive, this,
                asio::placeholders::error,
                asio::placeholders::bytes_transferred));    

        // データを受信する
        asio::async_read(
            socketTCP,
            receive_buff_,
            asio::transfer_all(),
            cbOnReceive);
    }
};



// IP通信の初期化を行う
IPReceive::IPReceive(TransportProtocol isTcp) {
    auto ioService = new asio::io_service;
    ipr_impl = new IPReceive_impl(*ioService, isTcp);
}

IPReceive::~IPReceive() {
    cout << "Finish" << endl;
    delete ipr_impl;
}


// TCP通信の初期化
void IPReceive::iprInitialize() {
    if (ipr_impl->isTCP)
        PIMP(ConnectTCP(ipAddress, portNum));
    else
        PIMP(ConnectUDP(ipAddress, portNum));
}

void IPReceive::iprReceiveMSG() {
    if (PIMP(isTCP)) {
        PIMP(ReceiveTCP());
        PIMP(ioService.run());
    }
}

char* IPReceive::GetLastData(){
    return PIMP(lastData);
}

Main.cpp

#include "IPReceiveData.h"
#define __test__
#include <string>
#include <iostream>
int main() {
    // TCPで初期化
    IPReceive ipSending(TP_TCP);


    ipSending.iprInitialize();

    ipSending.iprReceiveMSG();


    std::cout << ipSending.GetLastData() << std::endl;
    return 0;
}

試したこと

参考URLのようにReceiveTCP()をOnConnect()内で呼び出すと問題なく動作するのですが,今後連続でデータ待機を行う場合に毎回Connectを行う必要が出てくるので可能であればOnConnectからReceiveTCPを呼び出す方法は避けたいと考えています.

補足情報(言語/FW/ツール等のバージョンなど)

Visual Studio 2017
Boost 1.65.1

追記

受信ソース(修正箇所)

    // 接続が完了したか(async_accept用で、引数は固定)
    void OnAccept(const boost::system::error_code& error)
    {
        if (error)
            cout << "connect failed : " + error.message() << endl;
        else
            cout << "Success connection." << endl;

//-----編集箇所--------------------------
        ReceiveTCP();
//-----編集箇所--------------------------
    }


    // 受信が成功したか(async_read用で,引数は固定)
    void OnReceive(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error && error != asio::error::eof)
            cout << "receive failed: " << error.message() << endl;
        else {
            // 受信したデータをchar*に変換
            const char* data = asio::buffer_cast<const char*>(receive_buff_.data());
            lastData = const_cast<char*>(data);
            cout << lastData << endl;

            receive_buff_.consume(receive_buff_.size());

//-----編集箇所--------------------------
            if (lastData[0] != 'e') {
                cout << "endしないよ:" << lastData << endl;
                ReceiveTCP();
            }
            else            
                cout << "endするよ" << endl;
//-----編集箇所--------------------------
        }
    }

送信ソース

参考URLのソースから,他ソースコードから送信したい文字列を指定できるように変更した.

IPSendData.h

#ifndef __IPConnect_h__
#define __IPConnect_h__


enum TransportProtocol {
    TP_TCP = 1,
    TP_UDP = 0,
};

class IPSending {
public:
    // TCPかUDPか選択(最初に選択すれば、初期化やメッセージ送信ではTCPUDP区別なく関数を使える)
    IPSending(TransportProtocol isTcp);
    ~IPSending();

    void ipsInitialize(char* ipAddress, char *portNum);

    void ipsSendMSG(char* msg);

private:
    class IPSend_impl* ips_impl;
};

#endif

IPSendData.cpp

#define BOOST_COROUTINES_NO_DEPRECATION_WARNING    // asioはcoroutine2をサポートしていないので警告を消す
#define PIMP(x) ips_impl->x

#include <iostream>
#include <memory>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/asio/spawn.hpp>
#include "IPSendData.h"

namespace asio = boost::asio;
using asio::ip::tcp;
using asio::ip::udp;
using std::cout;
using std::endl;
using std::string;
using std::shared_ptr;



class IPSend_impl {
private:
    tcp::socket    socketTCP;    // TCP
    udp::socket    socketUDP;    // UDP
    udp::resolver  resolver;     // UDP
    asio::strand   strand;       // spawn
    udp::endpoint  receiver_endpoint;
    string         sendData;     // SendTCPでしか使わないが、使用する前にスコープ外へ出て消えてしまうので外


    // 接続が完了したか(async_connect用で、引数は固定)
    void OnConnect(const boost::system::error_code& error)
    {
        if (error)
            throw "connect failed : " + error.message();
        else
            cout << "connect correct!" << endl;
    }

    void OnSend(const boost::system::error_code& error, size_t bytes_transferred)
    {
        if (error) 
            throw "send failed: " + error.message();
        else 
            cout << "send correct!" << endl;    
    }


public:
    asio::io_service&    ioService;
    const bool           isTCP;


    // TCPとUDPを指定して初期化するコンストラクタ
    IPSend_impl(asio::io_service& ioService, TransportProtocol isTcp)
        :    ioService (ioService),
             resolver  (ioService),
             strand    (ioService),
             socketTCP (ioService),
             socketUDP (ioService),
             isTCP     (isTcp){}

    // デストラクタ
    ~IPSend_impl() {
    }


    // TCPの接続要求を行う
    void ConnectTCP(char *ipAddress, char *portNum) {
        // 接続するIPとポート番号を設定する
        auto address(    
            tcp::endpoint(
                asio::ip::address::from_string(ipAddress), atoi(portNum)));    

        // 接続時に実行するコールバック関数を設定する
        auto cbOnConnect(
            boost::bind(&IPSend_impl::OnConnect, this,
                asio::placeholders::error));

        // 上記で宣言したアドレスとコールバック関数を登録する
        socketTCP.async_connect(address, cbOnConnect);
    }

    // UDPの接続要求を行う
    void ConnectUDP(char *ipAddress, char *portNum) {
        auto query(udp::resolver::query(udp::v4(), ipAddress, portNum));
        receiver_endpoint = *resolver.resolve(query);

        socketUDP.open(udp::v4());
    }


    // TCPでメッセージを送信
    void SendTCP(char *msg)
    {
        sendData = msg;

        // 接続後に呼ばれる関数(今回は成否出力)
        auto cbOnSend(    
            boost::bind(&IPSend_impl::OnSend, this,    
                asio::placeholders::error,
                asio::placeholders::bytes_transferred));


        // ソケット、上記で宣言した送信データとコールバック関数を登録する
        asio::async_write(
            socketTCP,
            asio::buffer(sendData),
            cbOnSend);
    }


    // メッセージ送信
    void SendUDP(char *msg){
    }
};



// IP通信の初期化を行う
IPSending::IPSending(TransportProtocol isTcp) {
    auto ioService = new asio::io_service;
    ips_impl = new IPSend_impl(*ioService, isTcp);
}

IPSending::~IPSending() {
    delete ips_impl;
    cout << "Finalize IPSending." << endl;
}


// TCP通信の初期化
void IPSending::ipsInitialize(char *ipAddress, char *portNum) {
    if (ips_impl->isTCP)
        PIMP(ConnectTCP(ipAddress, portNum));
    else
        PIMP(ConnectUDP(ipAddress, portNum));
}

void IPSending::ipsSendMSG(char *msg) {
    if (PIMP(isTCP)) {
        PIMP(SendTCP(msg));
        PIMP(ioService.run());
    }
    else
        PIMP(SendUDP(msg));
}

Main.cpp

#include <iostream>
#include "IPSendData.h"
#define __test__

int main() {
    char test[30];

    IPSending ipSending(TP_TCP);

    ipSending.ipsInitialize("127.0.0.1", "31400");

    while (true) {
        std::cout << "文字を入力してください:";
        scanf_s(aaa, 10);

        ipSending.ipsSendMSG(aaa);

        if (aaa[0] == 'e')
            break;
    }

    return 0;
}


実行結果
それぞれ1文字ずつ[a, b, c, d, e]と送信した場合
イメージ説明

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • PineMatsu

    2017/11/06 17:33

    送信部分が見当たらないのですが、ソースは提示できますか?

    キャンセル

  • Unity-chan

    2017/11/06 18:08

    質問をご覧いただきありがとうございます.先ほど送信ソースを追加したので,確認よろしくお願いします(受信ソースの下).

    キャンセル

  • PineMatsu

    2017/11/07 16:43

    え?受信ソースの下には何もソースがありませんが。

    キャンセル

  • Unity-chan

    2017/11/08 01:42

    送信ソースを追記の受信ソース(修正箇所)の項目の下へ移動しました。

    キャンセル

回答 1

checkベストアンサー

0

非同期版を使用するのであれば、コールバック関数の中で次の非同期処理を実行する必要があります。
提示されたコードでは非同期処理を連続で実行しているだけなので、socketが接続される前にreadが行われてエラーとなります。

処理の流れとしては
async_acceptに接続時のコールバック(1)を登録する。
(1)のコールバック内でasync_readにデータ受信時のコールバック(2)を登録する
(2)のコールバック内でデータを処理し、
まだデータが必要であれば
async_readにデータ受信時のコールバック(2)を登録する(受信されたら再度(2)のコールバックが実行される)
必要でなければ
切断等の処理をして終了する。

というような(2)のコールバック関数があたかもループされて実行されるような構造になっていないといけません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/11/06 14:58

    返信が遅くなりもうしわけありません、回答ありがとうございます。
    回答して頂いた内容を元に、追記(質問下部)のようにプログラムを編集しました。しかし、データを受信しても文字列が表示されず、終了時に受信した文字列が全て結合されてしまいます。もしよろしければ再度アドバイスを頂けると嬉しいです。

    キャンセル

  • 2017/11/06 20:25

    async_readでtransfer_allじゃなくてtransfer_at_least(1)とかtransfer_exactly(1)とかを使ってください。

    キャンセル

  • 2017/11/10 14:55

    transfer_at_least(1)にしたところ,無事一回ごとにデータを表示できました!
    transfer_allの場合は,バッファが一杯(65535)になるまでデータをため込むのですね…。今回はアドバイスしていただいてありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.07%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る