当客户端断开连接而不取消连接时,Websocket.ReceiveAsync 导致崩溃

Posted

技术标签:

【中文标题】当客户端断开连接而不取消连接时,Websocket.ReceiveAsync 导致崩溃【英文标题】:Websocket.ReceiveAsync causing a crash when a client disconnects without cancelling connection 【发布时间】:2020-04-07 09:37:34 【问题描述】:

DotNet Core 3.1(运行 Kestral 或 IIS)

我有以下循环,在每个连接的客户端的任务中运行

 using (var ms = new MemoryStream())
                
                        do

                            webSocketReceiveResult = await socket.ReceiveAsync(buffer, CancellationToken.None);
                            ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count);

                        
                        while (!webSocketReceiveResult.EndOfMessage);

当客户端突然退出,网络故障,崩溃或其他任何事情并且没有机会关闭其连接时,它会使整个类崩溃,并且所有正在运行的任务。网络服务器仍在运行并将接受所有新连接,但所有现有连接都将终止。

错误是:'远程方在未完成关闭握手的情况下关闭了 WebSocket 连接。' 这是预期的,但我无法尝试捕获它以保留其余连接?

这里的完整方法:

    private static async Task SocketProcessingLoopAsync(ConnectedClient client)
    
        _ = Task.Run(() => client.BroadcastLoopAsync().ConfigureAwait(false));

        var socket = client.Socket;
        var loopToken = SocketLoopTokenSource.Token;
        var broadcastTokenSource = client.BroadcastLoopTokenSource; // store a copy for use in finally block
        string sessionName = "";
        string command = "";
        int commandCounter = 0;
        WebSocketReceiveResult webSocketReceiveResult = null;
        try
        



            var buffer = WebSocket.CreateServerBuffer(4096);

            while (socket.State != WebSocketState.Closed && socket.State != WebSocketState.Aborted && !loopToken.IsCancellationRequested)
            

                // collect all the bytes incoming

                using (var ms = new MemoryStream())
                
                        do
                        
                            webSocketReceiveResult = await socket.ReceiveAsync(buffer, CancellationToken.None);
                            ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count);

                        
                        while (!webSocketReceiveResult.EndOfMessage);

                    //var receiveResult = await client.Socket.ReceiveAsync(buffer, loopToken);

                    // if the token is cancelled while ReceiveAsync is blocking, the socket state changes to aborted and it can't be used
                    if (!loopToken.IsCancellationRequested)
                    
                        // the client is notifying us that the connection will close; send acknowledgement
                        if (client.Socket.State == WebSocketState.CloseReceived && webSocketReceiveResult.MessageType == WebSocketMessageType.Close)
                        
                            Console.WriteLine($"Socket client.SocketId: Acknowledging Close frame received from client");
                            broadcastTokenSource.Cancel();
                            await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None);
                            // the socket state changes to closed at this point
                        


                        if (client.Socket.State == WebSocketState.Open)
                        
                            if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text)
                            
                                // here we receive text instructioons from the clients

                                using (StreamReader reader = new StreamReader(ms, Encoding.UTF8))
                                
                                    ms.Seek(0, SeekOrigin.Begin);
                                    command = reader.ReadToEnd().ToLower();
                                

                                //   var command = Encoding.UTF8.GetString(buffer.Array, 0, webSocketReceiveResult.Count).ToLower();
                                if (command.Contains("session:"))
                                
                                    // assign the client to a session, and inform them of their position in it
                                    sessionName = command.Replace("session:", "");
                                    if (!ClientControl.Sessions.ContainsKey(sessionName))
                                    
                                        ClientControl.Sessions.TryAdd(sessionName, new BlockingCollection<ConnectedClient>());
                                    
                                    ClientControl.Sessions[sessionName].Add(client);
                                    // broadcast the collection count
                                    Broadcast(ClientControl.Sessions[sessionName].Count.ToString(), client, true, sessionName);
                                    Broadcast("Number of clients " + ClientControl.Sessions[sessionName].Count.ToString(), client, false, sessionName);
                                    Broadcast("Number of clients " + ClientControl.Sessions[sessionName].Count.ToString(), client, true, sessionName);
                                
                                else if (command.Contains("status:"))
                                
                                    string output = "<br/><h1>Sessions:</h1><br/><br/>";
                                    foreach (var session in ClientControl.Sessions)
                                    
                                        output += session.Key + " Connected Clients: " + session.Value.Count.ToString() + "<br/>";

                                    

                                    Broadcast(output, client, true, "");
                                
                                else if (command.Contains("ping"))
                                
                                    Console.WriteLine(command + " " + DateTime.Now.ToString() + " " + commandCounter);
                                

                            
                            else
                            
                                // we just mirror what is sent out to the connected clients, depending on the session

                                Broadcast(ms.ToArray(), client, sessionName);
                                commandCounter++;

                            

                        

                    

                // end memory stream 

            
        
        catch (OperationCanceledException)
        
            // normal upon task/token cancellation, disregard
        
        catch (Exception ex)
        
            Console.WriteLine($"Socket client.SocketId:");
            Program.ReportException(ex);
        
        finally
        
            broadcastTokenSource.Cancel();

            Console.WriteLine($"Socket client.SocketId: Ended processing loop in state socket.State");

            // don't leave the socket in any potentially connected state
            if (client.Socket.State != WebSocketState.Closed)
                client.Socket.Abort();

            // by this point the socket is closed or aborted, the ConnectedClient object is useless
            if (ClientControl.Sessions[sessionName].TryTake(out client))
                socket.Dispose();

            // signal to the middleware pipeline that this task has completed
            client.TaskCompletion.SetResult(true);
        
    

【问题讨论】:

您可以将此作为示例进行检查:github.com/georgekosmidis/AspNetCore.WebSockets 【参考方案1】:

好的,无法让它工作,我已经将循环重写为如下所示,当客户端挂起时它干净地退出

  do
                    
                        cancellationTokenSource = new CancellationTokenSource(10000);

                        task = socket.ReceiveAsync(buffer, loopToken);
                        while (!task.IsCompleted && !SocketLoopTokenSource.IsCancellationRequested)
                        
                            await Task.Delay(2).ConfigureAwait(false);
                        
                        if (socket.State != WebSocketState.Open || task.Status != TaskStatus.RanToCompletion)
                        
                            if (socket.State == WebSocketState.CloseReceived)
                            
                                Console.WriteLine($"Socket client.SocketId: Acknowledging Close frame received from client");
                                broadcastTokenSource.Cancel();
                                await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None);
                                ClientControl.Sessions[sessionName].TryTake(out client);
                            

                            Console.WriteLine("Client Left " + client.SocketId);
                            break;
                        
                        ms.Write(buffer.Array, buffer.Offset, task.Result.Count);

                    
                    while (!task.Result.EndOfMessage);

【讨论】:

以上是关于当客户端断开连接而不取消连接时,Websocket.ReceiveAsync 导致崩溃的主要内容,如果未能解决你的问题,请参考以下文章

当客户端更改 IP 地址或断开连接时,如何捕捉 websocket-close 事件?

websocket客户端可能会断开连接以一次接收多个包?

如何在WebSocket的服务器侧检测客户端的断开连接

websocket客户端在发送大数据时断开连接

Websocket PHP:从客户端发送到服务器的立即断开连接和奇怪的消息字符串

nodejs websocket检测断开连接的套接字