web-dev-qa-db-ja.com

.NETのNetworkStreamから読み取る正しい方法は何ですか

私はこれに苦労してきましたが、コードが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を返すことができますか?)そしてデータが利用できない最後の読み取りで無期限にブロックします...

はい、私は迷っています!

22
Loudenvier

基になるソケットReceiveTimeoutプロパティを設定すると、うまくいきました。次のようにアクセスできます:yourTcpClient.Client.ReceiveTimeout。詳細については、 docs を参照してください。

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

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());
}
8
Loudenvier

ネットワークコードは、書く、テストする、デバッグするのが難しいことで有名です。

次のような多くのことを考慮する必要があります。

  • 交換されるデータにどの「エンディアン」を使用しますか(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フラグを確認してから、 _.Length_プロパティをチェックするバッファ内stm.Read(resp, 0, stm.Length);

20
Colin Smith

要件に応じて、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());
}

お役に立てれば!

0
Furqan Safdar