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请求,询问服务器上是否有新数据.这是最后的手段.
经过长时间的研究,我最终得到了一个自定义中间件解决方案.这是我的中间件类:
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最终出局时写出更好的东西.希望这个例子对某人有用.欢迎提出意见和建议.
| 归档时间: |
|
| 查看次数: |
2106 次 |
| 最近记录: |