使用WebSockets与ASP.NET Web API

26
什么是在ASP.NET Web API应用程序中使用原始WebSockets的首选方法?
我们想要在我们的ASP.NET Web API应用程序的一些接口上使用二进制WebSockets。我很难确定应该如何做到这一点,因为似乎有几个冲突和/或过时的.NET实现在线上。
有一些看起来像ASP.NET的示例,比如这个,但我认为必须有一种方法可以在Web API框架内使用websockets,就像我们知道您可以在WebAPI中使用Signalr一样。
我本以为可以使用Microsoft.AspNet.SignalR.WebSockets.WebSocketHandler,但我不确定如何将WebSocketHandler链接到控制器...
class MyServiceController : ApiController
{
    [HttpGet]
    public HttpResponseMessage SwitchProtocols (string param)
    {
        HttpContext currentContext = HttpContext.Current;
        if (currentContext.IsWebSocketRequest || 
            currentContext.IsWebSocketRequestUpgrading)
        {
            // several out-dated(?) examples would 
            // use 'new MySocketHandler' for ???
            var unknown = new ????
            currentContext.AcceptWebSocketRequest(unknown); 
            return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
        }   
    }
}

class MySocketHandler : WebSocketHandler
{
    public MySocketHandler(): base(2048){}

    ...
}

不幸的是,AcceptWebSocketRequest现在不再接受WebSocketHandler作为参数,而是有一个新的签名...

public void AcceptWebSocketRequest(Func<AspNetWebSocketContext, Task> userFunc)

有没有人有一个链接或一个快速示例,可以在ASP.NET Web API应用程序中实现原始Websockets,并且是最新的?

5个回答

17

更新: 经过我和同事的进一步研究,我们得出结论,WebSocketHandler类似乎不打算在SignalR之外的内部进程中使用。由于没有明显的方法可以从SignalR中隔离出WebSocketHandler,这是令人遗憾的。尽管我发现它的接口略微比System.Web/System.Net接口更高级。此外,下面描述的方法使用了HttpContext,我认为应该避免。

因此,我们计划采取与Mrchief所展示的类似方法,但加入了一点Web API的特色。像这样...(注意:我们的socket只能写入,但我发现如果需要更新WebSocket.State,则必须执行读取操作)。

class MyServiceController : ApiController
{
    public HttpResponseMessage Get (string param)
    {
        HttpContext currentContext = HttpContext.Current;
        if (currentContext.IsWebSocketRequest || 
            currentContext.IsWebSocketRequestUpgrading)
        {
            currentContext.AcceptWebSocketRequest(ProcessWebsocketSession); 
            return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
        }   
    }

    private async Task ProcessWebsocketSession(AspNetWebSocketContext context)
    {
        var ws = context.WebSocket;

        new Task(() =>
        {
            var inputSegment = new ArraySegment<byte>(new byte[1024]);

            while (true)
            {
                // MUST read if we want the state to get updated...
                var result = await ws.ReceiveAsync(inputSegment, CancellationToken.None);

                if (ws.State != WebSocketState.Open)
                {
                    break;
                }
            }
        }).Start();

        while (true)
        {
            if (ws.State != WebSocketState.Open)
            {
                break;
            }
            else
            {
                byte[] binaryData = { 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe };
                var segment = new ArraySegment<byte>(binaryData);
                await ws.SendAsync(segment, WebSocketMessageType.Binary, 
                    true, CancellationToken.None);
            }
        }
    }
}

注意: 显然错误检查和正确使用 CancellationToken 的任务留给读者自己完成。


这个可以运行,但是我从测试中学到了一些东西,我会在我的答案中进行更新。也就是说,如果你想要更新ws.State,你必须调用ReadAsync! - Tony
1
谢谢您提供这段代码。然而,有一些编译错误-- Get 方法需要在 if 块后面加上返回语句,并且 lambda 中的 await 需要在 lambda 上加上 async 修饰符。 - Drew Noakes
感谢您提供的代码,这让我受益匪浅。然而,使用带有异步 Lambda 的 Task.Run 可能不是最好的选择(例如,您会收到编译器警告)。我最终创建了 ReadTest(WebSocket)WriteTask(WebSocket),并使用 await Task.WhenAll(ReadTask(ws), WriteTask(ws)) 并行运行它们。 - Ivan Krivyakov

14
这是一个旧问题,但我想在这里再添加一个答案。事实证明,您可以使用它,而我不知道为什么它们使它如此“隐藏”。如果有人能解释一下这个类的问题,或者我在这里做的是否“禁止”或“糟糕”的设计,那将是很好的。如果我们查看Microsoft.Web.WebSockets.WebSocketHandler,我们会发现这个公共方法:
[EditorBrowsable(EditorBrowsableState.Never)]
public Task ProcessWebSocketRequestAsync(AspNetWebSocketContext webSocketContext);

这个方法在 IntelliSense 中是隐藏的,但它确实存在,并且可以在不编译出错的情况下被调用。
我们可以使用这个方法来获取需要在 AcceptWebSocketRequest 方法中返回的任务。看看这个:

public class MyWebSocketHandler : WebSocketHandler
{
    private static WebSocketCollection clients = new WebSocketCollection();

    public override void OnOpen()
    {
        clients.Add(this);
    }

    public override void OnMessage(string message)
    {
        Send("Echo: " + message);
    }
}

然后在我的API控制器中:

public class MessagingController : ApiController
{
    public HttpResponseMessage Get()
    {
        var currentContext = HttpContext.Current;
        if (currentContext.IsWebSocketRequest ||
            currentContext.IsWebSocketRequestUpgrading)
        {
            currentContext.AcceptWebSocketRequest(ProcessWebsocketSession);
        }

        return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
    }

    private Task ProcessWebsocketSession(AspNetWebSocketContext context)
    {
        var handler = new MyWebSocketHandler();
        var processTask = handler.ProcessWebSocketRequestAsync(context);
        return processTask;
    }
}

这个完全正常运作。OnMessage被触发,然后回显到我的JavaScript实例化WebSocket...


1
我认为这个设计没有任何问题。使用EditorBrowsable属性来隐藏公共接口简直是疯狂的。我们只能想象为什么它会存在。 - Mathias Lykkegaard Lorenzen
在这种情况下,客户端发送消息的请求流程会是怎样的? - Þorvaldur Rúnarsson
@ÞorvaldurRúnarsson,我不认为我完全理解你的问题。我只是按照在线示例创建了一个JavaScript Web Socket客户端 ;) - René Sackers
1
@ÞorvaldurRúnarsson 是的。别忘了,如果你正在使用HTTPS,它需要是WSS://而不是WS://。只要试一试,在网上有很多例子;) - René Sackers
2
这个例子使用了Microsoft.Web.WebSockets,如果你看一下nuget包Microsoft.WebSockets(我知道这很令人困惑),它已经不再被开发,并且他们建议使用SignalR。这又回到了最初的问题,如何在上面的例子中使用SignalR.WebSocketHandler。根据我的研究,这并不容易实现。 - 165plo
显示剩余5条评论

4
我找到了这个示例

示例代码(源自文章):

public class WSHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(ProcessWSChat);
        }
    }

    public bool IsReusable { get { return false; } }

    private async Task ProcessWSChat(AspNetWebSocketContext context)
    {
        WebSocket socket = context.WebSocket;
        while (true)
        {
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);
            WebSocketReceiveResult result = await socket.ReceiveAsync(
                buffer, CancellationToken.None);
            if (socket.State == WebSocketState.Open)
            {
                string userMessage = Encoding.UTF8.GetString(
                    buffer.Array, 0, result.Count);
                userMessage = "You sent: " + userMessage + " at " + 
                    DateTime.Now.ToLongTimeString();
                buffer = new ArraySegment<byte>(
                    Encoding.UTF8.GetBytes(userMessage));
                await socket.SendAsync(
                    buffer, WebSocketMessageType.Text, true, CancellationToken.None);
            }
            else
            {
                break;
            }
        }
    }
}

我看到了,但它没有利用Web API接口? 它与WebAPI兼容吗? - Tony
你具体需要哪些接口?Web API是建立在ASP.Net之上的,这应该可以工作(但你可能需要稍微调整一下)。 - Mrchief
第二个例子不起作用,因为AcceptWebSocketRequest不再像我上面提到的那样接受WebSocketHandler。我在原来的问题中添加了一两个注释,以帮助澄清我的困惑。 - Tony
好的,我的错。我更新了我的答案来删除那部分。 - Mrchief

2

以下是基于Tony回答的代码,使用更清晰的任务处理方式。此代码每秒钟发送当前的UTC时间:

public class WsTimeController : ApiController
{
    [HttpGet]
    public HttpResponseMessage GetMessage()
    {
        var status = HttpStatusCode.BadRequest;
        var context = HttpContext.Current;
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(ProcessRequest);
            status = HttpStatusCode.SwitchingProtocols;

        }

        return new HttpResponseMessage(status);
    }

    private async Task ProcessRequest(AspNetWebSocketContext context)
    {
        var ws = context.WebSocket;
        await Task.WhenAll(WriteTask(ws), ReadTask(ws));
    }

    // MUST read if we want the socket state to be updated
    private async Task ReadTask(WebSocket ws)
    {
        var buffer = new ArraySegment<byte>(new byte[1024]);
        while (true)
        {
            await ws.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false);
            if (ws.State != WebSocketState.Open) break;
        }
    }

    private async Task WriteTask(WebSocket ws)
    {
        while (true)
        {
            var timeStr = DateTime.UtcNow.ToString("MMM dd yyyy HH:mm:ss.fff UTC", CultureInfo.InvariantCulture);
            var buffer = Encoding.UTF8.GetBytes(timeStr);
            if (ws.State != WebSocketState.Open) break;
            var sendTask = ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
            await sendTask.ConfigureAwait(false);
            if (ws.State != WebSocketState.Open) break;
            await Task.Delay(1000).ConfigureAwait(false); // this is NOT ideal
        }
    }
}

我们如何修改WriteTask方法,以便在数据库中有新事务时每次发送更新的数据。 - Osama Aftab
@Osama:有几种方法可以实现。您可以生成一个基于TaskCompletionSource的任务可枚举,每个任务都是基于事件完成的。当感兴趣的事件发生时,将TaskCompletionSoruce标记为已完成。我在https://www.nuget.org/packages/TaskTimer/中使用了这种方法。或者,您可以使用SemaphoreSlim.WaitAsync方法来发出信号表示发生了某些事情。另一种方法是使用AsyncEx包中的https://github.com/StephenCleary/AsyncEx/wiki/AsyncAutoResetEvent。 - Ivan Krivyakov

0

使用SignalR 2怎么样?

  • 可以通过NuGet安装
  • 适用于.NET 4.5+
  • 不需要永久循环
  • 可以进行广播 -这里有教程

如果SignalR2支持二进制WebSockets,那可能行得通。相比之下,v1版本不支持二进制WebSockets,而且它需要将数据包序列化/反序列化为本地对象,这往往需要额外的带宽。我需要让我的传输尽可能紧凑(因为我正在传输实时音频数据),所以原始的二进制WebSockets是最好的方法。 - Tony
7
仍然依赖jQuery是我不使用它的唯一原因。 - Asken

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接