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

我一直在为此而苦苦挣扎,无法find我的代码无法正确读取我写的TCP服务器的原因。 我正在使用TcpClient类和它的GetStream()方法,但没有按预期工作。 无论是操作块无限期(最后一次读取操作没有按预期超时),或数据被裁剪(出于某种原因,读取操作返回0并退出循环,也许服务器没有足够快的响应)。 这是实现这个function的三个尝试:

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

最后一个“起作用”,但是考虑到套接字已经支持读取超时,在循环内部放置一个硬编码的睡眠肯定看起来很丑陋! 我是否需要在NetworkStreamTcpClient上设置一些属性? 问题是否存在于服务器中? 服务器不closures连接,这取决于客户端。 上面也在UI线程上下文(testing程序)内运行,也许它有一些与… …

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

是的,我不知所措

networking代码是非常难以编写,testing和debugging的。

你经常有很多事情要考虑,比如:

  • 你将使用什么“endian”来交换数据(Intel x86 / x64是基于little-endian的) – 使用big-endian的系统仍然可以读取little-endian(反之亦然)的数据,但是它们必须重新排列数据。 当logging你的“协议”时,只要清楚你正在使用哪一个。

  • 是否有任何设置在套接字上设置,可以影响“stream”的行为(例如SO_LINGER) – 如果您的代码非常敏感,您可能需要打开或closures某些设置

  • 在现实世界中造成stream延迟的拥塞如何影响你的读/写逻辑

如果在客户端和服务器之间(在任何方向上)交换的“消息”的大小可能不同,那么为了使这个“消息”以可靠的方式交换(即协议),通常需要使用策略。

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

  • 将消息大小编码在数据之前的头部 – 这可能仅仅是发送的头2/4/8字节中的“数字”(取决于您的最大消息大小),或者可能是更具异国情调的“标题”

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

  • 使用超时…即没有接收字节的一定时间意味着没有更多的消息数据 – 但是,这可能是容易出错的短暂超时,这可以很容易地击中拥塞的stream。

  • 在单独的“连接”上有一个“命令”和“数据”通道….这是FTP协议使用的方法(优点是数据与命令清晰分离…以第二连接为代价)

每种方法都有其“正确性”的优点和缺点。

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

请参阅http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx 。 您可以访问TCPClient上的NetworkStream ,以便更改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()); } 

作为这个编写networking代码的其他变化的脚注…当你想要避免一个“块”的地方做一个Read ,你可以检查DataAvailable标志,然后只读取缓冲区中检查.Length属性,如stm.Read(resp, 0, stm.Length);

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

现在,只要需要某些数据到达套接字,代码就会“hibernate”,否则,如果在读操作开始时没有数据到达超过20ms,则会引发exception。 如果需要,我可以调整这个超时。 现在我在每次迭代中都不支付20ms的价格,我只是在最后一次读取操作中付款。 由于我从服务器读取的第一个字节的消息的内容长度,我可以使用它来调整它更多,而不是尝试读取,如果所有预期的数据已经收到。

我发现使用ReceiveTimeout比实现asynchronous读取更容易…下面是工作代码:

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

根据您的要求, Thread.Sleep使用起来非常好,因为您不确定数据何时可用,所以您可能需要等待数据可用。 我已经稍微改变了你的function的逻辑,这可能会帮助你更进一步。

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

希望这可以帮助!