What is the correct way to read from NetworkStream in .NET

前端 未结 3 1819
没有蜡笔的小新
没有蜡笔的小新 2020-11-27 04:06

I\'ve been struggling with this and can\'t find a reason why my code is failing to properly read from a TCP server I\'ve also written. I\'m using the TcpClient

相关标签:
3条回答
  • 2020-11-27 04:30

    Networking code is notoriously difficult to write, test and debug.

    You often have lots of things to consider such as:

    • what "endian" will you use for the data that is exchanged (Intel x86/x64 is based on little-endian) - systems that use big-endian can still read data that is in little-endian (and vice versa), but they have to rearrange the data. When documenting your "protocol" just make it clear which one you are using.

    • are there any "settings" that have been set on the sockets which can affect how the "stream" behaves (e.g. SO_LINGER) - you might need to turn certain ones on or off if your code is very sensitive

    • how does congestion in the real world which causes delays in the stream affect your reading/writing logic

    If the "message" being exchanged between a client and server (in either direction) can vary in size then often you need to use a strategy in order for that "message" to be exchanged in a reliable manner (aka Protocol).

    Here are several different ways to handle the exchange:

    • have the message size encoded in a header that precedes the data - this could simply be a "number" in the first 2/4/8 bytes sent (dependent on your max message size), or could be a more exotic "header"

    • use a special "end of message" marker (sentinel), with the real data encoded/escaped if there is the possibility of real data being confused with an "end of marker"

    • use a timeout....i.e. a certain period of receiving no bytes means there is no more data for the message - however, this can be error prone with short timeouts, which can easily be hit on congested streams.

    • have a "command" and "data" channel on separate "connections"....this is the approach the FTP protocol uses (the advantage is clear separation of data from commands...at the expense of a 2nd connection)

    Each approach has its pros and cons for "correctness".

    The code below uses the "timeout" method, as that seems to be the one you want.

    See http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx. You can get access to the NetworkStream on the TCPClient so you can change the 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());
    }
    

    As a footnote for other variations on this writing network code...when doing a Read where you want to avoid a "block", you can check the DataAvailable flag and then ONLY read what is in the buffer checking the .Length property e.g. stm.Read(resp, 0, stm.Length);

    0 讨论(0)
  • 2020-11-27 04:37

    As per your requirement, Thread.Sleep is perfectly fine to use because you are not sure when the data will be available so you might need to wait for the data to become available. I have slightly changed the logic of your function this might help you little further.

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

    Hope this helps!

    0 讨论(0)
  • 2020-11-27 04:38

    Setting the underlying socket ReceiveTimeout property did the trick. You can access it like this: yourTcpClient.Client.ReceiveTimeout. You can read the docs for more information.

    Now the code will only "sleep" as long as needed for some data to arrive in the socket, or it will raise an exception if no data arrives, at the beginning of a read operation, for more than 20ms. I can tweak this timeout if needed. Now I'm not paying the 20ms price in every iteration, I'm only paying it at the last read operation. Since I have the content-length of the message in the first bytes read from the server I can use it to tweak it even more and not try to read if all expected data has been already received.

    I find using ReceiveTimeout much easier than implementing asynchronous read... Here is the working code:

    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());
    }
    
    0 讨论(0)
提交回复
热议问题