てぃってぃの楽しい副業日記!

ワイと嫁(てぃってぃ)夫婦の、副業、育児、プログラミング、筋トレ、ゲームを綴ったブログです☆

C#で「TCPクライアント」を実装する

C#でTCPクライアントをササッと作る必要があったので、その時の個人用メモ。
ちなみに、C#のTCPサーバーの実装はこちら。

www.kt2525family.com
C#のTCPクライアントは、サーバーに同じくCに比べるとわかりにくいが、サーバー程ではない。基本的にはTCPClientとNetWorkStreamさえ使えれば、なんでも組める。

あとは、TCPクライアントとして、ネットにあるサンプルの「一回送受信して終わり」ではなく、常時稼働でずっと使えそうなソースサンプルを意識して書いてみた。けどまぁ時間があればもう少し手直しはします。
バグってたらコメントお願いします。

開発環境

  • VisualStudio2017
  • .Net4.7(Windows7と10が対応しているので)

基本方針

  • TCPクライアントをクラス化して、1サーバーごとに1インスタンスで管理できるようにする。

キーワード

  • TcpClient
  • NetworkStream
  • Read
  • IOException

【C#】ソースコード

TCPクライアント(TCPClient.cs)

TCPクライアントクラス。このクラスをインスタンス化して、アプリで制御する。
ソース全文をまるごと載せておきます。

using System;
using System.Net.Sockets;

namespace TCPClient
{
    class TCPClient
    {
        /* 接続先サーバー情報 */
        private string m_serverAddress;
        private int m_serverPort;
        private int m_readTimeout;
        private int m_writeTimeout;

        /* 状態フラグ */
        public bool m_connected;

        /* TCP通信ハンドル */
        private TcpClient m_client;
        private NetworkStream m_tcpStream;

        /// <summary>
        /// サーバー情報初期化
        /// </summary>
        /// <param name="serverAddress">サーバーアドレス</param>
        /// <param name="serverPort">サーバーポート番号</param>
        /// <param name="readTimeout">受信タイムアウト[ms]</param>
        /// <param name="writeTimeout">送信タイムアウト[ms]</param>
        /// <returns></returns>
        public bool SetConnectInfo(string serverAddress, int serverPort, int readTimeout = 1000, int writeTimeout = 1000)
        {
            // サーバー情報を更新
            m_serverAddress = serverAddress;
            m_serverPort    = serverPort;
            m_readTimeout   = readTimeout;
            m_writeTimeout  = writeTimeout;

            // 「切断」状態に初期化
            m_connected = false;

            // エラー処理を追加してfalseを返すのがベスト
            return true;
        }

        /// <summary>
        /// サーバー接続
        /// </summary>
        /// <returns></returns>
        public bool Connect()
        {
            bool result = false;

            try
            {
                // サーバーと接続
                // 接続完了するまでブロッキングする
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Connect() : [{m_serverAddress}:{m_serverPort}] に接続します ...");
                m_client = new System.Net.Sockets.TcpClient(m_serverAddress, m_serverPort);
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Connect() : 接続しました");

                // 接続完了
                result = true;

                // 「接続」状態に更新
                m_connected = true;

                // ネットワークストリームを取得
                m_tcpStream = m_client.GetStream();

                // 送受信タイムアウト時間を設定
                m_tcpStream.ReadTimeout  = m_readTimeout;
                m_tcpStream.WriteTimeout = m_writeTimeout;
            }
            catch (Exception ex)
            {
                // 接続失敗
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Connect() : ERROR !!! {ex.Message}");
            }

            return result;
        }

        /// <summary>
        /// 切断処理
        /// </summary>
        public void Disconnect()
        {
            m_tcpStream?.Close();
            m_client?.Close();
            m_connected = false;
        }

        /// <summary>
        /// 通信電文送信
        /// </summary>
        /// <param name="data"></param>
        public void Send(byte[] data)
        {
            try
            {
                // データ送信開始
                m_tcpStream.Write(data, 0, data.Length);

                // 送信成功
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Send() : [{MakeTeleLog(data, data.Length)}]");
            }
            catch (Exception ex)
            {
                // 送信失敗
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Send() : ERROR !!! {ex.Message}");

                // 「切断」状態に更新
                m_connected = false;

                // クライアント初期化
                m_tcpStream?.Close();
                m_client?.Close();
            }

        }

        /// <summary>
        /// 通信電文受信
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public int Receive(byte[] data)
        {
            int receiveSize = 0;

            try
            {
                // データ受信開始
                receiveSize = m_tcpStream.Read(data, 0, data.Length);

                // 受信成功
                if (receiveSize > 0)
                {
                    Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Receive() : [{MakeTeleLog(data, receiveSize)}]");
                }
                else
                {
                    // 受信サイズ = 0 はサーバーから切断時
                    throw new Exception("サーバーから切断されました");
                }
            }
            catch (System.IO.IOException)
            {
                // タイムアウト
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Receive() : 受信タイムアウト");
            }
            catch(Exception ex)
            {
                Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Receive() : ERROR !!! {ex.Message}");

                // 「切断」状態に更新
                m_connected = false;

                // クライアント初期化
                m_tcpStream?.Close();
                m_client?.Close();
            }

            return receiveSize;
        }

        /// <summary>
        /// 通信電文ログ取得
        /// </summary>
        /// <param name="data">通信電文</param>
        /// <param name="size">通信電文サイズ</param>
        /// <returns>通信電文ログ</returns>
        private string MakeTeleLog(byte[] data, int size)
        {
            string result = "";

            for (int i = 0; i < size; i++)
            {
                result += String.Format("{0,2:X2}", data[i]);
            }

            return result;
        }
    }
}


namespaceとかは適当に変えて下さい。

エラー処理も結構適当です。使用される側でもう少し補強しておいてください。(接続先情報の範囲チェックとか)

ポイントは、接続状態「m_connected」をpublicにして、メインアプリ側から参照できるようにする。 メイン側は、未接続(false)であればConnect()を呼び、接続(true)であれば、必要に応じて送受信(Send() / Receive())を呼ぶようにする。


メイン側のサンプルソース

上記TCPクライアントクラスを使用したサンプルソースを載せておきます。

using System;
using System.Collections.Generic;

namespace TCPClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // 接続テスト
            TCPClient client = new TCPClient();
            client.SetConnectInfo("127.0.0.1", 1234);

            // 送信データリスト
            // 例として5byteのデータを設定
            List<byte[]> sendData = new List<byte[]>();
            byte[] sampleData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
            sendData.Add(sample_data);

            // データ送信ループ
            bool receiveReq = false;
            byte[] receiveBuff = new byte[1024];
            while (true)
            {
                // 切断している場合は接続
                if (!client.m_connected)
                {
                    client.Connect();
                }

                // 接続している場合
                if (client.m_connected)
                {
                    // 送信データがある場合、データ送信する
                    // <例> 「0102030405」
                    if (sendData.Count > 0)
                    {
                        client.Send(send_data[0]);
                        sendData.RemoveAt(0);

                        // 受信処理に進む
                        receiveReq = true;
                    }

                    // 受信を期待する場合、データ受信する
                    if (receiveReq)
                    {
                        int receiveSize = client.Receive(receiveBuff);
                        if (receiveSize > 0)
                        {
                            // 受信データの解析処理
                            // ParseReceiveData(receiveBuff);

                            // 受信処理を終了
                            receiveReq = false;

                            // 受信成功の度に5byte送る
                            sampleData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
                            sendData.Add(sampleData);
                        }
                    }
                }

                // とりあえず5秒sleep
                System.Threading.Thread.Sleep(5000);
            }
        }
    }
}


サンプルアプリとしては、以下の動きにしています。
・接続できていなければ接続
・接続できていれば5秒周期で5byteのデータ送信、完了すれば受信に進む
スレッドに組み込むなりなんなり、使えるかと思います。

実行例

実行したらこんな感じにログが出ます。

[2020/03/11 17:07:12]【TCPClient】Connect() : ERROR !!! 対象のコンピューターにて拒否されたため、接続できませんでした。 127.0.0.1:1234

[2020/03/11 17:07:17]【TCPClient】Connect() : [127.0.0.1:1234] に接続します .

[2020/03/11 17:07:17]【TCPClient】Connect() : 接続しました

[2020/03/11 17:07:17]【TCPClient】Send() : [0102030405]

[2020/03/11 17:07:18]【TCPClient】Receive() : 受信タイムアウト

[2020/03/11 17:07:25]【TCPClient】Receive() : 受信タイムアウト

[2020/03/11 17:07:30]【TCPClient】Receive() : ERROR !!! サーバーから切断されました

[2020/03/11 17:07:35]【TCPClient】Connect() : [127.0.0.1:1234] に接続します

[2020/03/11 17:07:36]【TCPClient】Connect() : ERROR !!! 対象のコンピューターによって拒否されたため、接続できませんでした。 127.0.0.1:1234

[2020/03/11 17:07:41]【TCPClient】Connect() : [127.0.0.1:1234] に接続します .

[2020/03/11 17:07:41]【TCPClient】Connect() : 接続しました

[2020/03/11 17:07:42]【TCPClient】Receive() : 受信タイムアウト

[2020/03/11 17:07:47]【TCPClient】Receive() : [000C00000000030111170746]

[2020/03/11 17:07:52]【TCPClient】Send() : [0102030405]

[2020/03/11 17:07:53]【TCPClient】Receive() : 受信タイムアウト

[2020/03/11 17:07:59]【TCPClient】Receive() : 受信タイムアウト

[2020/03/11 17:08:04]【TCPClient】Receive() : [000C00000000030111170802]

[2020/03/11 17:08:09]【TCPClient】Send() : [0102030405]

[2020/03/11 17:08:09]【TCPClient】Receive() : [000C00000000030111170807]

[2020/03/11 17:08:14]【TCPClient】Send() : [0102030405]

[2020/03/11 17:08:14]【TCPClient】Receive() : [000C00000000030111170812]

応用例

本ソースを用いて、三菱PLCと通信をおこなうテスターを作ってみましたので、参考にどうぞ。

www.kt2525family.com

まとめ

そんなに難しくないね。

間違いなどあれば、受け付けます。