C Sharpとネットワーク - TCP Client

提供:MochiuWiki : SUSE, EC, PCB
2024年9月13日 (金) 19:33時点におけるWiki (トーク | 投稿記録)による版 (→‎受信 (SSL / TLS対応))
ナビゲーションに移動 検索に移動

概要

TCP (Transmission Control Protocol) は、インターネットプロトコルスイートの中核をなす通信プロトコルの1つである。
TCPは信頼性の高い通信を提供する強力なプロトコルであり、C#の豊富なネットワーキングAPIと組み合わせることで、効率的なネットワークアプリケーションの開発が可能になる。

C#においても、TCPを利用したネットワークプログラミングは重要な位置を占めている。

TCPの主な特徴は、信頼性の高い通信を提供することである。
これは、データの送受信を確実に行い、パケットの損失や重複、順序の入れ替わりを防ぐ機能を持っているためである。

また、フロー制御や輻輳制御の仕組みも備えており、ネットワークの状況に応じて通信速度を調整する。

C#でTCP通信を実装する場合は、主にSystem.Net.Sockets名前空間を使用する。
この名前空間には、TcpClientクラスやTcpListenerクラス等が含まれており、これらを使用してクライアントとサーバの通信を簡単に実装できる。

TcpClientクラスを使用すると、サーバへの接続やデータの送信が可能になる。
TcpListenerクラスは、サーバ側でクライアントからの接続を待ち受けるために使用する。

実際の通信では、ストリームを介してデータのやり取りを行う。
NetworkStreamクラスを使用して、接続したソケットからデータを読み書きする。
これにより、テキストやバイナリデータを効率的に送受信することができる。

C#でTCP通信を実装する場合は、非同期プログラミングの手法を活用することが推奨される。
BeginConnectメソッドやBeginAcceptメソッド等を使用するこにより、アプリケーションの応答性を向上させることができる。

セキュリティ面では、SSL / TLS証明書を使用して通信を暗号化することが可能である。
C#では、SslStreamクラスを使用してこれを実現することができる。

また、接続の確立、切断、タイムアウトの処理、エラーハンドリング等を適切に実装することにより、安定したネットワークアプリケーションを開発することができる。



クライアント

送信

以下の例では、TcpClientクラスを使用して、非同期でサーバに接続およびメッセージを送信している。
また、タイムアウトが発生した場合の再試行処理や高度なエラー処理等も追加することを推奨する。

NetworkStreamクラスのReadTimeoutWriteTimeoutプロパティを非同期処理と組み合わせることは非推奨である。
非同期操作でタイムアウトを実装する場合、Task.WhenAnyTask.Delayを組み合わせる方法が効果的である。

  • 互換性の問題
    ReadTimeoutプロパティおよびWriteTimeoutプロパティは、同期メソッド (Read, Write) に対してのみ有効である。
    非同期メソッド (ReadAsync, WriteAsync) には影響しない。

  • 非同期操作との不適合
    非同期操作は本質的にタイムアウトの概念と相容れない。
    非同期メソッドは完了するまで制御を返さないため、タイムアウトを設定しても期待通りに動作しない。


 using System;
 using System.Text;
 using System.Net.Sockets;
 using System.Threading.Tasks;
 
 class AsyncTcpClientSender
 {
    static async Task Main(string[] args)
    {
       string server = "<ホスト名またはIPアドレス>";  // サーバのアドレス
       int port = <ト番号>;                     // ポート番号
       int timeoutMilliseconds = 5000;            // タイムアウト (ミリ秒)
 
       try
       {
          using (TcpClient client = new TcpClient())
          {
             // サーバへ接続
             var connectTask = client.ConnectAsync(server, port);
 
             // サーバへの接続が確立するまで待機
             if (await Task.WhenAny(connectTask, Task.Delay(timeoutMilliseconds)) != connectTask)
             {  // タイムアウトが発生した場合
                throw new TimeoutException("エラー: サーバへの接続がタイムアウト");
             }
 
             using (NetworkStream stream = client.GetStream())
             {  // サーバへメッセージを送信
                string message = "Hello, Server";
                byte[] data = Encoding.UTF8.GetBytes(message);
 
                var sendTask = stream.WriteAsync(data, 0, data.Length);
                if (await Task.WhenAny(sendTask, Task.Delay(timeoutMilliseconds)) != sendTask)
                {
                   throw new TimeoutException("エラー: メッセージの送信がタイムアウト");
                }
 
                Console.WriteLine($"送信メッセージ: {message}");
             }
          }
       }
       catch (SocketException e)
       {  // ネットワーク関連のエラーが発生した場合
          Console.WriteLine($"SocketException: {e.Message}");
       }
       catch (TimeoutException e)
       {  // タイムアウトが発生した場合
          Console.WriteLine($"TimeoutException: {e.Message}");
       }
       catch (Exception e)
       {  // その他の予期せぬエラーが発生した場合
          Console.WriteLine($"予期せぬエラーが発生: {e.Message}");
       }
    }
 }


送信 (SSL / TLS対応)

以下の例では、SSL / TLS証明書を使用して、非同期でサーバに接続およびメッセージを送信している。
SslStreamクラスを使用して、SSL / TLS証明書に対応したセキュアな通信を実現している。

また、タイムアウトが発生した場合の再試行処理や高度なエラー処理等も追加することを推奨する。

※注意
サーバ側でもSSL / TLS証明書の対応が必要となる。
実務では、適切な証明書の管理と定期的な更新が重要である。

 using System;
 using System.Text;
 using System.Threading.Tasks;
 using System.Net.Sockets;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 class SslAsyncTcpClientSender
 {
    static async Task Main(string[] args)
    {
       string server           = "<ホスト名またはIPアドレス>";  // サーバのアドレス
       int port                = <ト番号>;                // ポート番号
       int timeoutMilliseconds = 5000;                      // タイムアウト (ミリ秒)
 
       try
       {
          using (TcpClient client = new TcpClient())
          {
             // サーバへ接続
             var connectTask = client.ConnectAsync(server, port);
             if (await Task.WhenAny(connectTask, Task.Delay(timeoutMilliseconds)) != connectTask)
             {
                throw new TimeoutException("エラー: サーバーへの接続がタイムアウト");
             }
 
             using (SslStream sslStream = new SslStream(
                    client.GetStream(),
                    false,
                    new RemoteCertificateValidationCallback(SslCertificateValidator.ValidateServerCertificate),
                    null))
             {
                // SSL / TLS接続を確立
                await sslStream.AuthenticateAsClientAsync(server);
 
                // サーバへメッセージを送信
                string message = "Hello, Secure Server";
                byte[] data = Encoding.UTF8.GetBytes(message);
 
                var sendTask = sslStream.WriteAsync(data, 0, data.Length);
                if (await Task.WhenAny(sendTask, Task.Delay(timeoutMilliseconds)) != sendTask)
                {
                   throw new TimeoutException("エラー: メッセージの送信がタイムアウト");
                }
 
                Console.WriteLine($"送信メッセージ: {message}");
             }
          }
       }
       catch (SocketException e)
       {  // ネットワーク関連のエラーが発生した場合
          Console.WriteLine($"SocketException: {e.Message}");
       }
       catch (TimeoutException e)
       {  // タイムアウトが発生した場合
          Console.WriteLine($"TimeoutException: {e.Message}");
       }
       catch (AuthenticationException e)
       {  // SSL/TLS認証に失敗した場合
          Console.WriteLine($"AuthenticationException: SSL/TLS認証に失敗: {e.Message}");
       }
       catch (Exception e)
       {  // その他の予期せぬエラーが発生した場合
          Console.WriteLine($"予期せぬエラーが発生: {e.Message}");
       }
    }
 }


 // ※注意
 // 実務では、使用環境に応じて、さらに厳密な検証ロジックを追加することを推奨する。
 // また、可能な限り信頼された認証局によって発行された証明書を使用することを推奨する。
 // 
 // 信頼された証明書のリストは定期的に更新して、不要になった証明書は速やかに削除すること。
 
 using System;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 public class SslCertificateValidator
 {
    // 信頼された証明書のサムプリント
    // 実際の環境に合わせて更新すること
    private static readonly string[] TrustedCertificates = new string[]
    {
       "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0",
       "B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1"
    };
 
    public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
       if (sslPolicyErrors == SslPolicyErrors.None)
       {  // 証明書チェーンとポリシーエラーが無い場合
          LogMessage("情報", "証明書の検証に成功");
          return true;
       }
 
       LogMessage("警告", $"証明書エラー: {sslPolicyErrors}");
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
       {
          return HandleChainErrors(chain);
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
       {
          LogMessage("エラー", "証明書の名前が不一致");
          return false;
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
       {
          LogMessage("エラー", "リモート証明書が利用不可");
          return false;
       }
 
       // その他のエラーの場合
       return false;
    }
 
    private static bool HandleChainErrors(X509Chain chain)
    {
       foreach (X509ChainStatus status in chain.ChainStatus)
       {
          if (status.Status == X509ChainStatusFlags.UntrustedRoot)
          {
             X509Certificate2 rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
             if (ValidateSelfSignedCertificate(rootCert))
             {
                LogMessage("情報", "信頼されたルート証明書を確認");
                return true;
             }
          }
 
          LogMessage("エラー", $"証明書チェーンエラー: {status.StatusInformation}");
       }
 
       return false;
    }
 
    private static bool ValidateSelfSignedCertificate(X509Certificate certificate)
    {
       string certHash = certificate.GetCertHashString();
 
       if (TrustedCertificates.Contains(certHash, StringComparer.OrdinalIgnoreCase))
       {
          LogMessage("情報", "信頼された自己署名証明書を確認");
          return true;
       }
 
       LogMessage("エラー", $"未知の自己署名証明書を確認  サムプリント: {certHash}");
       return false;
    }
 
    private static void LogMessage(string level, string message)
    {
       string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
       Console.WriteLine(logMessage);
 
       // ファイルやデータベースへのログ記録処理等を追加
    }
 }


受信

以下の例では、TcpClientクラスを使用して、非同期でサーバに接続およびメッセージを受信している。
また、タイムアウトが発生した場合の再試行処理や高度なエラー処理等も追加することを推奨する。

 using System;
 using System.Text;
 using System.IO;
 using System.Threading.Tasks;
 using System.Net.Sockets;
 
 class AsyncTcpClientReceiver
 {
    static async Task Main(string[] args)
    {
       string server           = "<ホスト名またはIPアドレス>";  // サーバーのアドレス
       int port                = <ト番号>;                // ポート番号
       int timeoutMilliseconds = 5000;                      // タイムアウト(ミリ秒)
 
       try
       {
          using (TcpClient client = new TcpClient())
          {
             // サーバへ接続
             var connectTask = client.ConnectAsync(server, port);
 
             // サーバへの接続が確立するまで待機
             if (await Task.WhenAny(connectTask, Task.Delay(timeoutMilliseconds)) != connectTask)
             {  // タイムアウトが発生した場合
                throw new TimeoutException("エラー: サーバへの接続がタイムアウト");
             }
 
             using (NetworkStream stream = client.GetStream())
             {  // サーバへメッセージを送信
                byte[] data = new byte[256];
                var readTask = stream.ReadAsync(data, 0, data.Length);
                if (await Task.WhenAny(readTask, Task.Delay(timeoutMilliseconds)) != readTask)
                {
                   throw new TimeoutException("エラー: メッセージの受信がタイムアウト");
                }
 
                int bytes = await readTask;
                string responseData = Encoding.UTF8.GetString(data, 0, bytes);
                Console.WriteLine($"受信メッセージ: {responseData}");
             }
          }
       }
       catch (ArgumentNullException e)
       {  // 無効な引数が渡された場合
          Console.WriteLine($"ArgumentNullException: {e.Message}");
       }
       catch (SocketException e)
       {  // ネットワーク関連のエラーが発生した場合
          Console.WriteLine($"SocketException: {e.Message}");
       }
       catch (IOException e)
       {  // ストリームの読み書き中にエラーが発生した場合
          Console.WriteLine($"IOException: {e.Message}");
       }
       catch (TimeoutException e)
       {  // タイムアウトが発生した場合
          Console.WriteLine($"TimeoutException: {e.Message}");
       }
       catch (Exception e)
       {  // その他の予期せぬエラーが発生した場合
          Console.WriteLine($"予期せぬエラーが発生: {e.Message}");
       }
    }
 }


受信 (SSL / TLS対応)

以下の例では、SSL / TLS証明書を使用して、非同期でサーバに接続およびメッセージを受信している。
また、タイムアウトが発生した場合の再試行処理や高度なエラー処理等も追加することを推奨する。

※注意
サーバ側でもSSL / TLS証明書の対応が必要となる。
実務では、適切な証明書の管理と定期的な更新が重要である。

 using System;
 using System.Text;
 using System.Threading.Tasks;
 using System.Net.Sockets;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 class SslAsyncTcpClientReceiver
 {
    static async Task Main(string[] args)
    {
       string server = "<ホスト名またはIPアドレス>";  // サーバーのアドレス
       int port = <ト番号>;                     // ポート番号
       int timeoutMilliseconds = 5000;            // タイムアウト(ミリ秒)
 
       try
       {
          using (TcpClient client = new TcpClient())
          {
             // サーバへ接続
             var connectTask = client.ConnectAsync(server, port);
             if (await Task.WhenAny(connectTask, Task.Delay(timeoutMilliseconds)) != connectTask)
             {
                throw new TimeoutException("エラー: サーバーへの接続がタイムアウト");
             }
 
             using (SslStream sslStream = new SslStream(
                    client.GetStream(),
                    false,
                    new RemoteCertificateValidationCallback(SslCertificateValidator.ValidateServerCertificate),
                    null))
             {
                // SSL/TLS接続を確立
                await sslStream.AuthenticateAsClientAsync(server);
 
                // サーバからメッセージを受信
                byte[] buffer = new byte[4096];
                var receiveTask = sslStream.ReadAsync(buffer, 0, buffer.Length);
                if (await Task.WhenAny(receiveTask, Task.Delay(timeoutMilliseconds)) != receiveTask)
                {
                   throw new TimeoutException("エラー: メッセージの受信がタイムアウト");
                }
 
                int bytesRead = await receiveTask;
                string receivedMessage = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                Console.WriteLine($"受信メッセージ: {receivedMessage}");
             }
          }
       }
       catch (SocketException e)
       {  // ネットワーク関連のエラーが発生した場合
          Console.WriteLine($"SocketException: {e.Message}");
       }
       catch (TimeoutException e)
       {  // タイムアウトが発生した場合
          Console.WriteLine($"TimeoutException: {e.Message}");
       }
       catch (AuthenticationException e)
       {  // SSL/TLS証明書の認証に失敗した場合
          Console.WriteLine($"AuthenticationException: SSL/TLS認証に失敗: {e.Message}");
       }
       catch (Exception e)
       {  // その他の予期せぬエラーが発生した場合
          Console.WriteLine($"予期せぬエラーが発生: {e.Message}");
       }
    }
 }


 // ※注意
 // 実務では、使用環境に応じて、さらに厳密な検証ロジックを追加することを推奨する。
 // また、可能な限り信頼された認証局によって発行された証明書を使用することを推奨する。
 // 
 // 信頼された証明書のリストは定期的に更新して、不要になった証明書は速やかに削除すること。
 
 using System;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 public class SslCertificateValidator
 {
    // 信頼された証明書のサムプリント
    // 実際の環境に合わせて更新すること
    private static readonly string[] TrustedCertificates = new string[]
    {
       "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0",
       "B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1"
    };
 
    public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
       if (sslPolicyErrors == SslPolicyErrors.None)
       {  // 証明書チェーンとポリシーエラーが無い場合
          LogMessage("情報", "証明書の検証に成功");
          return true;
       }
 
       LogMessage("警告", $"証明書エラー: {sslPolicyErrors}");
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
       {
          return HandleChainErrors(chain);
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
       {
          LogMessage("エラー", "証明書の名前が不一致");
          return false;
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
       {
          LogMessage("エラー", "リモート証明書が利用不可");
          return false;
       }
 
       // その他のエラーの場合
       return false;
    }
 
    private static bool HandleChainErrors(X509Chain chain)
    {
       foreach (X509ChainStatus status in chain.ChainStatus)
       {
          if (status.Status == X509ChainStatusFlags.UntrustedRoot)
          {
             X509Certificate2 rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
             if (ValidateSelfSignedCertificate(rootCert))
             {
                LogMessage("情報", "信頼されたルート証明書を確認");
                return true;
             }
          }
 
          LogMessage("エラー", $"証明書チェーンエラー: {status.StatusInformation}");
       }
 
       return false;
    }
 
    private static bool ValidateSelfSignedCertificate(X509Certificate certificate)
    {
       string certHash = certificate.GetCertHashString();
 
       if (TrustedCertificates.Contains(certHash, StringComparer.OrdinalIgnoreCase))
       {
          LogMessage("情報", "信頼された自己署名証明書を確認");
          return true;
       }
 
       LogMessage("エラー", $"未知の自己署名証明書を確認  サムプリント: {certHash}");
       return false;
    }
 
    private static void LogMessage(string level, string message)
    {
       string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
       Console.WriteLine(logMessage);
 
       // ファイルやデータベースへのログ記録処理等を追加
    }
 }