25

私はこれに苦労していて、私のコードが私も書いたTCPサーバーから正しく読み取れない理由を見つけることができません。TcpClientクラスとそのGetStream()メソッドを使用していますが、期待どおりに機能していません。操作が無期限にブロックされるか(最後の読み取り操作が期待どおりにタイムアウトしない)、またはデータがトリミングされます(何らかの理由で、読み取り操作が0を返し、ループを終了します。サーバーが十分な速度で応答していない可能性があります)。この機能を実装するための3つの試みは次のとおりです。

// this will break from the loop without getting the entire 4804 bytes from the server 
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        bytes = 0;
        if (stm.DataAvailable)
            bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

// this will block forever. It reads everything but freezes when data is exhausted
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

// inserting a sleep inside the loop will make everything work perfectly
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        Thread.Sleep(20);
        bytes = 0;
        if (stm.DataAvailable)
            bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

最後のものは「機能」しますが、ソケットがすでに読み取りタイムアウトをサポートしていることを考えると、ハードコードされたスリープをループ内に置くのは確かに醜いようです。のにいくつかのプロパティを設定する必要がありますTcpClientNetworkStream?問題はサーバーにありますか?サーバーは接続を閉じません。閉じるのはクライアント次第です。上記はUIスレッドコンテキスト(テストプログラム)内でも実行されていますが、おそらくそれと関係があります...

誰かが、NetworkStream.Readデータが利用できなくなるまでデータを読み取るために適切に使用する方法を知っていますか?私が望んでいるのは、古いWin32 winsockタイムアウトプロパティのようなものだと思います...ReadTimeoutなど。タイムアウトに達するまで読み取ろうとし、その後0を返します...しかし、データが必要なときに0を返すように見えることがあります。利用可能である(または途中で..利用可能な場合はReadが0を返すことができますか?)そして、データが利用できない場合、最後の読み取りで無期限にブロックします。

はい、途方に暮れています!

4

3 に答える 3

21

ネットワークコードは、記述、テスト、およびデバッグが難しいことで有名です。

多くの場合、次のような考慮すべきことがたくさんあります。

  • 交換されるデータにどの「エンディアン」を使用しますか(Intel x86 / x64はリトルエンディアンに基づいています)-ビッグエンディアンを使用するシステムでも、リトルエンディアンのデータを読み取ることができます(またはその逆)。データを再配置する必要があります。「プロトコル」を文書化するときは、使用しているプロトコルを明確にしてください。

  • 「ストリーム」の動作に影響を与える可能性のあるソケットに設定された「設定」はありますか(例:SO_LINGER)-コードが非常に機密性の高い場合は、特定の設定をオンまたはオフにする必要があります

  • ストリームの遅延を引き起こす現実世界の輻輳は、読み取り/書き込みロジックにどのように影響しますか

クライアントとサーバー間で(どちらの方向にも)交換される「メッセージ」のサイズが異なる可能性がある場合、その「メッセージ」を信頼できる方法で交換するための戦略(別名プロトコル)を使用する必要があります。

交換を処理するいくつかの異なる方法があります:

  • データの前にあるヘッダーにメッセージサイズをエンコードします。これは、送信された最初の2/4/8バイトの「数値」(最大メッセージサイズによって異なります)の場合もあれば、よりエキゾチックな「ヘッダー」の場合もあります。

  • 特別な「メッセージの終わり」マーカー(センチネル)を使用します。実際のデータが「マーカーの終わり」と混同される可能性がある場合は、実際のデータをエンコード/エスケープします。

  • タイムアウトを使用します。つまり、バイトを受信しない一定の期間は、メッセージのデータがなくなることを意味します。ただし、タイムアウトが短いとエラーが発生しやすくなり、混雑したストリームで簡単に発生する可能性があります。

  • 別々の「接続」に「コマンド」チャネルと「データ」チャネルがある....これはFTPプロトコルが使用するアプローチです(利点は、コマンドからデータを明確に分離することです... 2番目の接続を犠牲にして)

それぞれのアプローチには、「正確さ」の長所と短所があります。

以下のコードは、「タイムアウト」メソッドを使用しています。これは、必要な方法のようです。

http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspxを参照してください。にアクセスできるのでNetworkStreamTCPClientを変更できますReadTimeout

string SendCmd(string cmd, string ip, int port)
{
  var client = new TcpClient(ip, port);
  var data = Encoding.GetEncoding(1252).GetBytes(cmd);
  var stm = client.GetStream();
  // Set a 250 millisecond timeout for reading (instead of Infinite the default)
  stm.ReadTimeout = 250;
  stm.Write(data, 0, data.Length);
  byte[] resp = new byte[2048];
  var memStream = new MemoryStream();
  int bytesread = stm.Read(resp, 0, resp.Length);
  while (bytesread > 0)
  {
      memStream.Write(resp, 0, bytesread);
      bytesread = stm.Read(resp, 0, resp.Length);
  }
  return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

この書き込みネットワークコードの他のバリエーションの脚注として... Read「ブロック」を回避したい場所を実行するときは、フラグをチェックしてから、プロパティDataAvailableをチェックするバッファにあるものだけを読み取ることができます。.Lengthstm.Read(resp, 0, stm.Length);

于 2012-10-27T05:06:08.860 に答える
11

基になるソケットReceiveTimeoutプロパティを設定することでうまくいきました。次のようにアクセスできますyourTcpClient.Client.ReceiveTimeout詳細については、ドキュメントを読むことができます。

これで、コードは、一部のデータがソケットに到着するのに必要な場合にのみ「スリープ」します。または、読み取り操作の開始時に20ミリ秒を超えてデータが到着しない場合は、例外が発生します。必要に応じて、このタイムアウトを微調整できます。今、私はすべての反復で20ミリ秒の価格を支払っていません。最後の読み取り操作でのみ支払っています。サーバーから読み取られた最初のバイトのメッセージのコンテンツ長があるので、それを使用してメッセージをさらに微調整し、予想されるすべてのデータがすでに受信されている場合は読み取ろうとしないでください。

非同期読み取りを実装するよりもReceiveTimeoutを使用する方がはるかに簡単だと思います...動作するコードは次のとおりです。

string SendCmd(string cmd, string ip, int port)
{
  var client = new TcpClient(ip, port);
  var data = Encoding.GetEncoding(1252).GetBytes(cmd);
  var stm = client.GetStream();
  stm.Write(data, 0, data.Length);
  byte[] resp = new byte[2048];
  var memStream = new MemoryStream();
  var bytes = 0;
  client.Client.ReceiveTimeout = 20;
  do
  {
      try
      {
          bytes = stm.Read(resp, 0, resp.Length);
          memStream.Write(resp, 0, bytes);
      }
      catch (IOException ex)
      {
          // if the ReceiveTimeout is reached an IOException will be raised...
          // with an InnerException of type SocketException and ErrorCode 10060
          var socketExept = ex.InnerException as SocketException;
          if (socketExept == null || socketExept.ErrorCode != 10060)
              // if it's not the "expected" exception, let's not hide the error
              throw ex;
          // if it is the receive timeout, then reading ended
          bytes = 0;
      }
  } while (bytes > 0);
  return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
于 2012-10-27T06:13:29.087 に答える
0

要件に応じThread.Sleepて、データがいつ利用可能になるかわからないため、データが利用可能になるまで待つ必要がある場合があるため、完全に使用できます。関数のロジックを少し変更しました。これはもう少し役立つかもしれません。

string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();

    int bytes = 0;

    do
    {
        bytes = 0;
        while (!stm.DataAvailable)
            Thread.Sleep(20); // some delay
        bytes = stm.Read(resp, 0, resp.Length);
        memStream.Write(resp, 0, bytes);
    } 
    while (bytes > 0);

    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

お役に立てれば!

于 2012-10-27T04:59:49.273 に答える