MVC核心,Web套接字和线程

Alx*_*lxg 6 c# asp.net-mvc multithreading websocket asp.net-core

我正在研究一种解决方案,它使用Web套接字协议在服务器(MVC Core Web应用程序)上发生某些事件时通知客户端(Web浏览器).我使用Microsoft.AspNetCore.WebSockets nuget.

这是我的客户端代码:

  $(function () {
    var socket = new WebSocket("ws://localhost:61019/data/openSocket");

    socket.onopen = function () {
      $(".socket-status").css("color", "green");
    }

    socket.onmessage = function (message) {
      $("body").append(document.createTextNode(message.data));
    }

    socket.onclose = function () {
      $(".socket-status").css("color", "red");
    }
  });
Run Code Online (Sandbox Code Playgroud)

加载此视图时,套接字请求会立即发送到MVC Core应用程序.这是控制器动作:

[Route("data")]
public class DataController : Controller
{
    [Route("openSocket")]
    [HttpGet]
    public ActionResult OpenSocket()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            WebSocket socket = HttpContext.WebSockets.AcceptWebSocketAsync().Result;

            if (socket != null && socket.State == WebSocketState.Open)
            {
                while (!HttpContext.RequestAborted.IsCancellationRequested)
                {
                    var response = string.Format("Hello! Time {0}", System.DateTime.Now.ToString());
                    var bytes = System.Text.Encoding.UTF8.GetBytes(response);

                    Task.Run(() => socket.SendAsync(new System.ArraySegment<byte>(bytes),
                        WebSocketMessageType.Text, true, CancellationToken.None));
                    Thread.Sleep(3000);
                }
            }
        }
        return new StatusCodeResult(101);
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码效果很好.此处的WebSocket专门用于发送,不接收任何内容.然而,问题是while循环持续保持DataController线程,直到检测到取消请求.

这里的Web套接字绑定到HttpContext对象.一旦Web请求的HttpContext被破坏,套接字连接就会立即关闭.

问题1:是否有任何方法可以在控制器线程之外保留套接字?我尝试将它放入一个单独的,它存在于主应用程序线程上运行的MVC Core Startup类中.有没有办法在主应用程序线程中保持套接字打开或再次建立连接,而不是用while循环保持控制器线程?即使控制套接字连接的控制器线程保持打开也被认为是可以的,我想不出任何好的代码放在OpenSocket的while循环中.你怎么看待在控制器中有一个手动复位事件并等待它在OpenSocket动作中的while循环中设置?

问题2:如果无法在MVC中分离HttpContext和WebSocket对象,可以使用哪些其他替代技术或开发模式来实现套接字连接重用?如果有人认为SignalR或类似的库有一些代码允许套接字独立于HttpContext,请分享一些示例代码.如果有人认为对于这个特定场景有更好的替代MVC,请提供一个示例,如果MVC没有处理独立套接字通信的能力,我不介意切换到纯ASP.NET或Web API.

问题3:要求是保持套接字连接活动或能够重新连接,直到用户明确超时或取消请求.这个想法是在服务器上发生一些独立事件,触发已建立的套接字发送数据.如果您认为除了Web套接字之外的某些技术对于这种情况(如HTML/2或流式传输)更有用,您能否描述一下您将使用的模式和框架?

PS可能的解决方案是每秒发送一次AJAX请求,询问服务器上是否有新数据.这是最后的手段.

Alx*_*lxg 6

经过长时间的研究,我最终得到了一个自定义中间件解决方案.这是我的中间件类:

        public class SocketMiddleware
    {
        private static ConcurrentDictionary<string, SocketMiddleware> _activeConnections = new ConcurrentDictionary<string, SocketMiddleware>();
        private string _packet;

        private ManualResetEvent _send = new ManualResetEvent(false);
        private ManualResetEvent _exit = new ManualResetEvent(false);
        private readonly RequestDelegate _next;

        public SocketMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public void Send(string data)
        {
            _packet = data;
            _send.Set();
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.WebSockets.IsWebSocketRequest)
            {    
                string connectionName = context.Request.Query["connectionName"]);
                if (!_activeConnections.Any(ac => ac.Key == connectionName))
                {
                    WebSocket socket = await context.WebSockets.AcceptWebSocketAsync();
                    if (socket == null || socket.State != WebSocketState.Open)
                    {
                        await _next.Invoke(context);
                        return;
                    }
                    Thread sender = new Thread(() => StartSending(socket));
                    sender.Start();

                    if (!_activeConnections.TryAdd(connectionName, this))
                    {
                        _exit.Set();
                        await _next.Invoke(context);
                        return;
                    }

                    while (true)
                    {
                        WebSocketReceiveResult result = socket.ReceiveAsync(new ArraySegment<byte>(new byte[1]), CancellationToken.None).Result;
                        if (result.CloseStatus.HasValue)
                        {
                            _exit.Set();
                            break;
                        }
                    }

                    SocketHandler dummy;
                    _activeConnections.TryRemove(key, out dummy);
                }
            }

            await _next.Invoke(context);

            string data = context.Items["Data"] as string;
            if (!string.IsNullOrEmpty(data))
            {
                string name = context.Items["ConnectionName"] as string;
                SocketMiddleware connection = _activeConnections.Where(ac => ac.Key == name)?.Single().Value;
                if (connection != null)
                {
                    connection.Send(data);
                }
            }
        }

        private void StartSending(WebSocket socket)
        {
            WaitHandle[] events = new WaitHandle[] { _send, _exit };
            while (true)
            {
                if (WaitHandle.WaitAny(events) == 1)
                {
                    break;
                }

                if (!string.IsNullOrEmpty(_packet))
                {
                    SendPacket(socket, _packet);
                }
                _send.Reset();
            }
        }

        private void SendPacket(WebSocket socket, string packet)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(packet);
            ArraySegment<byte> segment = new ArraySegment<byte>(buffer);
            Task.Run(() => socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None));
        }
    }
Run Code Online (Sandbox Code Playgroud)

这个中间件将在每个请求上运行.调用Invoke时,它会检查它是否是Web套接字请求.如果是,则中间件检查此连接是否已打开,如果不是,则接受握手,中间件将其添加到连接字典中.字典是静态的很重要,因此在应用程序生命周期中只创建一次.

现在,如果我们停在这里并向上移动管道,HttpContext最终会被破坏,并且由于套接字未正确封装,它也将被关闭.所以我们必须保持中间件线程运行.它是通过要求socket接收一些数据来完成的.

如果要求只是发送,您可能会问为什么我们需要收到任何东西?答案是它是可靠地检测客户端断开连接的唯一方法.HttpContext.RequestAborted.IsCancellationRequested只有在while循环中不断发送时才有效.如果您需要等待WaitHandle上的某个服务器事件,则取消标志永远不会成立.我试图等待HttpContext.RequestAborted.WaitHandle作为我的退出事件,但它从未设置过.所以我们要求socket接收一些东西,如果有什么东西将CloseStatus.HasValue设置为true,我们知道客户端断开了.如果我们收到其他内容(客户端代码不安全),我们将忽略它并再次开始接收.

发送在一个单独的线程中完成.原因是相同的,如果我们等待主要的中间件线程,就不可能检测到断开连接.要通知发送方线程客户端断开连接,我们使用_exit同步变量.请记住,由于SocketMiddleware实例保存在静态容器中,因此可以在此处拥有私有成员.

现在,我们如何通过此设置实际发送任何内容?假设一个事件发生在服务器上,一些数据变得可用.为简单起见,我们假设此数据到达某些控制器操作的正常http请求内.SocketMiddleware将针对每个请求运行,但由于它不是Web套接字请求,因此调用_next.Invoke(context)并且请求到达控制器操作,其可能如下所示:

[Route("ProvideData")]
[HttpGet]
public ActionResult ProvideData(string data, string connectionName)
{
    if (!string.IsNullOrEmpty(data) && !string.IsNullOrEmpty(connectionName))
    {
        HttpContext.Items.Add("ConnectionName", connectionName);
        HttpContext.Items.Add("Data", data);
    }
        return Ok();
}
Run Code Online (Sandbox Code Playgroud)

Controller填充Items集合,该集合用于在组件之间共享数据.然后管道再次返回到SocketMiddleware,我们检查context.Items中是否有任何有趣的内容.如果我们从字典中选择相应的连接并调用其设置数据字符串的Send()方法并设置_send事件并允许在发送方线程内单次运行while循环.

瞧,我们有一个套接字连接发送服务器端事件.这个例子很原始,只是为了说明这个概念.当然,要使用此中间件,您需要在添加MVC之前在Startup类中添加以下行:

app.UseWebSockets();
app.UseMiddleware<SocketMiddleware>();
Run Code Online (Sandbox Code Playgroud)

代码非常奇怪,希望我们能够在针对dotnetcore的SignalR最终出局时写出更好的东西.希望这个例子对某人有用.欢迎提出意见和建议.

  • 因此,您构建了这个辅助类,但您仍然需要 2 个线程来为每个打开的套接字保持运行。我主要担心的是,如果打开的套接字数量增加,我们可能会耗尽可用线程。您是否进行过任何研究/负载测试来证明这种情况不会发生?连接的套接字数量可能很容易达到数百个。它还会起作用吗?谢谢 (2认同)