Каков правильный способ чтения из NetworkStream в .NET.

Я боролся с этим и не могу найти причину, по которой мой код не умеет правильно читать с TCP-сервера, который я также написал. Я использую класс TcpClient и его метод GetStream(), но что-то не работает должным образом. Либо операция блокируется бесконечно (последняя операция чтения не отключается, как ожидалось), либо данные обрезаны (по какой-либо причине операция чтения возвращает 0 и выходит из цикла, возможно, сервер не отвечает достаточно быстро). Это три попытки реализовать эту функцию:

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

Последний "работает", но он, безусловно, выглядит уродливым, чтобы помещать жесткий цикл внутри цикла, учитывая, что сокеты уже поддерживают таймауты чтения! Нужно ли настраивать какое-либо свойство (-и) на TcpClient NetworkStream? Проблема возникает на сервере? Сервер не закрывает соединения, это зависит от клиента. Вышеупомянутое также работает внутри контекста потока пользовательского интерфейса (тестовая программа), возможно, оно имеет какое-то отношение к этому...

Кто-нибудь знает, как правильно использовать NetworkStream.Read для чтения данных, пока не появится больше данных? Я предполагаю, что то, что я желаю, - это что-то вроде старых свойств таймаута Win32 winsock... ReadTimeout и т.д. Он пытается прочитать, пока не будет достигнут тайм-аут, а затем вернется 0... Но иногда кажется, что возвращается 0 когда данные должны быть доступны (или по пути.. может прочитать возврат 0, если он доступен?), и затем он блокируется бесконечно на последнем чтении, когда данные недоступны...

Да, я в недоумении!

Ответы

Ответ 1

Настройка базового сокета ReceiveTimeout сделала трюк. Вы можете получить доступ к нему следующим образом: yourTcpClient.Client.ReceiveTimeout. Вы можете прочитать docs для получения дополнительной информации.

Теперь код будет только "спать" до тех пор, пока необходимо, чтобы некоторые данные поступали в сокет, или он вызовет исключение, если в начале операции чтения не поступит никаких данных более чем на 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 not the "expected" exception, let 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());
}

Ответ 2

Сетевой код, как известно, трудно писать, тестировать и отлаживать.

У вас часто есть много вещей, которые нужно учитывать, например:

  • какой "endian" вы будете использовать для обменных данных (Intel x86/x64 основан на малоподобных) - системы, которые используют big-endian, могут читать данные, которые находятся в little-endian (и наоборот), но они должны переупорядочить данные. При документировании своего "протокола" просто дайте понять, какой из них вы используете.

  • Существуют ли какие-либо "настройки", которые были установлены в сокетах, которые могут влиять на поведение "потока" (например, SO_LINGER) - вам может потребоваться включить или отключить некоторые из них, если ваш код очень чувствителен

  • как перегруженность в реальном мире, которая вызывает задержки в потоке, влияет на вашу логику чтения/записи

Если "сообщение", которое обменивается между клиентом и сервером (в любом направлении), может различаться по размеру, тогда часто вам нужно использовать стратегию, чтобы это "сообщение" было обмениваться надежным образом (так называемый протокол).

Вот несколько способов обработки обмена:

  • имеют размер сообщения, закодированный в заголовке, который предшествует данным - это может быть просто "число" в первых отправленных 2/4/8 байтах (в зависимости от вашего максимального размера сообщения) или может быть более экзотический "заголовок"

  • используйте специальный маркер "конец сообщения" (дозорный), с реальными данными, закодированными/экранированными, если есть вероятность того, что реальные данные будут запутаны с "концом маркера"

  • использовать таймаут... т.е. определенный период отсутствия байтов означает, что для сообщения больше нет данных, однако это может быть причиной ошибки с короткими таймаутами, которые могут быть легко удалены по перегруженным потокам.

  • имеют канал "команда" и "данные" на отдельных "соединениях"... это подход, который использует протокол FTP (преимущество - четкое разделение данных от команд... за счет второе соединение)

Каждый подход имеет свои плюсы и минусы для "правильности".

В приведенном ниже коде используется метод "тайм-аут", поскольку он кажется тем, который вам нужен.

См. http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx. Вы можете получить доступ к NetworkStream в TCPClient, чтобы вы могли изменить 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);

Ответ 3

В соответствии с вашим требованием, 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());
}

Надеюсь, это поможет!