25

我一直在为此苦苦挣扎,找不到我的代码无法从我也编写的 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());
}

最后一个“有效”,但考虑到套接字已经支持读取超时,将硬编码的睡眠放入循环中肯定看起来很难看!我需要在 的 上设置一些属性TcpClientNetworkStream?问题是否存在于服务器上?服务器不会关闭连接,这取决于客户端。以上也在UI线程上下文(测试程序)中运行,可能与此有关...

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

是的,我不知所措!

4

3 回答 3

21

众所周知,网络代码难以编写、测试和调试。

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

  • 您将使用什么“endian”来交换数据(英特尔 x86/x64 基于 little-endian) - 使用 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);

于 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 回答