从.NET中的NetworkStream读取的正确方法是什么

Lou*_*ier 23 .net c# sockets tcpclient

我一直在努力解决这个问题,并且无法找到我的代码无法正确读取我已编写的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());
}
Run Code Online (Sandbox Code Playgroud)

最后一个"工作",但考虑到套接字已经支持读取超时,在循环中放置硬编码睡眠看起来很难看!我需要设置的一些属性(IES)TcpClientNetworkStream?问题是否存在于服务器中?服务器不关闭连接,由客户端执行此操作.以上也在UI线程上下文(测试程序)中运行,也许它与...有关...

有人知道如何正确使用NetworkStream.Read读取数据,直到没有更多数据可用?我想我想要的是类似于旧的Win32 winsock超时属性... ReadTimeout等等.它尝试读取直到达到超时,然后返回0 ...但是当数据应该有时似乎返回0可用(或在途中..如果可用,可以读取返回0吗?)然后当数据不可用时,它会在最后一次读取时无限期地阻塞...

是的,我不知所措!

Col*_*ith 20

众所周知,网络代码很难编写,测试和调试.

您经常需要考虑很多事情,例如:

  • 什么"endian"将用于交换的数据(Intel x86/x64基于little-endian) - 使用big-endian的系统仍然可以读取little-endian的数据(反之亦然),但是必须重新安排数据.记录您的"协议"时,只需清楚说明您正在使用哪一个.

  • 是否有任何"设置"已在套接字上设置,可能会影响"流"的行为(例如SO_LINGER) - 如果您的代码非常敏感,您可能需要打开或关闭某些设置

  • 如何导致流中延迟的现实世界中的拥塞会影响您的读/写逻辑

如果在客户端和服务器之间交换的"消息"(在任一方向上)的大小可能不同,那么通常需要使用策略以便以可靠的方式(也称为协议)交换该"消息".

以下是处理交换的几种不同方法:

  • 将消息大小编码在数据之前的标题中 - 这可能只是发送的前2/4/8字节中的"数字"(取决于您的最大消息大小),或者可能是更奇特的"标题"

  • 如果真实数据可能与"标记结束"混淆,则使用特殊的"消息结束"标记(sentinel),对实际数据进行编码/转义

  • 使用超时....即,没有接收到字节的特定时间段意味着没有更多的数据用于消息 - 但是,这可能容易出错并且短暂超时,这很容易在拥挤的流上被击中.

  • 在单独的"连接"上有一个"命令"和"数据"通道....这是FTP协议使用的方法(优点是明确地将数据与命令分离......以第二次连接为代价)

每种方法都有其"正确性"的优缺点.

下面的代码使用"超时"方法,因为这似乎是你想要的.

请参阅http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx.你可以访问NetworkStreamon,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());
}
Run Code Online (Sandbox Code Playgroud)

作为编写网络代码的其他变体的脚注...当Read你想要避开"块"的地方时,你可以检查DataAvailable标志然后只读取缓冲区中检查.Length属性的内容,例如stm.Read(resp, 0, stm.Length);


Lou*_*ier 9

设置底层套接字ReceiveTimeout属性就可以了.你可以像这样访问它:yourTcpClient.Client.ReceiveTimeout.您可以阅读文档以获取更多信息.

现在,只要某些数据到达套接字所需的代码,代码将只"休眠",或者如果在读取操作开始时没有数据到达,则会引发异常超过20ms.如果需要,我可以调整此超时.现在我没有在每次迭代中支付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());
}
Run Code Online (Sandbox Code Playgroud)

  • 小心使用这个黑客.我们有一个Java应用程序,它依赖于套接字超时来检测消息结束,并且我们不时地在流中丢失一个字节.不可否认,我不知道这是否也适用于.NET,但我怀疑它是底层TCP/IP堆栈的原因.您不应该在超时后重用套接字. (2认同)