Exam Objectives
Quick Overview of Training Materials
Exam Ref 70-486: Developing ASP.NET MVC 4 Web Applications - Objective 1.6
Programming ASP.NET MVC 4 (OReilly) - Chapter 11
OReilly - Websockets
Programming ASP.NET MVC 4 (OReilly) - Chapter 11
OReilly - Websockets
Channel 9 - Building real-time web apps with HTML5 WebSockets - 1 hour video, Client side
MSDN - WebSockets: Stable and Ready for Developers
Channel 9 - Building apps with WebSockets using IIS, ASP.NET and WCF - 1 hour video, Server side
Channel 9 - Web Dev Futures Day Session 5: Building real-time apps with WebSockets
Channel 9 - Building Real-Time Applications with ASP.NET SignalR
Channel 9 - Damian Edwards and David Fowler Demonstrate SignalR
Microsoft Virtual Academy - Lighting Up Real-Time Web Communications with SignalR
Simple server implemtation in C# EchoHandler.ashx.cs
MSDN - WebSockets: Stable and Ready for Developers
Channel 9 - Building apps with WebSockets using IIS, ASP.NET and WCF - 1 hour video, Server side
Channel 9 - Web Dev Futures Day Session 5: Building real-time apps with WebSockets
Channel 9 - Building Real-Time Applications with ASP.NET SignalR
Channel 9 - Damian Edwards and David Fowler Demonstrate SignalR
Microsoft Virtual Academy - Lighting Up Real-Time Web Communications with SignalR
Simple server implemtation in C# EchoHandler.ashx.cs
Tutorial: Getting Started with SignalR 2
Introduction to Scaleout in SignalR
My Post - Microsoft 70-480: Implement a callback (WebSockets)
Introduction to Scaleout in SignalR
My Post - Microsoft 70-480: Implement a callback (WebSockets)
Before any data can be send over a websockets connection, the connection must first be established. This is accomplished through a handshake between the client (browser) and the server. The handshake is initialized on the client by calling the Websocket() javascript constructor. The constructor is passed the URL of the websocket server using the format "ws:\\<server url>", sending a protocol upgrade request. While the url starts with "ws:" (or "wss" for secure websockets), it is still an HTTP request. To finish the handshake, the server sends back a response with status "101" to switch protocol to Websockets. Below is what the request and response looks like:
While the Javascript code is very straightforward:
var uri = 'ws://troywebsocketstest1.azurewebsites.net/EchoHandler.ashx'; websocket = new WebSocket(uri);
... the server side code can be handled multiple ways. There are a number of .NET namespaces that handle different aspects of websockets. One low-level way to handle websockets requests is by implementing IHttpHandler and using the System.Net.Websockets namespace, as in Paul Batum's EchoServer example. This is done by adding a Generic Handler class and then implementing the ProcessRequest method.
public class EchoHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { if (context.IsWebSocketRequest) context.AcceptWebSocketRequest(HandleWebSocket); else context.Response.StatusCode = 400; } private async Task HandleWebSocket(WebSocketContext wsContext) { ... } public bool IsReusable { ... } }
In this example, the HandleWebSocket delegate is where the magic happens. While the connection is open, it waits to receive a message, which it then bounces back to the client. In Paul's example he didn't implement the binary portion, but it is really the same as the text portion minus the convertion to and from UTF8 encoding.
Another approach is to use the classes found in the Microsoft.Web.Websockets namespace. This namespace comes from the Microsoft.Websockets Nuget package. We still use an .ashx file, but instead of passing a delegate to AcceptWebsocketsRequest, we pass a class that inherits the WebSocketHandler class and overrides the appropriate methods. The following code was part of a quickie chat app demoed by Stephan Schackow at Build 2011:
public void ProcessRequest(HttpContext context) { var user = context.Request.Cookies["username"].Value; if(context.IsWebSocketRequest) { context.AcceptWebSocketRequest(new ChatHandler(user)); } }
public class ChatHandler : WebSocketHandler { private string user; private JavaScriptSerializer serializer = new JavaScriptSerializer(); private static WebSocketCollection chatapp = new WebSocketCollection(); public ChatHandler(string username) { user = username; } public override void OnOpen() { chatapp.Add(this); chatapp.Broadcast(serializer.Serialize(new { type = MessageType.Join, from = user, value = user + " joined the server.", })); }
The WebSocketCollection class is handy here because the Broadcast() method can by used to send a message to all connected clients, which totally makes sense for a chat app, ey?
The client side API for websockets allows sending strings (including pretty much anything that can be serialized like DOMstring and JSON objects) as text, or ArrayBuffers and Blobs as binary data. I wanted to recreate the canvas based binary echo example demoed by Ravi Rao, but I didn't want to go with the .msToBlob() method he used (since it is IE specific). So I had to find a little help on stackoverflow: how to extract raw data from canvas, how to convert raw data to blob, and how to create blob from dataUrl were all helpful. I also didn't feel like creating a paintable canvas from scratch so I used this example by ROBO design. I got very frustrated when I kept getting solid black images back when getting the dataUrl as type "image/jpeg", eventually figured out that my problem was due to jpeg not supporting transparency, go figure. Eventually I was able to bounce a binary message off a modified EchoHandler.ashx. Notice the difference in the frames (Chrome dev console):
Some of the stranger text frames were caused by me playing around trying to send stringified DOM elements over the wire. I kept getting circular reference errors from JSON and lost interest.
One interesting point addressed in Paul Batum's EchoHandler example is the fact that a websockets message may actually be broken up in to multiple data frames. While wrestling with getting the client side canvas to encode right, I disabled most of the code that checks for additional messages... basically commented out this loop:
The result was interesting... I finally got encoding to work (by switching to png which supports transparency... I figured out the root issue later), but I only got a third of the image back. This was because only the first frame was echoed.
I'm not going to beat the client side stuff to death since I already did that in a previous post. One interesting note: the OReilly chapter on WebSockets highlights an interesting point regarding binary data: if the binary data is going to be manipulated client-side, it probably makes sense to automatically convert it to an ArrayBuffer. This is done by setting the ws.binaryType property (where ws is your websocket instance in JavaScript) to "arraybuffer".
One interesting point addressed in Paul Batum's EchoHandler example is the fact that a websockets message may actually be broken up in to multiple data frames. While wrestling with getting the client side canvas to encode right, I disabled most of the code that checks for additional messages... basically commented out this loop:
while (receiveResult.EndOfMessage == false) { ... }
The result was interesting... I finally got encoding to work (by switching to png which supports transparency... I figured out the root issue later), but I only got a third of the image back. This was because only the first frame was echoed.
I'm not going to beat the client side stuff to death since I already did that in a previous post. One interesting note: the OReilly chapter on WebSockets highlights an interesting point regarding binary data: if the binary data is going to be manipulated client-side, it probably makes sense to automatically convert it to an ArrayBuffer. This is done by setting the ws.binaryType property (where ws is your websocket instance in JavaScript) to "arraybuffer".
Choose connection loss strategy
I found it interesting that the Exam Ref seemed to take the position that only the client needs to be concerned about connection loss, since the server can't communicate with a disconnected client. In situations where the client and server are the only interested parties, that may be true, but many use cases for WebSockets involve multiple clients connected to the same server and sharing messages (chat, multiplayer gaming, etc.) and in these cases there is definitely a need for the server to act when a connection is closed.
Connection loss is usually going to be handled by the onclose and onerror events on the client, and the WebSocketHandler class in Microsoft.Web.Websockets also has OnError and OnClose methods that can be overriden. In the chat application example, these methods are used to notify everyone else in the chatroom that someone left:
"Keep-alive" messages, also called "ping-pong" messages, are used by some browsers to prevent the underlying TCP connection from timing out, and as a mechanism to monitor the health of the connection. In cases where the browser and server are not using keep-alive messages, the close or error events may not be triggered until the next attempt by the application to use the connection.
Connection loss is usually going to be handled by the onclose and onerror events on the client, and the WebSocketHandler class in Microsoft.Web.Websockets also has OnError and OnClose methods that can be overriden. In the chat application example, these methods are used to notify everyone else in the chatroom that someone left:
public override void OnClose() { chatapp.Remove(this); chatapp.Broadcast(serializer.Serialize(new { type = MessageType.Leave, from = user, value = user + " has left the server." })); } public override void OnError() { chatapp.Remove(this); chatapp.Broadcast(serializer.Serialize(new { type = MessageType.Leave, from = user, value = user + " has thrown an error." })); }
"Keep-alive" messages, also called "ping-pong" messages, are used by some browsers to prevent the underlying TCP connection from timing out, and as a mechanism to monitor the health of the connection. In cases where the browser and server are not using keep-alive messages, the close or error events may not be triggered until the next attempt by the application to use the connection.
One point that has been emphasized several places is that WebSockets is not an inherently reliable message delivery tool. While for many use cases a "fire and forget" approach works fine, where a message is sent and no real follow up is conducted, in other cases a degree of reliability may be desired. On way to ensure messages are received is to respond with acknowledgement messages. Another mechanism that can increase the likelyhood of message delivery is the use of message queues. If a sent message is not acknowledged, or a network error is detected, messages can be retried once connectivity is restored and message deliver is confirmed.
It is possible to try to reconnect to the server once a broken connection is detected (onerror event, failed heartbeat, whatever). However, care should be used to avoid locking up the client UI with a flood of retry attempts. The slideshare material recommends a backoff algorithm to increase time between retries. Timeouts can also be utilized to prevent message queues from becoming too large.
A valid point made by the Exam Ref is that it will be necessary to handle the possibility of duplicate messages if a retry mechanism is built into your application, since it is possible that a connection might fail AFTER a message is sent but BEFORE receipt is confirmed.
The MVA for SignalR has an interesting demo that uses SQL Server behind the server side hub to keep track of connections. If a connection isn't heard from for a set interval (say the client lost internet so a close message was never sent), then the hub knows that the connection is dead and frees those resources. On the client side, they demonstrate a closed connection attempting to reconnect. Websockets won't automatically retry opening the connection, so the app will have to implement that logic.
When to use WebSockets
Websockets is ideal for applications that require two way, full duplex, real time communication - chat, multi-player gaming, dashboards, collaboration, location (gps) tracking in real time.
The stockticker demo is mentioned in a couple talks, though this doesn't seem to necessitate two way communication, and could probably be done just as well with server sent events (the demo used by Ravi Rao demonstrates the latency advantage over HTTP Polling). One reason SSE doesn't get any love in the .NET world is probably because IE is the only browser that doesn't support it.
WebSockets makes sense when a long lived, persistant connection is appropriate. It is not a replacement for AJAX. If updates to the client are infrequent, it may be simpler to just use AJAX calls rather than implement WebSockets. Also, while WebSockets is becoming more available, it is still a new standard and may not be availble to all clients. If performance needs can still be met with other techniques (such as polling), and there are known client compatibility issues, then it may make sense to steer clear of WebSockets (or use a library like SignalR that automatically falls back to available features)
Some security solutions may interfere with WebSockets connections due to the lack of HTTP headers or because of the way proxy servers communicate. In some cases, using a secure connection may increase the likelyhood of establishing and maintaining a WebSockets connection.
Implement SignalR
Note: the Exam Ref does NOT cover SignalR, at all. Nada. However, Chapter 11 of the OReilly book covers it pretty well. SignalR is an abstraction over a variety of real time communication technologies. Since not all clients support WebSockets, SignalR will gracefully fall back to other two-way communication models can be used with these clients:
- Server sent events (SSE), also called EventSource, supported by older versions of all the major browsers except IE.
- Forever frame - A hidden iframe element in the page is set to chunked encoding, and additional elements are sent by the server to relay messages
- Ajax Long Polling - The client sends the server an AJAX message, and instead of responding immediately, the server holds on to it until it has a message to send.
Key concepts:
- Connections - Persistent connections can be used to broadcast plain text to connected users. A more low-level way to implement SignalR. It generally is recommended to use hubs whenever possible. If a very specific messsage format is to be used, connections may be appropriate. (The ConnectionExtensions class contains the broadcast and send methods)
- Hubs - The prefered way to use SignalR. The client and server are able to use the underlying persistant connection to call methods on each other.
- Backplane - Used to connect multiple servers to each other to allow cross server message passing. When the backplane is in use, all messages from a client to a particular server are passed to the backplane, which then passes the messages to all the servers, who then forward the messages to all their respective connected users. Currently SQL Server, Redis Cache, and Azure Service Bus backplanes are all available, though for very large, complicated SignalR implementations is may be necessary to build a custom backplane to meet scalability requirements.
namespace SignalRChat { [HubName("hitCounter")] public class HitCounterHub : Hub { static int _hitCount = 0; public void RecordHit() { _hitCount += 1; Clients.All.onRecordHit(_hitCount); } } }
"Clients" is a property on Hub that represents all the clients currently connected to the hub. "All" is a dynamic that is used to make a procedure call on all the clients. "Users" will reference a list of users, "Groups" will reference lists of groups of users, "Caller" will reference only the client making the call, "Others" will reference everyone EXCEPT the caller, and "OthersInGroup" will referernce everyone but the caller in a list of groups. In the above example, "onRecordHit" is actually defined on the client.
The javascript on the client side is also very straight forward:
$(function () { var connection = $.hubConnection(); var hub = connection.createHubProxy("hitCounter"); hub.on("onRecordHit", function (hitCount) { $('#hitCountValue').text(hitCount); }); connection.start(function () { hub.invoke('recordHit'); }); });
When the page is loaded, a new persistant connection is created with $.hubConnection();. The hub proxy created on top of that connection links the client to the hub on the server. When the server calls Clients.All.onRecordHit, the event "onRecordHit" is triggered on the client, and that code is executed. For the client to execute a method on the server, it uses the hub.invoke() method.
The tutorials on ASP.NET use a different syntax on the client side for creating and using a hub, though the result is the same. In the code below, "chatHub" is the name of the Hub class on the server, and calling this on $.connection creates the hub proxy, equivalent to .createHubProxy("chatHub"). Instead of chat.on("addNewMessageToPage"...), it uses chat.client.addNewMessageToPage. The concepts are still the same, though.
var chat = $.connection.chatHub; chat.client.addNewMessageToPage = function (name, message) { $('#discussion').append('<li><strong>' + htmlEncode(name) + '</strong>: ' + htmlEncode(message) + '</li>'); }; $.connection.hub.start().done(function () { $('#sendmessage').click(function () { chat.server.send($('#displayname').val(), $('#message').val()); $('#message').val('').focus(); }); }); });
The OReilly text offers a quick example of creating a Persistant Connection. On the server, the connection is created with a class that inherits PersistantConnection. A route to the class is registered by calling:
RouteTable.Routes.MapConnection<ConnectionClassName>("route","route/{*operation}");
Then on the client side, the connection is create with $.connection('/route'). Incoming messages are handled by a delegate assigned to connection.received, and the connection is started by calling connection.start().
For a much more in depth dive into using Signalr, check out this post about my BoxBlaster demo.
No comments:
Post a Comment