我一直在努力解决这个问题,并且无法找到我的代码无法正确读取我已编写的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
的{{1}}上设置一些属性?问题是否存在于服务器中?服务器不关闭连接,由客户端执行此操作。以上也在UI线程上下文(测试程序)中运行,也许它与...有关...
在没有更多数据可用之前,有人知道如何正确使用NetworkStream
来读取数据吗?我想我想要的是像旧的Win32 winsock超时属性...... NetworkStream.Read
等等。它试图读取直到达到超时,然后返回0 ...但它有时似乎数据应该可用时返回0(或者在途中..如果可用则可以读取返回0吗?)然后在数据不可用时无限期地在最后一次读取时阻塞...
是的,我不知所措!
答案 0 :(得分:19)
网络代码的编写,测试和调试非常困难。
您经常需要考虑很多事情,例如:
您将使用什么“endian”来交换数据(Intel x86 / x64基于little-endian) - 使用big-endian的系统仍然可以读取little-endian中的数据(和反之亦然),但他们必须重新安排数据。在记录您的“协议”时,只需明确您正在使用的协议。
是否有任何“设置”已在套接字上设置,这可能会影响“流”的行为(例如SO_LINGER) - 如果您的代码非常敏感,您可能需要打开或关闭某些设置/ p>
现实世界中导致流延迟的拥塞如何影响您的读/写逻辑
如果在客户端和服务器之间交换的“消息”(在任一方向上)的大小可能不同,那么通常需要使用策略以便以可靠的方式交换“消息”(也称为协议)
以下是处理交换的几种不同方法:
将邮件大小编码在数据之前的标头中 - 这可能只是发送的前2/4/8个字节中的“数字”(取决于您的最大邮件大小),或者可能是更具异国情调的“标题”
使用特殊的“消息结束”标记(sentinel),如果真实数据可能与“标记结束”相混淆,则对真实数据进行编码/转义
使用超时....即。接收无字节的特定时间段意味着没有更多的消息数据 - 但是,这可能容易出错,超时很短,很容易在拥挤的流上点击。
在单独的“连接”上有一个“命令”和“数据”通道....这是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);
答案 1 :(得分:8)
设置底层套接字ReceiveTimeout
属性就可以了。您可以像这样访问它:yourTcpClient.Client.ReceiveTimeout
。您可以阅读docs了解更多信息。
现在代码只有在某些数据到达套接字时才会“休眠”,或者如果在读取操作开始时没有数据到达,则会引发异常超过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());
}
答案 2 :(得分: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());
}
希望这有帮助!