Free Trial

Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.

  • Create BookmarkCreate Bookmark
  • Create Note or TagCreate Note or Tag
  • PrintPrint

Scaling with Web Sockets

Web Sockets has been getting a lot of press recently. This highly publicized feature is part of the in-progress HTML5 specification. Though the spec is still in-progress, this hasn’t stopped many of the major players from building HTML5 features into their browsers. The feature known as Web Sockets is no exception.

Web Sockets provides a mechanism for two-way communication between the client and the server. This makes your site faster and more scalable by reducing the number of requests and shrinking the payload size of any sent packets. Additionally, the full-duplex communication channel that a web socket provides allows the information on a page to update in near-real time. You can find more information about the benefits of Web Sockets here.

Note

While Web Sockets can certainly improve the performance and scalability of your site, you should use it responsibly. More information can be found here.

You may be wondering what areas of your solution or types of solution would best benefit from the use of Web Sockets. The typical response would be “anything that needs to be updated in real time,” but that seems a bit too generic. Here are a few specific ideas that seem to be a great fit: real-time site usage statistics and reporting, stock ticker displays, the canonical chat example, real-time games, a Kanban board, and user alerts and notifications. In practice, you will need to evaluate your solution to determine which pieces require the advantages that Web Sockets provides, and utilize them only in these areas.

Building a Web Socket Example with .NET 4.5 and IIS 8

There are a few options when it comes to building web socket servers. Versions of IIS earlier than IIS 8 do not support Web Sockets. So if you want to host your web socket in IIS, you’ll have to use IIS 8 and .NET 4.5. Paul Batum talks more about this and shows a C# example in his blog. The F# example that follows takes a similar approach as defined in Paul’s post.

Note

There is an unsupported prototype for Web Sockets that will work in theory in earlier versions of IIS.

Let’s build a simple survey that will provide a charted view of the responses in real time via a web socket. The survey will provide a single question that asks, “What is your favorite programming language?” Possible answers include F#, C#, Erlang, JavaScript, and Other. With a tiny bit of help from the Twitter Bootstrap CSS, the web version of the question aspect of our survey appears as shown in Figure 4-1. The image shows two instances of Chrome side by side, with our survey loaded in each. This will make it easy for us to see how responsive our website becomes when using Web Sockets.

Note

You will need to run Visual Studio in administrator mode to execute this example.

Survey form

Figure 4-1. Survey form

The view that displays after you have submitted the answer to the survey question shows a chart that represents all responses to the survey. This is the view we want to update in real time. We’ll use a JavaScript charting library called D3. Since it’s not directly related to the web socket aspects of this example, I won’t show the D3-related code. However, you can find it in the mainModule.js file in the WebSocketIIS8Example project.

After enabling web sockets in IIS 8, we create a new ASP.NET MVC 4 application and make sure all projects are set to use .NET 4.5. We can now install the Microsoft.WebSockets NuGet package. We can also add a new .ashx file to the C# project and a few supporting .fs files to the F# WebApp project.

The first .fs file in this example is named ChartWebSocket.ashx.fs and it’s similar in concept to the code-behind .cs file that is often created when an .ashx item is created in a C# project. The code in this file creates a class that implements IHttpHandler. When a web socket request is processed, a new instance of a WebSocketChartHandler class is instantiated. The code looks like this:

namespace FsWeb

open System
open System.Web
open Microsoft.Web.WebSockets
open WebSocketServer

type ChartWebSocket() =
    interface IHttpHandler with 
        member x.ProcessRequest context = 
            if context.IsWebSocketRequest then
                context.AcceptWebSocketRequest(new WebSocketChartHandler())
        member x.IsReusable = true

The WebSocketChartHandler.fs file is composed of a few different pieces. First a WebSocketCollection is created to keep track of all the clients. Second, a VoteCounts record is defined. This will be used as the container that later gets serialized to JSON and sent to each web socket client. These two pieces of code look like the following:

let mutable clients = WebSocketCollection()

type VoteCounts = { language : string; count : int }

To help us out, a MailboxProcessor is used to keep a running total of the votes that have been cast for each programming language. This example could easily be extended to store the survey results in a database, but for the sake of simplicity we will be storing the responses in memory. The MailboxProcessor implementation looks like this:

type Message = 
    | Vote of string * AsyncReplyChannel<seq<string*int>>

let votesAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop votes =
        async {
            let! message = inbox.Receive()
            match message with
            | Vote(language, replyChannel) -> 
                let newVotes = language::votes 
                newVotes
                |> Seq.countBy(fun lang -> lang)
                |> replyChannel.Reply 
                do! loop(newVotes) 
            do! loop votes 
        }
    loop List.empty)

The MailboxProcessor implementation isn’t all that different from the one shown in Chapter 1, so I will only spend time explaining the code that is emphasized.

When a Vote message is received from the virtual queue, it’s added to an F# list. F# lists are singly linked list data structures, which allow them to perform well while remaining immutable. This list performs well because a copy of the list isn’t created each and every time a new value is added. Instead, the new value is added to a new address space and a reference is used to point to the rest of the list. We use the cons operator (::) to make this happen. After the operation is complete the new survey result will effectively be at the front of the list.

The votes are then counted for each language and a sequence of string * int is returned. For example, if three votes for F# have been cast, along with one for C# and two for JavaScript, the result of pushing a new vote for F# into the MailboxProcessor would be a sequence that conceptually contains F#, 4; C#, 1; JavaScript, 2.

The last bit of code contained in the WebSocketChartHandler.fs file is the WebSocketChartHandler class definition. This class inherits WebSocketHandler and overrides three of the methods it contains. The code looks like this:

type WebSocketChartHandler() =
    inherit WebSocketHandler()
    
    override x.OnOpen() = clients.Add x            
    override x.OnMessage(language:string) =  
        votesAgent.PostAndReply(fun reply -> Message.Vote(language, reply))   
        |> Seq.map(fun v -> { language = fst v; count = snd v } )
        |> JsonConvert.SerializeObject 
        |> clients.Broadcast
    override x.OnClose() =
        clients.Remove x |> ignore

Each method is pretty self-explanatory, so I’ll concentrate on the emphasized code in the OnMessage method. The first line sends the vote to the MailboxProcessor and waits for the reply. The result of that call is passed to Seq.map to be projected into a sequence of the VoteCounts record that was shown previously. This sequence of VoteCounts is then serialized to JSON using Json.NET. Lastly, the result is broadcast to all clients of the web socket.

The following is an abbreviated version of the JavaScript that connects to the web socket server:

$(function () {
    var uri,
        updateChart,
        $pages = $(".page");

    $pages.hide();
    $pages.first().toggle();

    updateChart = function (data) {
        /* Removed for brevity */
    };

    uri = "ws://localhost/WebSocketExample/ChartWebSocket.ashx";

    websocket = new WebSocket(uri);

    websocket.onopen = function () {
        $("#vote").click(function (event) {
            var vote = $("input:radio[name=langOption]:checked");

            if (vote.length) {
                websocket.send(vote.val());
            };

            $pages.hide();
            $("#results").toggle();

            event.preventDefault();
        });
    };

    websocket.onmessage = function (event) {
        updateChart($.parseJSON(event.data));
    };
});

You can see this in action by opening two browser instances as shown in Figure 4-1 and casting votes for your favorite language(s). Each browser will show a bar chart that contains the total votes cast for each language that has at least one vote. Additionally, the bar chart on each browser will update to display the latest totals almost immediately after clicking the Cast Vote button from any of the clients. Figure 4-2 shows an example of this.

Survey results

Figure 4-2. Survey results

Creating a Web Socket Server with Fleck

Although the example I just showed works well, what do we do when we don’t have IIS 8, don’t want to upgrade to .NET 4.5, or want to host the web socket outside of IIS? Luckily, there are several options available to us. My favorite is a library called Fleck. To get started with Fleck, install the Fleck NuGet package. You can then use code such as the following to stand up a self-hosted web socket server:

module FsFleckServer

open System
open System.Collections.Generic
open Fleck
open Newtonsoft.Json

type VoteCounts = { language : string; count : int }  
     
type Message = 
    | Vote of string * AsyncReplyChannel<seq<string*int>>

let votesAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop votes =
        async {
            let! message = inbox.Receive()
            match message with
            | Vote(language, replyChannel) -> 
                let newVotes = language::votes 
                newVotes
                |> Seq.countBy(fun lang -> lang)
                |> replyChannel.Reply 
                do! loop(newVotes) 
            do! loop votes 
        }
    loop List.empty)

let main() = 
    FleckLog.Level <- LogLevel.Debug
    let clients = List<IWebSocketConnection>()
    use server = new WebSocketServer "ws://localhost:8181"

    let socketOnOpen socket = clients.Add socket

    let socketOnClose socket = clients.Remove socket |> ignore

    let socketOnMessage language = 
        let results = 
            votesAgent.PostAndReply(fun reply -> Message.Vote(language, reply))   
            |> Seq.map(fun v -> { language = fst v; count = snd v } )
            |> JsonConvert.SerializeObject 
        clients |> Seq.iter(fun c -> c.Send results)

    server.Start(fun socket ->
                    socket.OnOpen <- fun () -> socketOnOpen socket
                    socket.OnClose <- fun () -> socketOnClose socket
                    socket.OnMessage <- fun message -> socketOnMessage message)
    
    Console.ReadLine() |> ignore

main()

About the using Function

I’ve talked about the use keyword a few times now and briefly mentioned the using function, but haven’t provided any examples as of yet that show how to use using. The using function allows you to have more control over when Dispose() will be called. Here’s how we could have written the code in the main module of the Fleck example using the using function:

using (new WebSocketServer "ws://localhost:8181")
    (fun server -> 
       let socketOnOpen socket = clients.Add socket

       let socketOnClose socket = clients.Remove socket |> ignore

       let socketOnMessage language = 
           let results = 
               votesAgent.PostAndReply(fun reply -> 
                                           Message.Vote(language, reply))   
               |> Seq.map(fun v -> { language = fst v; count = snd v } )
               |> JsonConvert.SerializeObject 
           clients |> Seq.iter(fun c -> c.Send results)

       server.Start(fun socket ->
                      socket.OnOpen <- fun () -> socketOnOpen socket
                      socket.OnClose <- fun () -> socketOnClose socket
                      socket.OnMessage <- fun message -> 
                                              socketOnMessage message) 

       Console.ReadLine() |> ignore)

The two type definitions and the defined agent are exactly the same as what we had in the IIS 8 example. The emphasized code instantiates a WebSocketServer; defines three methods to handle socket open events, socket close events, and messaging events, respectively; and starts up the server.

To use it, all you have to do is change the JavaScript uri assignment in the previously shown JavaScript code to point at the URL of the Fleck server and clear your browser cache. In the end, the application works exactly as it did in the IIS 8 example.

  • Safari Books Online
  • Create BookmarkCreate Bookmark
  • Create Note or TagCreate Note or Tag
  • PrintPrint