C#で実用的なソケット通信クラスを作った(TCP編)

  • by

作成したクラスの概要

今回作成したのはSystem.Net.Sockets.Socketクラスを使用したTcpClientクラスとTcpServerクラスです。今回はTCP通信を行うアプリケーションにそのまま組み込めるクラスを作成しました。

TcpServerの処理フロー

サーバーは受付ポートを指定してインスタンスを生成します。メインスレッドで行うのはインスタンス生成とサーバータスクの生成で、リクエスト受付はサブタスクにて行います。クライアントからリクエストを受信するとReceiveイベントが発生し、処理によって生成された応答をクライアント側に返します。Receiveイベントはこのクラスを使用する別クラス側で定義することで汎用的に使用できるクラスとなっています。クライアントへの応答後、再度、受信待ち状態に戻ります。

TcpClientの処理フロー

クライアントは1回のリクエスト送信、応答受信を確認すると終了します。リクエスト送信はサブスレッドで行い、サーバー側からの応答を確認するとReceiveイベントが発生します。Receiveイベントは別クラス側で定義することで汎用的に使用できるクラスとなっており、UI操作等に使用します。

TcpServerクラスのコード

実際のサーバー側コードはこちらです。

/// <summary>
/// TCP Serverクラス
/// </summary>
class TcpServer
{
    private Socket socket;
    private IPEndPoint localEP;
    public byte[] receiveData;
    public byte[] sendData;
    public bool IsLitenOn;
    private Task comTask;

    //デリゲートの宣言
    //ReceiveEventArgs型のオブジェクトを返すようにする
    public delegate string ReceiveEventHandler(object sender, ReceiveEventArgs e);

    //イベントデリゲートの宣言
    public event ReceiveEventHandler Receive;

    /// <summary>
    /// コンストラクター
    /// 受信ポートを引数にしてインスタンスを作成
    /// </summary>
    /// <param name="port"></param>
    public TcpServer(int port)
    {
        localEP = new IPEndPoint(IPAddress.Any, port);
        IsLitenOn = true;

    }

    /// <summary>
    /// GenSocket
    /// サーバー用ソケットを作成する
    /// </summary>
    /// <returns></returns>
    public int GenSocket()
    {
        int code = 0;
        byte[] buf = new byte[1024];
        int record = 0;
        socket = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        socket.Bind(localEP);
        socket.Listen(10);

        

        while (IsLitenOn)
        {
            Socket handler = socket.Accept();
            record = handler.Receive(buf);
            receiveData = new byte[record];
            Array.Copy(buf,receiveData, record);
            
            //Responseデータ作成
            ReceiveEventArgs e = new ReceiveEventArgs();
            e.Message = Encoding.UTF8.GetString(receiveData);
            string str = Receive(this, e);

            //Response送信
            handler.Send(Encoding.UTF8.GetBytes(str));

            // ソケットの終了
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
        }
        

        return code;
    }

    /// <summary>
    /// ComTaskGen
    /// 非同期タスクで"GenSocket()"メソッドを開始する
    /// </summary>
    public void ComTaskGen()
    {
        Func<int> func = GenSocket;
        comTask = new Task<int>(func);
        comTask.Start();
    }

    /// <summary>
    /// RequestDataGen
    /// 動作確認用テストレスポンス作成メソッド
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    public byte[] RequestDataGen(string str)
    {
        byte[] buf = new byte[1024];
        sendData = receiveData;
        return receiveData;
    }

    /// <summary>
    /// ReadReceiveData
    /// 直近の受信データを文字列形式で返す
    /// receiveDataは削除する
    /// </summary>
    /// <returns></returns>
    public string ReadReceiveData()
    {
        string str = Encoding.UTF8.GetString(receiveData);
        receiveData = null;
        return str;
    }

    /// <summary>
    /// ReadSendData
    /// 直近の送信データを文字列形式で返す
    /// sendDataは削除する
    /// </summary>
    /// <returns></returns>
    public string ReadSendData()
    {
        string str = Encoding.UTF8.GetString(receiveData);
        sendData = null;
        return str;
    }

    /// <summary>
    /// ReceiveEventArgs : EventArgs
    /// Receiveイベントで渡すデータ
    /// </summary>
    public class ReceiveEventArgs : EventArgs
    {
        public string Message;
        public byte[] data;
    }
}

 

TcpClientクラスのコード

クライアント側のコードはこちらです。

/// <summary>
/// TCP Clientクラス
/// </summary>
class TcpClient
{

    public Socket socket;
    public IPAddress ipAddress;
    private IPEndPoint remoteEP;
    public byte[] receiveData;
    public byte[] sendData;
    public Task comTask;

    //デリゲートの宣言
    //ReceiveEventArgs型のオブジェクトを返すようにする
    public delegate string ReceiveEventHandler(object sender, ReceiveEventArgs e);

    //イベントデリゲートの宣言
    public event ReceiveEventHandler Receive;

    /// <summary>
    /// コンストラクター
    /// サーバホスト名、送信先ポートを引数にしてインスタンスを作成
    /// </summary>
    /// <param name="hostName"></param>
    /// <param name="port"></param>
    public TcpClient(string hostName,int port)
    {
        IPHostEntry ipHostList = Dns.GetHostEntry(hostName);
        for(int i = 0; i < ipHostList.AddressList.Length; i++)
        {
            if(ipHostList.AddressList[i].AddressFamily == AddressFamily.InterNetwork)
            {
                ipAddress = ipHostList.AddressList[i];
                remoteEP = new IPEndPoint(ipAddress, port);
                break;
            }
        }
    }

    /// <summary>
    /// SetSendData
    /// 文字列データを送信用プロパティに格納する
    /// </summary>
    /// <param name="msg"></param>
    public void SetSendData(string msg)
    {
        sendData = Encoding.UTF8.GetBytes(msg);
    }

    /// <summary>
    /// GenSocket
    /// 送信用ソケットを作成しサーバにリクエストを送る
    /// </summary>
    /// <returns></returns>
    private int GenSocket()
    {
        int code = 0;

        socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        socket.Connect(remoteEP);
        
        socket.Send(sendData);

        byte[] buf = new byte[1024];
        int record = socket.Receive(buf);
        receiveData = new byte[record];
        Array.Copy(buf, receiveData, record);

        //Receiveイベントで渡すデータ作成
        ReceiveEventArgs e = new ReceiveEventArgs();
        e.Message = Encoding.UTF8.GetString(receiveData);
        Receive(this, e);

        socket.Shutdown(SocketShutdown.Both);
        socket.Close();

        return code;
    }

    /// <summary>
    /// ComTaskGen
    /// 非同期タスクで送信用ソケット作成、実行する
    /// </summary>
    public void ComTaskGen()
    {
        Func<int> func = GenSocket;
        comTask = new Task<int>(func);
        comTask.Start();
    }

    /// <summary>
    /// ReadReceiveData
    /// 受信データを文字列形式で返す
    /// </summary>
    /// <returns></returns>
    public string ReadReceiveData()
    {
        string str = Encoding.UTF8.GetString(receiveData);
        receiveData = null;
        return str;
    }

    /// <summary>
    /// ReceiveEventArgs : EventArgs
    /// Receiveイベントで渡すデータ
    /// </summary>
    public class ReceiveEventArgs : EventArgs
    {
        public string Message;
        public byte[] data;
    }
}

 

Receiveイベントの実装方法

TcpServerおよびTcpClientで実装されるReceiveイベントの使用方法です。

UI操作用delegateの定義(必要な場合のみ)

ReceiveイベントでUI処理を行う場合は後程使用するUI処理用delegateを定義しておきます。処理フローで説明した通り、通信はサブタスクで行うため、サブスレッドからUIの直接編集ができないことに注意してください。

private delegate void UiControlData(object obj);

 

TcpServer_Receiveイベントの定義と追加

サーバークラスを利用する場合、Receiveイベントの処理を定義する必要があります。イベントを定義するには、以下のように処理内容を定義します。このコード例ではリクエストを受信すると"World"の文字列を応答として返します。

/// <summary>
/// TcpServer_Receive
/// サーバーのリクエスト受信イベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns></returns>
private string TcpServer_Receive(object sender, TcpServer.ReceiveEventArgs e)
{
    //イベントが発生したとき
    //クライアントへのレスポンス
    string res = "World";

    //UI表示用メッセージを作成
    string receiveBuf = "Receive:" + e.Message;
    string sendBuf = "Send:" + res;

    //別タスク実行なので下記コードだとエラーが出る
    //TbxServerReceiveString.AppendText(receiveBuf);
    //TbxServerReceiveString.AppendText(sendBuf);

    //UI変更はInvokeで実行する
    this.Invoke(new UiControlData(ServerReceiveUiAdd), (object)receiveBuf);
    this.Invoke(new UiControlData(ServerSendUiAdd), (object)sendBuf);
    return res;
}

上記コード例のようにリクエストと応答をUIに表示する場合、Invokeによる処理が必要になります。

次にインスタンス生成後にイベントを追加するコード例です。

tcpServer = new TcpServer(port);
tcpServer.Receive += new TcpServer.ReceiveEventHandler(this.TcpServer_Receive);
tcpServer.ComTaskGen();
TimerServerReceive.Enabled = true;

TcpServerを利用するには上記2つのコードを追加します。

TcpClient_Receiveイベントの定義と追加

クライアントクラスを利用する場合、Receiveイベントの処理を定義する必要があります。イベントを定義するには、以下のように処理内容を定義します。

/// <summary>
/// TcpClient_Receive
/// クライアントの受信イベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns></returns>
private string TcpClient_Receive(object sender, TcpClient.ReceiveEventArgs e)
{
    //イベントが発生したとき
    string res = e.Message;

    //別タスク実行なので下記コードだとエラーが出る
    //TbxClientReceiveString.AppendText(res);
    this.Invoke(new UiControlData(ClientReceiveUiAdd),(object)e.Message);
    return res;
}

次にインスタンス生成後にイベントを追加するコード例です。

tcpClient = new TcpClient(hostName, port);
tcpClient.Receive += new TcpClient.ReceiveEventHandler(this.TcpClient_Receive);
tcpClient.SetSendData(TbxClientSendString.Text);
tcpClient.ComTaskGen();

TcpClientを利用するには上記2つのコードを追加します。

仕様の留意点

最後にシステムに組み込む上で、いくつか留意点があるので説明します。

TcpServer受信の停止について

現在のコードはTcpServerの受信停止ができません。停止したい場合はIsLitenOnプロパティをfalseにした後、一度リクエストを受信する必要があります。受信機能を備えたい場合はTimerで一定時間ごとにIsListenOnを監視し、falseの場合はTcpServerインスタンス内で生成したTcpClientからリクエストを送信する方法があります。

TcpClientの受信データ確認方法

TcpClientクラスでは応答データの読み込み方法を2種類用意しています。1つは例で説明したイベントハンドラを利用する方法で、もう一つはTimerでReadReceiveData()メソッドを利用する方法です。このメソッドを下記コード例のように、receiveDataプロパティがnullの場合のみ実行することでメインスレッドから応答データを取得できます。

/// <summary>
/// TimerClientReceive_Tick
/// 
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TimerClientReceive_Tick(object sender, EventArgs e)
{
    //if(tcpClient.comTask.IsCompleted)
    if(tcpClient.receiveData != null)
    {

        TbxClientReceiveString2.AppendText(tcpClient.ReadReceiveData());
    }
}

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

eleven − nine =