如何优雅地关闭.Net中的双向WebSocket

f5emj3cl  于 2023-08-05  发布在  .NET
关注(0)|答案(3)|浏览(187)

我有一个WebSocket服务器,它接受来自客户端的二进制数据流,并在每读取4MB时用另一个文本数据流进行响应。服务器使用IIS 8和asp.net web api

服务器

public class WebSocketController : ApiController
{
    public HttpResponseMessage Get()
    {
        if (!HttpContext.Current.IsWebSocketRequest)
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        }

        HttpContext.Current.AcceptWebSocketRequest(async (context) =>
        {
            try
            {
                WebSocket socket = context.WebSocket;

                byte[] requestBuffer = new byte[4194304];
                int offset = 0;

                while (socket.State == WebSocketState.Open)
                {
                    var requestSegment = new ArraySegment<byte>(requestBuffer, offset, requestBuffer.Length - offset);
                    WebSocketReceiveResult result = await socket.ReceiveAsync(requestSegment, CancellationToken.None);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        // Send one last response before closing
                        var response = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Server got " + offset + " bytes\n"));
                        await socket.SendAsync(response, WebSocketMessageType.Text, true, CancellationToken.None);

                        // Close
                        await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                        break;
                    }

                    offset += result.Count;
                    if (offset == requestBuffer.Length)
                    {
                        // Regular response
                        var response = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Server got 4194304 bytes\n"));
                        await socket.SendAsync(response, WebSocketMessageType.Text, true, CancellationToken.None);
                        offset = 0;
                    }
                }
            }
            catch (Exception ex)
            {
                // Log and continue
            }
        });

        return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
    }
}

字符串
C#客户端使用ClientWebSocket类连接到服务器并发送请求。它创建了一个任务,用于接收来自服务器的响应,该任务与请求发送并行运行。发送完请求后,它在套接字上调用CloseAsync,然后等待Receive任务完成。

客户

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WebSocketClient
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                CallWebSocketServer().Wait();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static async Task CallWebSocketServer()
        {
            using (ClientWebSocket socket = new ClientWebSocket())
            {
                await socket.ConnectAsync(new Uri("ws://localhost/RestWebController"), CancellationToken.None);
                byte[] buffer = new byte[128 * 1024];

                Task receiveTask = Receive(socket);
                for (int i = 0; i < 1024; ++i)
                {
                    await socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Binary, true, CancellationToken.None);
                }
               
                await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                receiveTask.Wait();

                Console.WriteLine("All done");
            }
        }

        static async Task Receive(ClientWebSocket socket)
        {
            try
            {
                byte[] recvBuffer = new byte[64 * 1024];
                while (socket.State == WebSocketState.Open)
                {
                    var result = await socket.ReceiveAsync(new ArraySegment<byte>(recvBuffer), CancellationToken.None);
                    Console.WriteLine("Client got {0} bytes", result.Count);
                    Console.WriteLine(Encoding.UTF8.GetString(recvBuffer, 0, result.Count));
                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        Console.WriteLine("Close loop complete");
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception in receive - {0}", ex.Message);
            }
        }
    }
}


问题是客户端在CloseAsync调用时阻塞。
在这种情况下,正常关闭WebSocket的正确方法是什么?

cfh9epnr

cfh9epnr1#

想明白了。

服务器

基本上,我必须调用ClientWebSocket.CloseOutputAsync(而不是CloseAsync)方法来告诉框架不再从客户端发送输出。

await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);

字符串

客户

然后在Receive函数中,我必须允许套接字状态WebSocketState.CloseSent从服务器接收Close响应

static async Task Receive(ClientWebSocket socket)
{
    try
    {
        byte[] recvBuffer = new byte[64 * 1024];
        while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent)
        {
            var result = await socket.ReceiveAsync(new ArraySegment<byte>(recvBuffer), CancellationToken.None);
            Console.WriteLine("Client got {0} bytes", result.Count);
            Console.WriteLine(Encoding.UTF8.GetString(recvBuffer, 0, result.Count));
            if (result.MessageType == WebSocketMessageType.Close)
            {
                Console.WriteLine("Close loop complete");
                break;
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Exception in receive - {0}", ex.Message);
    }
}

huwehgph

huwehgph2#

我使用以下代码的各种组合做了一些研究。该场景是客户端发送数据,直到服务器关闭WebSocket。

服务器

public class HomeController : Controller
{
    public async Task Index()
    {
        using (WebSocket webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync())
        {
            await Task.Delay(3000);
            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "ServerClose", new CancellationTokenSource(20_000).Token);
        }
    }
}

字符串

客户

static async Task SendDataToWebSocket(string webSocketUrl)
{
    ClientWebSocket webSocket = new ClientWebSocket();
    try
    {
        await webSocket.ConnectAsync(new Uri(webSocketUrl), new CancellationTokenSource(5000).Token);
        ProcessCloseEvent(webSocket);

        while (true)
        {
            ArraySegment<byte> data = new ArraySegment<byte>(Encoding.UTF8.GetBytes("something"));
            await webSocket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None);
            await Task.Delay(1000);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Error: " + e.Message);
    }
}

private static async void ProcessCloseEvent(ClientWebSocket webSocket)
{
    ArraySegment<byte> data = new ArraySegment<byte>(new byte[1000]);
    var result = await webSocket.ReceiveAsync(data, CancellationToken.None);
    Console.WriteLine("Count: {0}, CloseStatus: {1}, Data: {2}", result.Count, result.CloseStatus, Encoding.UTF8.GetString(data));
    await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "ClientClose", CancellationToken.None);
}


我观察到以下情况:

  • CloseAsync成对工作,这意味着第一端调用CloseAsync并等待,直到另一端也调用它。
  • 由于CloseAsyncCancelationToken.None将无限期地等待,人们更喜欢使用CloseOutputAsync,似乎不需要等待对方的回应。
  • 尽管在ReceiveAsync调用返回的结果中可以观察到WebSocket关闭事件,但WebSocket.StateWebSocket.CloseStatus仅在ReceiveAsync之后更新。
  • CloseAsyncCloseOutputAsync可以以任意组合配对,以响应WebSocket关闭事件。由于在关闭WebSocket时不需要等待响应,也不需要知道另一端是否处理了close事件,因此CloseOutputAsync应该更好地使用。
  • 一旦客户端使用CloseAsync响应服务器的请求,WebSocket.State将更改为Closed,随后的SendAsyncReceiveAsync将抛出异常。
  • 如果客户端没有对WebSocket关闭事件做出React,而服务器处理了WebSocket,则客户端的SendAsync会抛出IOException,并显示以下消息:“无法将数据写入传输连接:已建立的连接已被主机中的软件中止。”
  • 如果客户端没有对WebSocket关闭事件做出React,并且服务器处理了WebSocket,则客户端的ReceiveAsync将抛出一个WebSocketException消息:“远程方在未完成关闭握手的情况下关闭了WebSocket连接。”
  • 在服务器处理完WebSocket后,客户端就可以毫无例外地发送/接收一些消息,因此在检查关闭和交换数据之间应该没有并发问题。
  • 由于CloseAsync等待另一端,因此可以使用它来确定是否已成功接收所有发送的数据。如果双方同时发送和关闭,则不适用。
    **注意:**我使用.NET 6进行测试。上述观察可能不适用于以其他语言编写的客户端/服务器。
hrirmatl

hrirmatl3#

我建议你看看这些链接:
异步服务器:https://msdn.microsoft.com/en-us/library/fx6588te%28v=vs.110%29.aspx
异步客户端:https://msdn.microsoft.com/en-us/library/bew39x2a(v=vs.110).aspx
最近我实现了类似的东西与这些链接作为一个例子.方法“BeginReceive”(用于服务器)和“BeginConnect”(用于客户端)启动每个新线程。这样就不会有任何东西

相关问题