有朋友来找我的一个问题:在连接的服务器端使用NetworkStream类时,如果客户端断开连接,NetworkStream检测不到。
精简后,他的 C# 代码如下所示:
List<TcpClient> connections = new List<TcpClient>();
TcpListener listener = new TcpListener(7777);
listener.Start();
while(true)
{
if (listener.Pending())
{
connections.Add(listener.AcceptTcpClient());
}
TcpClient deadClient = null;
foreach (TcpClient client in connections)
{
if (!client.Connected)
{
deadClient = client;
break;
}
NetworkStream ns = client.GetStream();
if (ns.DataAvailable)
{
BinaryFormatter bf = new BinaryFormatter();
object o = bf.Deserialize(ns);
ReceiveMyObject(o);
}
}
if (deadClient != null)
{
deadClient.Close();
connections.Remove(deadClient);
}
Thread.Sleep(0);
}
该代码有效,因为客户端可以成功连接并且服务器可以读取发送给它的数据。但是,如果远程客户端调用 tcpClient.Close(),服务器不会检测到断开连接 - client.Connected 保持为真,而 ns.DataAvailable 为假。
对 Stack Overflow 的搜索提供了一个答案——因为没有调用 Socket.Receive,所以套接字没有检测到断开连接。很公平。我们可以解决这个问题:
foreach (TcpClient client in connections)
{
client.ReceiveTimeout = 0;
if (client.Client.Poll(0, SelectMode.SelectRead))
{
int bytesPeeked = 0;
byte[] buffer = new byte[1];
bytesPeeked = client.Client.Receive(buffer, SocketFlags.Peek);
if (bytesPeeked == 0)
{
deadClient = client;
break;
}
else
{
NetworkStream ns = client.GetStream();
if (ns.DataAvailable)
{
BinaryFormatter bf = new BinaryFormatter();
object o = bf.Deserialize(ns);
ReceiveMyObject(o);
}
}
}
}
(为简洁起见,我省略了异常处理代码。)
此代码有效,但是,我不会将此解决方案称为“优雅”。我知道的问题的另一个优雅解决方案是为每个 TcpClient 生成一个线程,并允许 BinaryFormatter.Deserialize (née NetworkStream.Read) 调用阻塞,这将正确检测断开连接。但是,这确实会产生为每个客户端创建和维护线程的开销。
我觉得我错过了一些秘密的、很棒的答案,它会保留原始代码的清晰度,但避免使用额外的线程来执行异步读取。虽然,也许 NetworkStream 类从来没有为这种用途而设计。任何人都可以解释一下吗?
更新:只是想澄清一下,我有兴趣看看 .NET 框架是否有一个解决方案,涵盖了 NetworkStream 的这种使用(即轮询和避免阻塞)——显然可以做到;NetworkStream 可以很容易地包装在提供该功能的支持类中。看起来很奇怪,该框架本质上要求您使用线程来避免阻塞 NetworkStream.Read,或者窥视套接字本身以检查断开连接 - 几乎就像它是一个错误。或者可能缺少某个功能。;)