Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.
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.
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.
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.
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.
You will need to run Visual Studio in administrator mode to execute this example.
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:
namespaceFsWebopenSystemopenSystem.WebopenMicrosoft.Web.WebSocketsopenWebSocketServertypeChartWebSocket()=interfaceIHttpHandlerwithmemberx.ProcessRequestcontext=ifcontext.IsWebSocketRequestthencontext.AcceptWebSocketRequest(newWebSocketChartHandler())memberx.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:
letmutableclients=WebSocketCollection()typeVoteCounts={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:
typeMessage=|Voteofstring*AsyncReplyChannel<seq<string*int>>letvotesAgent=MailboxProcessor.Start(funinbox->letrecloopvotes=async{let!message=inbox.Receive()matchmessagewith|Vote(language,replyChannel)->let newVotes = language::votes newVotes |> Seq.countBy(fun lang -> lang) |> replyChannel.Replydo!loop(newVotes)do!loopvotes}loopList.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:
typeWebSocketChartHandler()=inheritWebSocketHandler()overridex.OnOpen()=clients.Addxoverridex.OnMessage(language:string)=votesAgent.PostAndReply(fun reply -> Message.Vote(language, reply)) |> Seq.map(fun v -> { language = fst v; count = snd v } ) |> JsonConvert.SerializeObject |> clients.Broadcastoverridex.OnClose()=clients.Removex|>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(){varuri,updateChart,$pages=$(".page");$pages.hide();$pages.first().toggle();updateChart=function(data){/* Removed for brevity */};uri="ws://localhost/WebSocketExample/ChartWebSocket.ashx";websocket=newWebSocket(uri);websocket.onopen=function(){$("#vote").click(function(event){varvote=$("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.
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:
moduleFsFleckServeropenSystemopenSystem.Collections.GenericopenFleckopenNewtonsoft.JsontypeVoteCounts={language:string;count:int}typeMessage=|Voteofstring*AsyncReplyChannel<seq<string*int>>letvotesAgent=MailboxProcessor.Start(funinbox->letrecloopvotes=async{let!message=inbox.Receive()matchmessagewith|Vote(language,replyChannel)->letnewVotes=language::votesnewVotes|>Seq.countBy(funlang->lang)|>replyChannel.Replydo!loop(newVotes)do!loopvotes}loopList.empty)letmain()=FleckLog.Level<-LogLevel.Debugletclients=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()|>ignoremain()
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.