It’s time for post No. 2 in the WCF 4.5 series. Part 1 of 2 was about WebSocket support with SOAP-based messages. Part 2 is about WebSocket support with plain text messages that enables the interaction between web browsers and WCF.
Previous posts:
- What’s new in WCF 4.5? Let’s start with WCF configuration
- What’s new in WCF 4.5? A single WSDL file
- What’s new in WCF 4.5? Configuration tooltips and intellisense in config files
- What’s new in WCF 4.5? Configuration validations
- What’s new in WCF 4.5? Multiple authentication support on a single endpoint in IIS
- What’s new in WCF 4.5? Automatic HTTPS endpoint for IIS
- What’s new in WCF 4.5? BasicHttpsBinding
- What’s new in WCF 4.5? Changed default for ASP.NET compatibility mode
- What’s new in WCF 4.5? Improved streaming in IIS hosting
- What’s new in WCF 4.5? UDP transport support
- What’s new in WCF 4.5? WebSocket support (Part 1 of 2)
If you haven’t read part 1, please go over it first so you can get the gist about WebSockets, NetHttpBinding, and how it is used in WCF.
In part 1, I demonstrated how to create both binary encoded SOAP bindings and text encoded SOAP bindings with WebSockets. The problem is that in JavaScript, it can get difficult to create and parse SOAP messages - this is why we tend to use XML/JSON based bindings (such as WebHttpBinding
) instead of SOAP-based bindings (BasicHttpBinding
/WsHttpBinding
) when calling WCF services from JavaScript.
Creating a duplex service with WebSockets
, NetHttpBinding
, and plain text messages, is just like creating any other WCF service:
- Define the contract and callback contract
- Implement the service
- Configure the host
- Consume the service from a client app
First, we will create our contract. Since we need to receive and send messages, we will create a duplex contract, each contract with a single method which we will mark with action=”*”:
Contracts
[ServiceContract]
public interface IWebSocketEchoCallback
{
[OperationContract(IsOneWay = true, Action = "*")]
void Send(Message message);
}
[ServiceContract(CallbackContract = typeof(IWebSocketEchoCallback))]
public interface IWebSocketEcho
{
[OperationContract(IsOneWay = true, Action = "*")]
void Receive(Message message);
}
The echo service itself is a simple implementation that receives the message and sends it back to the client:
EchoService
public class EchoService : IWebSocketEcho
{
IWebSocketEchoCallback _callback = null;
public EchoService()
{
_callback =
OperationContext.Current.GetCallbackChannel<IWebSocketEchoCallback>();
}
public void Receive(Message message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
WebSocketMessageProperty property =
(WebSocketMessageProperty)message.Properties["WebSocketMessageProperty"];
WebSocketContext context = property.WebSocketContext;
var queryParameters = HttpUtility.ParseQueryString(context.RequestUri.Query);
string content = string.Empty;
if (!message.IsEmpty)
{
byte[] body = message.GetBody<byte[]>();
content = Encoding.UTF8.GetString(body);
}
string str = null;
if (string.IsNullOrEmpty(content))
{
str = "Opening connection from user " +
queryParameters["Name"].ToString();
}
else
{
str = "Received message: " + content;
}
_callback.Send(CreateMessage(str));
}
private Message CreateMessage(string content)
{
Message message = ByteStreamMessage.CreateMessage(
new ArraySegment<byte>(
Encoding.UTF8.GetBytes(content)));
message.Properties["WebSocketMessageProperty"] =
new WebSocketMessageProperty
{ MessageType = WebSocketMessageType.Text };
return message;
}
}
The Receive
method handles two types of calls:
- The first “connection upgrade” message – when the client first connects to the service and tries to upgrade the connection from HTTP to
WebSocket
. In this call, the request is sent using HTTP GET
, and therefore there is no body, but we can access the URL’s query string. - The second message and on are the messages being sent by the client over the WebSocket transport – these messages contain a message body, with no special query string.
Line 5-9 shows how to create a standard duplex service by storing the callback channel in a local variable. The callback channel will be used later on in the code in order to send messages back to the client. The service uses the default instancing mode which is PerSession
, so a new instance will be created for each client, and the local variable will point to a different callback channel in each service instance.
Lines 17-27 demonstrates the technique of parsing the message – either by checking its query string or by reading the byte array from the message and transforming it to a string
.
Lines 32-43 checks which type of message is being handled, the first connection request, or a consequent message from the client. In each case, the service responds by echoing the message back to the client.
Line 46-56 demonstrates how to create a Message
object with a simple string
content when using the byte stream encoding.
Note: To use the ByteStreamMessage
type, add a reference to the System.ServiceModel.Channels
assembly.
Note: WebSocket messages can be either text or binary, so if you are planning on using binary messages, you will need to change the code to work with byte arrays instead of string
s.
Now that we have the contracts and the service, we need to define our host and endpoint. In this example, I will use IIS as the host and I will use the routing mechanism of ASP.NET to create a service URL address that doesn’t contain the annoying “.svc” extension. The following global.asax code shows how to do that:
Global.Asax
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new ServiceRoute("echo",
new ServiceHostFactory(),
typeof(EchoService)));
}
}
And now for the endpoint configuration. Since NetHttpBinding
uses SOAP messages, and there is no “WebSocketHttpBinding
” for passing plain byte streams, we need to create a custom binding that will allow us to receive messages over WebSocket where the message can either be a text message or a binary message (the WebSocket API supports both types).
The standard encodings of WCF - text, binary, and MTOM, will not enable us to receive non-SOAP byte streams, that is why we need to use a new encoding which was introduced in WCF 4 – the ByteStreamMessageEncoding.
The following endpoint and binding configuration will allow us to open a WebSocket listener that receives simple byte streams:
Service Configuration
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
<services>
<service name="UsingWebSockets.EchoService">
<endpoint address=""
binding="customBinding"
bindingConfiguration="webSocket"
contract="UsingWebSockets.IWebSocketEcho" />
</service>
</services>
<bindings>
<customBinding>
<binding name="webSocket">
<byteStreamMessageEncoding/>
<httpTransport>
<webSocketSettings transportUsage="Always"
createNotificationOnConnection="true"/>
</httpTransport>
</binding>
</customBinding>
</bindings>
</system.serviceModel>
The important part in the configuration is lines 13-21:
- We set
transportUsage
to Always
to force the usage of WebSocket rather than HTTP. - We set
createNotificationOnConnection
to true
to allow our Receive
method to be invoked for the connection request message (the first GET
request which is sent to the service). - We use
byteStreamMessageEncoding
which allows the service to receive simple byte streams as input instead of complex SOAP structures.
To test our code, we can add an HTML page to our project. The following code is based on the StockTicker
demo from the HTML5 Labs website:
Echo Client
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Echo Demo</title>
<script src="Scripts/jquery-1.4.1.js"
type="text/javascript"></script>
<script>
$(document).ready(function () {
if (!window.WebSocket && window.MozWebSocket) {
window.WebSocket = window.MozWebSocket;
}
$('#echoForm').submit(function (event) {
$('#echoForm')
.add('#echoForm > *')
.attr('disabled', 'disabled');
var uri = 'ws://' + window.location.hostname +
window.location.pathname.replace('EchoDemo.html', 'echo') +
'?Name=' + $("#name").val();
connect(uri);
event.preventDefault();
});
});
function connect(uri) {
$('#messages').prepend('<div>Connecting...</div>');
var websocket = new WebSocket(uri);
websocket.onopen = function () {
window.focus();
$('#echoForm').hide();
$('#outputArea').show();
window.setInterval(function()
{
websocket.send("the time is " + new Date());
}, 1000);
$('#messages').html(
'<div>Connected. Waiting for messages...</div>');
};
websocket.onclose = function () {
if (document.readyState == "complete") {
var warn = $('<div>').html(
'Connection lost. Refresh the page to start again.').
css('color', 'red');
$('#messages').append(warn);
}
};
websocket.onmessage = function (event) {
$("#messages").append(event.data + "<br>");
};
};
</script>
</head>
<body>
<form id="echoForm" action="">
<input type="text" id="name"
placeholder="type your name" />
</form>
<div id="outputArea" style="display: none">
<div id="messages" style="height: 80%; overflow: hidden">
</div>
</div>
</body>
</html>
Most of the above code is jQuery stuff to handle the incoming message, so let’s point out the important parts:
Lines 18-20 – In these lines, we create the URI of the service. Note the use of the ws:// scheme – this is the scheme of WebSocket, but it works just fine even when our service base address is set to HTTP.
Lines 27-56 – The connect
function basically does all the rest. The WebSocket functions are based on the WebSocket API.
- Line 30 – Create the
WebSocket
object - Lines 32-42 – Open the connection
- Lines 36-49 – Run a function every 1 second that sends the current time to the service
- Lines 44-51 – Handle the WebSocket channel closing
- Lines 53-55 – Handle a received message (a message sent from the service to the client)
Running the client will show the following output:
To conclude, in order to create a service that can receive and send message to browsers using WebSockets, we need to do the following:
- Create the duplex contract which contains simple
Receive
and Send
methods (or any other names you like). - Implement the contract in a service like you’ll implement any other duplex service. The only thing you need to take care of is how to read and write the message.
- Create an endpoint which uses a custom binding which supports WebSockets and simple byte-stream encoding.
Although the above works quite well, there is another way to create this type of service – by creating a service class that inherits from the Microsoft.WebSockets.WebSocketService
type. The Microsoft.WebSockets
package, available from NuGet, enables the creation of WebSocket-based services. Once you inherit your service from WebSocketService
, you can override methods such as OnMessage
, OnOpen
, OnClose
, and OnError
. Working with these methods is quite easy, as demonstrated in the following code:
EchoService2
public class EchoService2 :
Microsoft.ServiceModel.WebSockets.WebSocketService
{
public override void OnMessage(string message)
{
string str = "Received message: " + message;
Send(str);
}
public override void OnOpen()
{
var queryParameters = this.QueryParameters;
string str = "Opening connection from user " +
queryParameters["Name"].ToString();
Send(str);
}
protected override void OnClose()
{
base.OnClose();
}
protected override void OnError()
{
base.OnError();
}
}
As you can see, in this case you don’t have to work directly with byte arrays. In order to host this service, you also don’t need to define a special endpoint configuration, as this package includes a WebSocketHost
class that automatically creates and configures a WebSocket endpoint. To create a WebSocketHost
and provide it to IIS, we need to create a class that inherits from ServiceHostFactory
, as demonstrated in the following code:
WebSocketServiceHostFactory
public class WebSocketServiceHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
var host = new WebSocketHost(serviceType, baseAddresses);
host.AddWebSocketEndpoint();
return host;
}
}
Note: The ServiceHostFactory
is declared in the System.ServiceModel.Activation
assembly, so don’t forget to add a reference to it.
Once we have the new factory, we can register it with the routing mechanism (lines 5-7):
Global.Asax
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new ServiceRoute("echo2",
new WebSocketServiceHostFactory(),
typeof(EchoService2)));
RouteTable.Routes.Add(new ServiceRoute("echo",
new ServiceHostFactory(),
typeof(EchoService)));
}
}
All that is left is to change the client HTML code in line 19 to call the ‘echo2
’ service instead of ‘echo
’.
You can see more examples on how to use this package in Paul Batum’s blog post, and in his //BUILD session.
So as you can see, it is quite easy to create a WCF service that can receive messages from a browser and push messages to a browser by using WebSockets. Farewell long polling, I hope we never meet again.
You can download the above code (both versions) from my SkyDrive. The source code also includes a sample self-hosted WebSocket service and an HTML page that uses it instead of the IIS-hosted service.