25

What is the preferred method for using raw websockets in an ASP.NET Web API application?

We'd like to use binary WebSockets on a couple of our interfaces of our ASP.NET Web API application. I'm having a difficult time determining how this should be done as there seems to be several conflicting and/or out-dated implementations online for .NET.

There are examples which appear to be ASP.NET like this one, but I think there must be a means to use websockets within the Web API framework. As I know you can use Signalr within WebAPI.

I thought using Microsoft.AspNet.SignalR.WebSockets.WebSocketHandler would work, but I'm not sure how to link the WebSocketHandler to the Controller...

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){}

    ...
}

Unfortunately, AcceptWebSocketRequest no longer accepts a WebSocketHandler, instead its new signature is...

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

Does anyone have a link or a quick sample implementing raw websockets in ASP.NET Web API application that is up-to-date?

Tony
  • 1,986
  • 2
  • 25
  • 36

5 Answers5

16

UPDATE: After a bit more research by myself and a coworker, we came to the conclusion that the WebSocketHandler class does not appear to be intended to be used outside of the internal processes of SignalR. As there is no obvious means to leverage WebSocketHandler isolated from SignalR. This is unfortunate as I find its interfaces slightly more high-level than the System.Web/System.Net interfaces. Moreover, the method described below makes use of HttpContext which I believe should be avoided.

As such we plan to take an approach similar to the one shown by Mrchief, but with a bit more Web API flavor. Like this...(NOTE: our socket is write-only, but I discovered you MUST perform read operations of you want WebSocket.State to get updated properly.

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);
            }
        }
    }
}

NOTE: Obviously error checking and proper usage of a CancellationToken is left as an exercise for the reader.

Tony
  • 1,986
  • 2
  • 25
  • 36
  • This works, but I learned a few things from testing that I'll update in my answer. Namely, you must call ReadAsync if you want the ws.State to get updated! – Tony Sep 05 '14 at 11:47
  • 1
    Thanks for this code. There are some compile errors though -- the `Get` method needs a return after the `if` block, and the `await` inside the lambda requires the `async` modifier on the lambda. – Drew Noakes Mar 28 '15 at 14:24
  • Thanks for the code, this gave me a great boost. However, using `Task.Run` with async lambda may not be the greatest idea (e.g. you get a compiler warning). I ended up creating `ReadTest(WebSocket)` and `WriteTask(WebSocket)` and running them in parallel using `await Task.WhenAll(ReadTask(ws), WriteTask(ws))` – Ivan Krivyakov Aug 27 '17 at 02:27
14

This is an older question, but I would like to add another answer to this.
It turns out, you CAN use it, and I have no clue why they made it so "hidden". Would be nice if someone could explain to me what's wrong with this class, or if what I'm doing here is somehow "forbidden" or "bad design".

If we look in the Microsoft.Web.WebSockets.WebSocketHandler, we find this public method:

[EditorBrowsable(EditorBrowsableState.Never)]
public Task ProcessWebSocketRequestAsync(AspNetWebSocketContext webSocketContext);

This method is hidden from intellisense, but it's there, and can be called without compilation errors.
We can use this method to get the task that we need to return in the AcceptWebSocketRequest method. Check this out:

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);
    }
}

And then in my API controller:

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;
    }
}

This works completely fine. OnMessage gets triggered, and echoes back to my JavaScript instantiated WebSocket...

René Sackers
  • 2,395
  • 4
  • 24
  • 43
  • 1
    I see nothing bad with this design. And that `EditorBrowsable` attribute to hide a public interface is just directly crazy. Why it even exists, one can only wonder. – Mathias Lykkegaard Lorenzen Jan 17 '16 at 16:09
  • +1 - René, would you happen to have a POC of this technique lying around somewhere? I want to use it, but I'm unsure on how to initialize. I imagine you specify a RoutePrefix for your ApiController, am I right? – OnoSendai Apr 14 '16 at 15:03
  • @lbotinelly, I suggest you try some google searches along the lines of "ASP.NET 4.5/5 WebSockets", you'll find some interesting articles like this one, which has a very detailed video, including the use of the WebSocketHandler: http://weblogs.asp.net/dwahlin/building-an-html5-web-sockets-server-with-asp-net-4-5 :) – René Sackers Apr 15 '16 at 16:40
  • How would the request flow be from the client be for sending a message in this case? – Þorvaldur Rúnarsson Oct 11 '16 at 16:36
  • @ÞorvaldurRúnarsson I don't believe I fully understand your question. I simply created a web socket client in JavaScript as demonstrated by numerous examples online ;) – René Sackers Oct 12 '16 at 13:53
  • So it's just enough to change the https to ws and you should be able to connect? – Þorvaldur Rúnarsson Oct 12 '16 at 22:01
  • and use that url to feed the WebSocket client APIs – Þorvaldur Rúnarsson Oct 12 '16 at 22:10
  • 1
    @ÞorvaldurRúnarsson Yup. Don't forget, if you're working with HTTPS, it needs to be WSS://, not WS://. Just try, there's loads of examples online ;) – René Sackers Oct 13 '16 at 08:04
  • 2
    This example is using the Microsoft.Web.WebSockets, which if you look at the nuget package Microsoft.WebSockets (I know it's confusing) it is no longer being developed and they recommend using SignalR. Which leads back to the original question of how to use SignalR.WebSocketHandler in the example above. Which based on my research can't be done easily. – 165plo Mar 09 '17 at 21:41
  • The best lightweight implementation. Always choose this over signalr – Zoli Jan 07 '21 at 16:23
4

I found this example:

Sample code (reproduced from the post):

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;
            }
        }
    }
}
Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • I saw that but it doesn't leverage the Web API interfaces? Is that compatible with WebAPI? – Tony Sep 04 '14 at 14:50
  • Which interfaces are you looking for exactly? Web API is built on top of ASP.Net and this should work (you may have to tweak things a bit though). – Mrchief Sep 04 '14 at 14:56
  • The second example doesn't work as AcceptWebSocketRequest no longer accepts a WebSocketHandler as I mentioned above. I added a note or two to my original question to help clarify my confusion. – Tony Sep 04 '14 at 15:00
  • Right, my bad. Updated my answer to delete that section. – Mrchief Sep 04 '14 at 15:04
2

Sharing my code based on Tony's answer with cleaner task handling. This code sends out current UTC time approximately every second:

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
        }
    }
}
Ivan Krivyakov
  • 1,898
  • 1
  • 17
  • 27
  • How can we modify the WriteTask method to send out the updated data every time there is a new transaction in DB. – Osama Aftab Sep 26 '17 at 16:35
  • @Osama: there are several ways to do it. You can generate an enumerable of Tasks, each based on a TaskCompletionSource. Mark TaskCompletionSoruce as complete when an event of interest happens. I used this approach in https://www.nuget.org/packages/TaskTimer/. Alternatively, you can use SemaphoreSlim.WaitAsync method to signal that something happened. Yet another approach is to use https://github.com/StephenCleary/AsyncEx/wiki/AsyncAutoResetEvent from the AsyncEx package. – Ivan Krivyakov Sep 28 '17 at 02:44
0

What about using SignalR 2?

  • Can be installed via NuGet
  • For .NET 4.5+
  • No permanent loop required
  • Broadcasting possible -Tutorial here
Anytoe
  • 1,605
  • 1
  • 20
  • 26
  • If SignalR2 supports binary websockets that might work. v1 did not, moreover, it wanted to Serialize/Deserialize packets into local objects which tends to require additional bandwidth. I needed to make my transport as tight as possible (I was streaming live audio data), so raw binary websockets was the best approach. – Tony Aug 15 '16 at 15:26
  • 7
    And there is still a dependency on jquery which is the only reason I'm not using it. – Asken Jan 15 '17 at 07:52