C#で「TCPクライアント」を実装する
C#でTCPクライアントをササッと作る必要があったので、その時の個人用メモ。
ちなみに、C#のTCPサーバーの実装はこちら。
あとは、TCPクライアントとして、ネットにあるサンプルの「一回送受信して終わり」ではなく、常時稼働でずっと使えそうなソースサンプルを意識して書いてみた。けどまぁ時間があればもう少し手直しはします。
バグってたらコメントお願いします。
改訂履歴
(2022/10/03 修正)tcpの受信ループにて、1回目に受けきれなかった場合の2回目以降の受信も受信バッファの先頭にデータを格納していた問題を修正しました。ご指摘ありがとうございます。
(2021/03/27 修正)2点修正しました。ご指摘ありがとうございます。
- Read()関数について、TCP通信ではサーバーから全データ一括で受信できると限らず、分割される場合もあるため、読み残しが無いか確認する様に修正
- Read()関数について、例外を使用者側に再スローする様に修正
開発環境
- VisualStudio2017
- .Net4.7(Windows7と10が対応しているので)
基本方針
- TCPクライアントをクラス化して、1サーバーごとに1インスタンスで管理できるようにする。
キーワード
- TcpClient
- NetworkStream
- Read
- IOException
【C#】ソースコード
TCPクライアント(TCPClient.cs)
TCPクライアントクラス。このクラスをインスタンス化して、アプリで制御する。
ソース全文をまるごと載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
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 { // データ受信開始 while (m_tcp_stream.DataAvailable) { receive_size += m_tcp_stream.Read(data, receiveSize, data.Length); } // 受信成功 if (receive_size > 0) { Console.WriteLine($"{System.DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss]")}【TCPClient】Receive() : [{MakeTeleLog(data, receive_size)}]"); } } catch (System.IO.IOException ex) { // タイムアウト 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_tcp_stream?.Close(); m_client?.Close(); throw ex; } return receive_size; } /// <summary> /// 通信電文ログ取得 /// </summary> /// <param name="data">通信電文</param> /// <param name="size">通信電文サイズ</param> /// <returns>通信電文ログ</returns> public 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クライアントクラスを使用したサンプルソースを載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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) { try { int receive_size = client.Receive(receive_buff); if (receive_size > 0) { // 受信データの解析処理 // ParseReceiveData(receive_buff); // 受信処理を終了 receive_req = false; // 受信成功の度に5byte送る sample_data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; send_data.Add(sample_data); } } catch (Exception ex) { // 例外の内容に応じた処理を行う } } } // とりあえず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と通信をおこなうテスターを作ってみましたので、参考にどうぞ。
まとめ
そんなに難しくないね。
間違いなどあれば、受け付けます。
ディスカッション
コメント一覧
ありがとうございます
利用させてもらいます
ずいぶん助かります
一点気になりました
受信タイムアウトの発生時の例外時
strem、clientを
他の例外と同様、閉じたほうが良いようです
(どこかで読んだのですが出典が見つからないのです。すいません)
コメントありがとうございます。
内容ですが、ちょっと微妙かと思います。対向が電源断等で正しく切断できなかった場合等を懸念されているのですかね。受信タイムアウト時に切断するつくりにするかどうかは、本ソースの使用者と使用目的にゆだねたいと思います。
どこかで余裕あればもう少しちゃんとした回答を記載します。
受信時にRead一発で終わっているが、TCP通信はサーバーからデータが100byte送信された時、分割して送信される事がある。1回目10byte:2回目90byte等。
読み残しがないかを確認する必要がある。
また、受信例外発生時に例外情報を再スローしていないため、使用者側からは読み取り0byteということしかわからない。
基本的に例外は使用者側で対処するべきでは?
名無しさん
ご指摘ありがとうございます。どちらも言われる通りですね。修正しました。
参考にさせていただいております。
質問ですが、受信処理で while ループでネットワークストリームからデータを受信しているところは、一回目に読み込んだ data はループの二回目以降に読み込んだ値で上書きされてしまわないのでしょうか?
コメントありがとうございます。
ご指摘通り、上書きしてしまいますね。。。改訂履歴に記載し、修正しました。
ありがとうございました。