16 September 2012

Getting .NET 4.5 WebSockets Working

I just spent most of the afternoon trying to get WebSockets working with Visual Studio 2012 and an MVC project. Most of the examples seem outdated (from old RC builds) or too low level, and the few updated ones weren't complete. So, here's my little guide on how to get started with a simple echo/chat program. It just takes 2 classes and a test page.

NOTE: WebSockets on IIS only works on Windows 8. Windows 7 does not have the necessary websocket DLL that is needed by IIS. :( That wrinkle aside, WebSockets will work with IIS 8 Regular or Express editions. This demo used IIS Express.

First, you must create a new Web API project. In this example, the project is named (with great flare and creativity) "Project1". Then, you need to add the Microsoft.WebSockets package from NuGet. (Right-click the project, click Manage NuGet Packages..., search field in upper right: microsoft.websockets, select the Microsoft.WebSockets package and click Install)
First create your WebSocket Service class, and put it somewhere in the project (mine is in /Controllers):

using Microsoft.Web.WebSockets;
using System;

namespace Project1.Controllers
{
    public class ChatClient : WebSocketHandler
    {
        public readonly Guid ConnectionId = Guid.NewGuid();
        private static WebSocketCollection chatClients =
            new WebSocketCollection();

        public override void OnOpen()
        {
            chatClients.Add(this);
            chatClients.Broadcast(
                "Client joined: " + ConnectionId.ToString()
            );
        }

        public override void OnClose()
        {
            chatClients.Broadcast(
                "Client left: " + ConnectionId.ToString()
            );
            chatClients.Remove(this);
        }

        public override void OnMessage(string message)
        {
            chatClients.Broadcast(
                ConnectionId.ToString() + " said: " + message
            );
        }
    }
}

Note I'm using Guid as a connection id, and the messages end up looking pretty ugly: 0195093f-70a5-4bfe-b707-8ac96ba94c31 said: test. But you can change that for your own needs.

The next step is to setup an ApiController. This is necessary to upgrade the HTTP request to a WebSocket request.

using Microsoft.Web.WebSockets;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;

namespace Project1.Controllers
{
    public class WebSocketController : ApiController
    {
        public HttpResponseMessage Get()
        {
            HttpContext.Current.AcceptWebSocketRequest(
                new ChatClient()
            );
            return new HttpResponseMessage(
                HttpStatusCode.SwitchingProtocols
            );
        }
    }
}

As noted in another example I found, the first using statement is VERY IMPORTANT. It adds the AcceptWebSocketRequest overload that is needed for this code. The other overloads are lower level than I wanted.

That's it! But wait you say, how can I test it? Ok, here ya go. This is a simple html page I created to test the application. It doesn't use any external files (not even jQuery). You can replace the contents of Views/Home/Index.cshtml in the project with this:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <script type="text/javascript">
        var connectButton,
            disconnectButton,
            messageInput,
            sendButton,
            responseDiv,
            uriSpan,
            uri,
            webSocket;

        var connect = function () {
            connectButton.disabled = true;
            disconnectButton.disabled = false;
            sendButton.disabled = false;
            webSocket = new WebSocket(uri);
            webSocket.onmessage = function (e) {
                responseDiv.innerHTML +=
                    '<div>' + e.data + '</div>';
            };
            webSocket.onopen = function (e) {
                responseDiv.innerHTML +=
                    '<div>Connecting...</div>';
            };
            webSocket.onclose = function (e) {
                responseDiv.innerHTML +=
                    '<div>Disconnected.</div>';
            };
            webSocket.onerror = function (e) {
                responseDiv.innerHTML += '<div>Error</div>'
            };
        };

        var disconnect = function () {
            connectButton.disabled = false;
            disconnectButton.disabled = true;
            sendButton.disabled = true;
            webSocket.close();
        };

        var sendMessage = function () {
            var message = messageInput.value;
            webSocket.send(message);
            messageInput.value = '';
        };

        var setup = function () {
            connectButton = document.getElementById('connect');
            disconnectButton =
                document.getElementById('disconnect');
            messageInput = document.getElementById('message');
            responseDiv = document.getElementById('responseLog');
            sendButton = document.getElementById('sendMessage');
            uriSpan = document.getElementById('uri');
            uri = 'ws://localhost:52618/api/websocket';
            uriSpan.innerHTML = uri;
        };
    </script>
</head>
<body onload="setup()" style="font-family: sans-serif;">
    <div>
        <div>
            <span id="uri"></span>
            <button id="connect" onclick="connect()">
                Connect
            </button>
            <button id="disconnect"
                disabled="disabled"
                onclick="disconnect()">Disconnect</button>
        </div>
        <label for="message">Message</label>
        <input id="message"/>
        <button id="sendMessage"
            onclick="sendMessage()"
            disabled="disabled">Send</button>
        <hr />
        <label for="responseLog">Response</label>
        <div id="responseLog"
            style="border: 1px solid grey;
                   width: 600px; height: 400px;
                   overflow: auto;
                   font-family: monospace;">
        </div>
    </div>
</body>
</html>

NOTE: Change the uri value to match the port your project uses. Otherwise it should work as is.

And here was my test run in Chrome 21 and IE 10.



No comments: