客户端关闭套接字后,C#tcp异步侦听器在我的onreceive回调上被卡住



我有一个侦听器套接字,它可以像TCP服务器一样接受、接收和发送。我在下面给出了我的接受和接收代码,它与微软文档中的示例没有太大区别。主要区别在于,我的服务器在停止接收数据后不会终止连接(我不知道这是不是一个糟糕的设计?)。

private void on_accept(IAsyncResult xResult)
{
Socket listener = null;
Socket handler = null;
TStateObject state = null;
Task<int> consumer = null;
try
{
mxResetEvent.Set();
listener = (Socket)xResult.AsyncState;
handler = listener.EndAccept(xResult);
state = new TStateObject()
{
Socket = handler
};
consumer = async_input_consumer(state);
OnConnect?.Invoke(this, handler);
handler.BeginReceive(state.Buffer, 0, TStateObject.BufferSize, 0, new AsyncCallback(on_receive), state);
}
catch (SocketException se)
{
if (se.ErrorCode == 10054)
{
on_disconnect(state);
}
}
catch (ObjectDisposedException)
{
return;
}
catch (Exception ex)
{
System.Console.WriteLine("Exception in TCPServer::AcceptCallback, exception: " + ex.Message);
}
}
private void on_receive(IAsyncResult xResult)
{
Socket handler = null;
TStateObject state = null;
try
{
state = xResult.AsyncState as TStateObject;
handler = state.Socket;
int bytesRead = handler.EndReceive(xResult);
UInt16 id = TClientRegistry.GetIdBySocket(handler);
TContext context = TClientRegistry.GetContext(id);
if (bytesRead > 0)
{
var buffer_data = new byte[bytesRead];
Array.Copy(state.Buffer, buffer_data, bytesRead);
state.BufferBlock.Post(buffer_data);
}
Array.Clear(state.Buffer, 0, state.Buffer.Length);
handler.BeginReceive(state.Buffer, 0, TStateObject.BufferSize, 0, new AsyncCallback(on_receive), state);
}
catch (SocketException se)
{
if(se.ErrorCode == 10054)
{
on_disconnect(state);
}
}
catch (ObjectDisposedException)
{
return;
}
catch (Exception ex)
{
System.Console.WriteLine("Exception in TCPServer::ReadCallback, exception: " + ex.Message);
}
}

此代码用于连接到嵌入式设备,工作(大部分)正常。我正在调查内存泄漏,并试图通过准确复制设备的功能来加快这一过程(我们与设备的连接速度约为70kbps,经过整个周末的压力测试,内存泄漏使服务器的内存占用面积增加了一倍)。

因此,我编写了一个C#程序来复制数据事务,但我遇到了一个问题,当我断开测试程序的连接时,服务器会陷入一个循环,它会无休止地调用on_receive回调。我的印象是,在收到东西之前,BeginReceive不会被触发,它似乎会调用on_receive,像异步回调一样结束接收,处理数据,然后我希望连接等待更多数据,所以我再次调用BeginReceive

我的测试程序中出现问题的部分在这里:

private static void read_write_test()
{
mxConnection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
mxConnection.Connect("12.12.12.18", 10);
if (mxConnection.Connected)
{
byte[] data = Encoding.ASCII.GetBytes("HANDSHAKESTRING"); //Connect string
int len = data.Length;
mxConnection.Send(data);
data = new byte[4];
len = mxConnection.Receive(data);
if (len == 0 || data[0] != '1')
{
mxConnection.Disconnect(false);
return;
}
}
//Meat of the test goes here but isn't relevant
mxConnection.Shutdown(SocketShutdown.Both);
mxConnection.Close();
}

直到Shutdown(SocketShutdown.Both)调用,一切都按预期进行。然而,当我进行该调用时,服务器似乎从未收到客户端关闭套接字的通知,并陷入了无休止地尝试接收的循环中。我已经做了家庭作业,我认为根据这次讨论,我正在正确地关闭我的连接。我在断开连接部分也做了mxConnection.Disconnect(false),但同样的事情也发生了。

当设备与服务器断开连接时,我的服务器捕获到一个SocketException,错误代码为10054,文档中写道:

对等方重置连接。

已强制关闭现有连接由远程主机执行。如果上的对等应用程序远程主机突然停止,主机重新启动,主机或远程网络接口被禁用,或者远程主机使用硬盘close(有关上的SO_LINGER选项的更多信息,请参阅setsockopt远程插座)。如果连接由于保持活动检测到故障而中断,而更多的行动正在进行中。正在进行的操作失败使用WSAENETRESET。WSAECONNRESET的后续操作失败。

我已经用它来处理正在关闭的套接字,并且在大多数情况下都运行良好。然而,对于我的C#测试程序,它的工作方式似乎并不相同。

我是不是遗漏了什么?如果有任何意见,我将不胜感激。谢谢

主要区别在于,我的服务器在停止接收数据后不会终止连接(我不知道这是不是一个糟糕的设计?)。

当然是。

服务器似乎从未收到客户端关闭套接字的通知,并陷入了无休止地尝试接收的循环中

服务器确实收到通知。只是你忽略了它。通知是你的接收操作返回0。当这种情况发生时,您只需再次致电BeginReceive()即可。这将启动新的读取操作。哪个…返回0!你只是一次又一次地这样做。

当接收操作返回0时,您应该完成远程端点启动的优雅闭包(通过调用Shutdown()Close())。不要尝试再次接收。你会一直得到同样的结果。

我强烈建议你多做作业。一个好的起点是Winsock程序员的常见问题解答。它是一个相当古老的资源,根本不涉及.NET。但在大多数情况下,新手网络程序员在.NET中犯的错误与20年前新手Winsock程序员犯的错误是一样的。这份文件今天仍然和当时一样重要。

顺便说一下,您的客户端代码也有一些问题。首先,当Connect()方法成功返回时,套接字已连接。您不必检查Connected属性(事实上,永远不应该检查该属性)。其次,Disconnect()方法没有做任何有用的事情。当您想要重用底层套接字句柄,但应该在此处处理Socket对象时,就会使用它。按照常见的套接字API习惯用法,只需使用Shutdown()Close()。第三,任何从TCP套接字接收的代码都必须在循环中这样做,并利用接收到的字节计数值来确定读取了哪些数据,以及是否读取了足够的数据来做任何有用的事情。TCP可以在成功读取时返回任何正数的字节,并且程序的工作是识别发送的任何特定数据块的开始和结束。

您在EndReceive()Receive():的文档中遗漏了这一点

如果远程主机使用Shutdown方法关闭Socket连接,并且已接收到所有可用数据,则Receive方法将立即完成并返回零字节。

当您读取零字节时,您仍然会启动另一个BeginReceive(),而不是关闭:

if (bytesRead > 0)
{
var buffer_data = new byte[bytesRead];
Array.Copy(state.Buffer, buffer_data, bytesRead);
state.BufferBlock.Post(buffer_data);
}
Array.Clear(state.Buffer, 0, state.Buffer.Length);
handler.BeginReceive(state.Buffer, 0, TStateObject.BufferSize, 0, new AsyncCallback(on_receive), state);

由于您一直在"shutdown"的套接字上调用BeginReceive,因此您将不断获得接收零字节的回调。

与微软EndReceive():文档中的示例进行比较

public static void Read_Callback(IAsyncResult ar){
StateObject so = (StateObject) ar.AsyncState;
Socket s = so.workSocket;
int read = s.EndReceive(ar);
if (read > 0) {
so.sb.Append(Encoding.ASCII.GetString(so.buffer, 0, read));
s.BeginReceive(so.buffer, 0, StateObject.BUFFER_SIZE, 0, 
new AsyncCallback(Async_Send_Receive.Read_Callback), so);
}
else{
if (so.sb.Length > 1) {
//All of the data has been read, so displays it to the console
string strContent;
strContent = so.sb.ToString();
Console.WriteLine(String.Format("Read {0} byte from socket" + 
"data = {1} ", strContent.Length, strContent));
}
s.Close();
}
}