循环直到完全读取TcpClient响应

12

我写了一个简单的TCP客户端和服务器,问题出在客户端。

我在读取来自服务器的完整响应方面遇到了一些麻烦。我必须让线程休眠以允许发送所有数据。

我尝试过几次将这段代码转换为循环,直到服务器完成发送数据为止。

// Init & connect to client
TcpClient client = new TcpClient();
Console.WriteLine("Connecting.....");
client.Connect("192.168.1.160", 9988);

// Stream string to server
input += "\n";
Stream stm = client.GetStream();
ASCIIEncoding asen = new ASCIIEncoding();
byte[] ba = asen.GetBytes(input);
stm.Write(ba, 0, ba.Length);

// Read response from server.
byte[] buffer = new byte[1024];

System.Threading.Thread.Sleep(1000); // Huh, why do I need to wait?

int bytesRead = stm.Read(buffer, 0, buffer.Length);
response = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine("Response String: "+response);

client.Close();

4
客户端需要一种方法来判断服务器何时完成(例如,一个特殊终止序列的长度前缀)。 - SLaks
1
请尝试使用 StreamReader http://msdn.microsoft.com/zh-cn/library/system.io.streamreader.aspx - David
4
StreamReader如何帮助?它也不知道服务器何时完成。 - John Saunders
4个回答

28
建立在套接字之上的流的本质是,您拥有一个开放的管道,可以传输和接收数据,直到套接字关闭为止。
然而,由于客户端/服务器交互的本质,这个管道并不总是保证有内容可供读取。客户端和服务器必须达成协议,通过管道发送内容。
当您将 .NET 中的 Stream 抽象 与套接字的概念叠加在一起时,客户端和服务器之间仍然需要达成一致;您可以随意调用 Stream.Read,但如果连接到您的 Stream 的套接字的另一端没有发送内容,该调用将等待直到有内容。
这就是协议存在的原因。在最基本的级别上,它们有助于定义在两个方之间发送的完整消息。通常,机制是以下之一:
- 发送要读取的字节数之前的长度前缀消息 - 使用一些字符模式标记消息的结尾(取决于发送的内容,任何消息的任何部分都越任意,则越不太可能使用此方法)

话虽如此,你没有遵循上述步骤;你对Stream.Read的调用只是说“读取1024字节”,但实际上可能没有1024字节可读。如果是这种情况,调用Stream.Read将阻塞,直到有数据。

调用Thread.Sleep的原因可能是因为在一秒钟之后,Stream上有1024字节可读,它不会阻塞。

此外,如果你真的想读取1024字节,就不能假设调用Stream.Read将填充1024字节的数据。 Stream.Read方法的返回值告诉你实际读取了多少字节。如果你需要更多的消息,那么你需要再次调用Stream.Read

Jon Skeet写了一个确切的方法,如果你想要一个示例。


3
Jon Skeet的文章非常棒,谢谢你让我知道。+1 - ahmet alp balkan
对象反序列化提供了解决长度/消息结束标记问题的方法。例如,使用protobuf时,.Deserialize<SomeMessageType>(stream)将自动读取整个对象,直到读取完毕。在幕后,protobuf有其自己的确定对象起始和结束的方式。这假定序列化库正确实现了流。出于性能原因,文件流通常返回少于完整字节缓冲区的内容,因此任何正确编写的读取循环都应检查实际读取的字节数,并不要假设单个.Read已经填满了整个缓冲区。 - AaronLS
一个修正,你应该使用DeserializeWithLengthPrefix和它的SerializeWithLengthPrefix对应项,如https://dev59.com/HV_Va4cB1Zd3GeqPS2Na#8901333所述。 - AaronLS
1
也许在您编写此答案时情况有所不同,但是 Stream.Read 不会阻塞直到请求的字节数被读取。第三个参数的文档记录为“从当前流中读取的最大字节数。”其返回值的文档记录为“已读入缓冲区的总字节数。如果当前没有那么多字节可用,则可以少于请求的字节数,或者如果已到达流的末尾,则为零(0)。” - user247702

2

尝试重复

int bytesRead = stm.Read(buffer, 0, buffer.Length);

当bytesRead > 0时,这是一个常见的模式,我记得。当然,不要忘记为缓冲区传递适当的参数。


1

您不知道将要读取的数据大小,因此必须设置决策机制。一个是超时,另一个是使用分隔符。

在您的示例中,您从仅一个迭代(读取)中读取任何数据,因为您没有设置读取超时并且使用默认值即“0”毫秒。因此,您必须睡眠1000毫秒。使用接收超时设置为1000毫秒可以获得相同的效果。

我认为使用数据长度作为前缀不是真正的解决方案,因为当套接字被两侧关闭时,套接字等待时间无法正确处理。相同的数据可能会发送到服务器并导致服务器出现异常。我们使用前缀-结束字符序列。每次读取后,我们检查开始和结束字符序列的数据,如果我们无法获取结束字符,则调用另一个读取。但是,当然,这仅适用于您控制服务器端和客户端代码的情况。


我们真的需要自己实现这个“起始/结束字符”,还是.NET已经处理了它? - alansiqueira27
1
“起始/结束字符”是自定义的,因此.NET不实现这些功能。实际上,我们使用数据长度作为消息前缀来实现了一个系统,并且该系统正常工作。但是,您必须实现强大的套接字架构。您可以使用此代码https://dev59.com/IXNA5IYBdhLWcg3wrf43#20147995。 - Ahmet Arslan

0
在我刚写的TCP客户端/服务器中,我生成要发送到内存流的数据包,然后获取该流的长度,并在发送数据时将其用作前缀。这样,客户端就知道需要读取多少字节的数据才能获得完整的数据包。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接