プログラムを書こう!

実務や自作アプリ開発で習得した役に立つソフトウェア技術情報を発信するブログ

C++/CLIでTCPサーバ処理を行う(その2)

この記事は2020年09月18日に投稿しました。

f:id:paveway:20190914064630j:plain

目次

  1. はじめに
  2. C++/CLIでTCPサーバ処理を行う(その2)
  3. おわりに

1. はじめに

こんにちは、iOSのエディタアプリPWEditorの開発者の二俣です。
今回は業務で使用しているC++/CLIでTCPサーバ処理を行う方法(その2)についてです。

目次へ

2. C++/CLIでTCPサーバ処理を行う(その2)

C++/CLIでTCPサーバ処理を行うには、以下のような実装になります。
今回は再接続に対応しました。

実装例

SocketServer.h
#pragma once

namespace Sample
{
    using namespace System;
    using namespace System::Collections::Generic;
    using namespace System::IO;
    using namespace System::Net;
    using namespace System::Net::Sockets;
    using namespace System::Threading;

    ref class Status
    {
    public:
        literal String^ WaitConnect = "WaitConnect";
        literal String^ WaitRecvData = "WaitRecvData";
    };

    public ref class SocketServerController
    {
    private:
        int portNo;
        Thread^ communicateThread;
        TcpListener^ tcpListener;
        TcpClient^ tcpClient;
        NetworkStream^ stream;

        delegate void StatusEventHandlerDelegate();
        Dictionary<String^, StatusEventHandlerDelegate^>^ statusEventHandlerTable;
        String^ status;
        bool isTerminate;

    public:
        SocketServerController(int portNo);

        void Start();
        void Close();

    private:
        void CommunicateThread();
        
        void WaitConnectStatusEventHandler();
        void WaitRecvDataStatusEventHandler();

        String^ CharArrayToString(array<unsigned char>^ src);
    };
}
SocketServerController.cpp
#include "SocketServerController.h"
#include <stdlib.h>
#include <string.h>

namespace Sample
{
    using namespace System::Diagnostics;
    using namespace System::Threading::Tasks;

    /**
    * @brief コンストラクタ
    *
    * @param [in] portNo ポート番号
    */
    SocketServerController::SocketServerController(int portNo)
    {
        this->portNo = portNo;

        communicateThread = nullptr;
        tcpListener = nullptr;
        tcpClient = nullptr;
        stream = nullptr;

        // ステータスイベントテーブルを生成し、イベントハンドラを設定します。
        statusEventHandlerTable = gcnew Dictionary<String^, StatusEventHandlerDelegate^>();
        statusEventHandlerTable->Add(Status::WaitConnect, gcnew StatusEventHandlerDelegate(this, &SocketServerController::WaitConnectStatusEventHandler));
        statusEventHandlerTable->Add(Status::WaitRecvData, gcnew StatusEventHandlerDelegate(this, &SocketServerController::WaitRecvDataStatusEventHandler));
        status = Status::WaitConnect;
    }

    /**
    * @brief 処理を開始します。
    */
    void SocketServerController::Start()
    {
        // 通信スレッドを生成し、開始します。
        ThreadStart^ start = gcnew ThreadStart(this, &SocketServerController::CommunicateThread);
        communicateThread = gcnew Thread(start);
        communicateThread->Start();
    }

    /**
    * @brief 終了します。
    */
    void SocketServerController::Close()
    {
        // 停止フラグを設定します。
        isTerminate = true;

        // Tcpクライアントが有効な場合
        if (tcpClient != nullptr)
        {
            // Tcpクライアントをシャットダウンします。
            tcpClient->Client->Shutdown(SocketShutdown::Both);
        }

        // ネットワークストリームが有効な場合
        if (stream != nullptr)
        {
            // ネットワークストリームをクローズします。
            stream->Close();
            stream = nullptr;
        }

        // Tcpクライアントが有効な場合
        if (tcpClient != nullptr)
        {
            // Tcpクライアントをクローズします。
            tcpClient->Close();
            tcpClient = nullptr;
        }

        // TCPリスナーが有効な場合
        if (tcpListener != nullptr)
        {
            // TCPリスナーを停止します。
            tcpListener->Stop();
            tcpListener = nullptr;
        }

        // 通信スレッドの終了を待ちます。
        if (communicateThread != nullptr)
        {
            communicateThread->Join(5000);
        }
    }

    /**
    * @brrief 通信スレッド
    */
    void SocketServerController::CommunicateThread()
    {
        do
        {
            // テーブルに登録されたステータスの場合
            if (statusEventHandlerTable->ContainsKey(status))
            {
                // ステータスイベント処理を行います。
                StatusEventHandlerDelegate^ statusEventHandler = statusEventHandlerTable[status];
                statusEventHandler();
            }
        } while (!isTerminate);

        communicateThread = nullptr;
    }

    /**
    * @brief 接続待ちステータスイベントハンドラ
    */
    void SocketServerController::WaitConnectStatusEventHandler()
    {
        Console::WriteLine("接続待ち");
        try
        {
            // TCPリスナーを生成し、開始します。
            if (tcpListener == nullptr)
            {
                tcpListener = gcnew TcpListener(IPAddress::Any, portNo);
                tcpListener->Start();
            }

            Task<TcpClient^>^ task = tcpListener->AcceptTcpClientAsync();
            do
            {
                // クライアントからの接続待ち。
                bool result = task->Wait(500);
                // タイムアウトした場合
                if (!result)
                {
                    // 接続待ちに戻ります。
                    continue;
                }

                Console::WriteLine("接続");

                // TCPクライアントを取得します。
                tcpClient = task->Result;

                // ネットワークストリームを取得します。
                stream = tcpClient->GetStream();

                // 受信タイムアウトを設定します。
                stream->ReadTimeout = 3000;

                // ステータスをデータ受信待ちに設定します。
                status = Status::WaitRecvData;
                return;
            } while (!isTerminate);
        }

        // 例外の場合
        // タイムアウト例外以外はエラー終了します。
        catch (Exception^ e)
        {
            // 原因となる例外を取得します。
            Exception^ innerException = e->InnerException;
            // 原因となる例外が取得できない場合
            if (innerException == nullptr)
            {
                Console::WriteLine("例外A(接続待ち):" + e->Message);
            }

            try
            {
                // エラー要因がタイムアウト以外の場合
                SocketException^ se = safe_cast<SocketException^>(innerException);
                if (se->ErrorCode != (int)SocketError::TimedOut)
                {
                    Console::WriteLine("例外B(接続待ち):" + se->Message);
                }
                else
                {
                    return;
                }
            }

            // SocketExceptionにキャストできない場合
            catch (Exception^ e)
            {
                Console::WriteLine("例外C(接続待ち):" + e->Message);
            }
        }

        // 停止フラグを設定します。
        isTerminate = true;
    }

    /**
    * @brief データ受信待ちステータスイベントハンドラ
    */
    void SocketServerController::WaitRecvDataStatusEventHandler()
    {
        Console::WriteLine("データ受信待ち");

        // 受信データ用バッファ
        array<unsigned char>^ readBuffer = gcnew array<unsigned char>(1024);
        do
        {
            try
            {
                // 受信データ用バッファをクリアします。
                Array::Clear(readBuffer, 0, readBuffer->Length);
                // 1回で受信できない場合の受信データのオフセット位置
                int offset = 0;
                // 受信したデータサイズ
                int size = 1024;
                do
                {
                    // データ受信待ち。
                    // 受信タイムアウトで設定時間経過するとタイムアウト例外が発生します。
                    int readBytes = stream->Read(readBuffer, offset, size);

                    // 切断された場合
                    if (readBytes == 0)
                    {
                        Console::WriteLine("切断");

                        // TCPクライアントが有効な場合
                        if (tcpClient != nullptr)
                        {
                            // TCPクライアントをシャットダウンします。
                            tcpClient->Client->Shutdown(SocketShutdown::Both);
                        }

                        // ネットワークストリームが有効な場合
                        if (stream != nullptr)
                        {
                            // ネットワークストリームをクローズします。
                            stream->Close();
                            stream = nullptr;
                        }

                        // TCPクライアントが有効な場合
                        if (tcpClient != nullptr)
                        {
                            // TCPクライアントをクローズします。
                            tcpClient->Close();
                            tcpClient = nullptr;
                        }

                        // 接続待ちステータスを設定します。
                        status = Status::WaitConnect;
                        return;
                    }

                    Console::WriteLine("データ受信");
                    offset += readBytes;
                    size -= offset;
                } while (stream->DataAvailable);

                // 受信したデータを文字列に変換します。
                String^ recvData = CharArrayToString(readBuffer);
                Console::WriteLine("データ受信完了:" + recvData);
            }

            // 例外の場合
            // タイムアウト例外以外はエラー終了します。
            catch (Exception^ e)
            {
                // 原因となる例外を取得します。
                Exception^ innerException = e->InnerException;
                // 原因となる例外が取得できない場合
                if (innerException == nullptr)
                {
                    Console::WriteLine("例外A(データ受信待ち):" + e->Message);
                    break;
                }

                try
                {
                    // エラー要因がタイムアウト以外の場合
                    SocketException^ se = safe_cast<SocketException^>(innerException);
                    if (se->ErrorCode != (int)SocketError::TimedOut)
                    {
                        Console::WriteLine("例外B(データ受信待ち):" + se->Message);
                        break;
                    }
                }

                // SocketExceptionにキャストできない場合
                catch (Exception^ e)
                {
                    Console::WriteLine("例外C(データ受信待ち):" + e->Message);
                    break;
                }
            }
        } while (!isTerminate);

        // 停止フラグを設定します。
        isTerminate = true;
    }

    /**
    * @brief 受信データを文字列に変換します。
    *
    * @param [in] src 受信データ
    * @return 文字列に変換した受信データ
    */
    String^ SocketServerController::CharArrayToString(array<unsigned char>^ src)
    {
        char* pCharArray = (char*)malloc(src->Length);
        memset(pCharArray, '\0', src->Length);
        for (int i = 0; i < src->Length; ++i)
        {
            pCharArray[i] = src[i];
        }
        String^ result = gcnew String(pCharArray);
        free(pCharArray);
        return result;
    }
}

目次へ

3. おわりに

以前

www.paveway.info

を紹介しましたが、今回は再接続に対応してみました。

紹介している一部の記事のコードはGitlabで公開しています。
興味のある方は覗いてみてください。

目次へ


私が勤務しているニューラルでは、主に組み込み系ソフトの開発を行っております。
弊社製品のハイブリッドOS [Bi-OS][Bi-OS]は高い技術力を評価されており、特に制御系や通信系を得意としています。
私自身はiOSモバイルアプリウィンドウズアプリを得意としております。
ソフトウェア開発に関して相談などございましたら、お気軽にご連絡ください。

また一緒に働きたい技術者の方も随時募集中です。
興味がありましたらご連絡ください。

EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688

目次へ