web-dev-qa-db-ja.com

C#でのソケットプログラミングの開始-ベストプラクティス

SO Socketsについて多くのリソースを見てきました。知りたい詳細をカバーしているものはないと思います。私のアプリケーションでは、サーバーがすべての処理を行い、クライアントに定期的な更新を送信します。 。

この投稿の目的は、ソケットアプリケーションを開発するときに必要なすべての基本的なアイデアをカバーし、ベストプラクティスについて説明することです。これは、ほとんどすべてのソケットベースのアプリケーションで見られる基本的なことです。

1 -ソケットでのバインドとリッスン

私は次のコードを使用しています。それは私のマシンでうまく機能します。これを実サーバーにデプロイするときに、他のことに注意する必要がありますか?

IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName());
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444);

serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
                     ProtocolType.Tcp);
serverSocket.Bind(endPoint);
serverSocket.Listen(10);

2 -データの受信

私は255サイズのバイト配列を使用しました。したがって、255バイトを超えるデータを受信して​​いる場合は、完全なデータを取得するまでreceiveメソッドを呼び出す必要があります。完全なデータを取得したら、これまでに受信したすべてのバイトを追加して、完全なメッセージを取得する必要があります。あれは正しいですか?それとももっと良いアプローチがありますか?

3 --データの送信とデータ長の指定

TCPで受信するメッセージの長さを見つける方法がないため、メッセージに長さを追加する予定です。これがパケットの最初のバイトになります。したがって、クライアントシステム読み取ることができるデータの量を知っています。

他のより良いアプローチはありますか?

4 -クライアントを閉じる

クライアントが閉じられると、閉じたことを示すメッセージがサーバーに送信されます。サーバーは、クライアントリストからクライアントの詳細を削除します。以下は、ソケットを切断するためにクライアント側で使用されるコードです(メッセージング部分は示されていません)。

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

何か提案や問題はありますか?

5 --サーバーを閉じる

サーバーは、シャットダウンを示すメッセージをすべてのクライアントに送信します。各クライアントは、このメッセージを受信するとソケットを切断します。クライアントはクローズメッセージをサーバーに送信してクローズします。サーバーはすべてのクライアントからクローズメッセージを受信すると、ソケットを切断してリスニングを停止します。各クライアントソケットでDisposeを呼び出して、リソースを解放します。それは正しいアプローチですか?

6 -不明なクライアントの切断

場合によっては、クライアントがサーバーに通知せずに切断することがあります。これを処理する私の計画は次のとおりです。サーバーがすべてのクライアントにメッセージを送信するとき、ソケットのステータスを確認します。接続されていない場合は、そのクライアントをクライアントリストから削除し、そのクライアントのソケットを閉じます。

どんな助けでも素晴らしいでしょう!

35
Navaneeth K N

これは「はじめに」なので、私の答えは、拡張性の高い実装ではなく、単純な実装に固執します。物事をより複雑にする前に、最初に単純なアプローチに慣れることが最善です。

1-バインディングとリスニング
あなたのコードは私には問題ないようです、個人的に私は以下を使用します:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));

DNSルートを使用するのではなく、どちらにしても実際の問題はないと思います。

1.5-クライアント接続の受け入れ
完全を期すためにこれに言及するだけです...私はあなたがこれをしていると仮定しています。さもなければあなたはステップ2に到達しないでしょう。

2-データの受信
すべてのサーバーメッセージが最大255バイトであると期待できない限り、バッファを255バイトより少し長くします。 TCPパケットサイズよりも大きくなる可能性が高いバッファが必要だと思います。そうすれば、単一のデータブロックを受信するために複数の読み取りを行う必要がなくなります。

1500バイトを選択するのは問題ないはずですが、Niceラウンド数の場合は2048バイトでもかまいません。

または、byte[]を使用してデータフラグメントを格納することを避け、代わりにサーバー側のクライアントソケットをNetworkStreamでラップし、BinaryReaderでラップすることもできます。バッファサイズを気にせずに、ソケットからメッセージのコンポーネントを直接読み取ります。

-データの送信とデータ長の指定
あなたのアプローチはうまく機能しますが、送信を開始する前にパケットの長さを簡単に計算できる必要があることは明らかです。

または、メッセージ形式(コンポーネントの順序)が、クライアントが次のデータを増やす必要があるかどうかをいつでも判断できるように設計されている場合(たとえば、コード0x01は、次がintであり、文字列、コード0x02は、次が16バイトになることを意味します。クライアント側のNetworkStreamアプローチと組み合わせると、これは非常に効果的なアプローチになる可能性があります。

安全のために、受信したコンポーネントの検証を追加して、正常な値のみを処理するようにすることをお勧めします。たとえば、長さが1 TBの文字列の指示を受け取った場合、どこかでパケットが破損している可能性があり、接続を閉じてクライアントに再接続を強制し、「最初からやり直す」方が安全な場合があります。このアプローチにより、予期しない障害が発生した場合に非常に優れたキャッチオール動作が得られます。

4/5-クライアントとサーバーを閉じる
個人的には、メッセージなしでCloseだけを選択します。接続が閉じられると、接続のもう一方の端での読み取り/書き込みのブロックで例外が発生し、それに対応する必要があります。

堅牢なソリューションを得るには、とにかく「不明な切断」に対応する必要があるため、切断をさらに複雑にすることは一般的に無意味です。

6-不明な切断
ソケットのステータスすら信用しません...クライアントまたはサーバーが気付かないうちに、クライアント/サーバー間のパスのどこかで接続が切断される可能性があります。

予期せず停止した接続を通知する唯一の保証された方法は、次に接続に沿って何かを送信しようとしたときです。その時点で、接続に問題が発生した場合は常に失敗を示す例外が発生します。

その結果、すべての予期しない接続を検出する唯一の確実な方法は、「ping」メカニズムを実装することです。理想的には、クライアントとサーバーが定期的にメッセージを相手側に送信し、その結果、 「ping」を受信しました。

不要なpingを最適化するには、他のトラフィックが設定された時間受信されなかった場合にのみpingを送信する「タイムアウト」メカニズムが必要になる場合があります(たとえば、サーバーがx秒以上経過している場合、クライアントはpingを送信して、通知なしに接続が切断されていないことを確認します。

より高度な
高いスケーラビリティが必要な場合は、すべてのソケット操作(Accept/Send/Receive)の非同期メソッドを調べる必要があります。これらは「開始/終了」バリアントですが、使用するのがはるかに複雑です。

単純なバージョンが起動して機能するまで、これを試すことはお勧めしません。

また、数十のクライアントを超えて拡張することを計画していない場合でも、これは実際には問題にならないことに注意してください。非同期技術が本当に必要なのは、サーバーを完全に停止させずに、接続された数千または数十万のクライアントに拡張する場合のみです。

私はおそらく他の重要な提案をたくさん忘れていますが、これはあなたが最初にかなり堅牢で信頼できる実装を得るのに十分なはずです

27
jerryjvl

1-ソケットでのバインドとリッスン

私には元気そうだ。ただし、コードはソケットを1つのIPアドレスにのみバインドします。単にIPアドレス/ネットワークインターフェースでリッスンしたい場合は、IPAddress.Anyを使用してください。

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));

将来の証拠として、IPv6をサポートすることをお勧めします。 IPv6アドレスをリッスンするには、IPAddress.IPv6Anyの代わりにIPAddress.Anyを使用します。

Dual-Stack Socket を使用する場合を除いて、IPv4アドレスとIPv6アドレスを同時にリッスンすることはできないことに注意してください。これには、IPV6_V6ONLYソケットオプションの設定を解除する必要があります。

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0);

ソケットで Teredo を有効にするには、PROTECTION_LEVEL_UNRESTRICTEDソケットオプションを設定する必要があります。

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10);

2-データの受信

チャンクを手動で読み取る代わりに、ソケットをNetworkStreamでラップする Stream を使用することをお勧めします。

ただし、固定バイト数の読み取りは少し厄介です。

using (var stream = new NetworkStream(serverSocket)) {
   var buffer = new byte[MaxMessageLength];
   while (true) {
      int type = stream.ReadByte();
      if (type == BYE) break;
      int length = stream.ReadByte();
      int offset = 0;
      do
         offset += stream.Read(buffer, offset, length - offset);
      while (offset < length);
      ProcessMessage(type, buffer, 0, length);
   }
}

NetworkStreamが本当に優れているのは、他のStreamと同じように使用できることです。セキュリティが重要な場合は、NetworkStreamSslStream でラップして、サーバーと(オプションで)クライアントをX.509証明書で認証します。圧縮も同じように機能します。

var sslStream = new SslStream(stream, false);
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true);
// receive/send data SSL secured

3-データの送信とデータ長の指定

あなたのアプローチはうまくいくはずですが、おそらく車輪の再発明に進んでこのための新しいプロトコルを設計したくないかもしれません。 [〜#〜] beep [〜#〜] または protobuf のような単純なものを見てください。

目標によっては、WCFやその他のRPCメカニズムなどのソケットよりも抽象化を選択することを検討する価値があるかもしれません。

4/5/6-クロージングと不明な切断

内容 jerryjvl 発言:-)信頼できる検出メカニズムは、接続がアイドル状態のときにpingを送信するかキープアライブを送信することだけです。

いずれにせよ、未知の切断に対処する必要がありますが、私は個人的に、警告なしに接続を閉じるのではなく、相互に合意して接続を閉じるためにいくつかのプロトコル要素を保持します。

13
dtb

非同期ソケットの使用を検討してください。あなたは主題に関するより多くの情報をで見つけることができます

2
Dzmitry Huba