Writing a WCF 4.5 WebSocket Service
Peter Vogel continues his exploration of WCF 4.5's support for WebSockets by writing the code to accept data from the client and then return data to the client whenever that data becomes available.
In my last column, I discussed why I thought WebSockets was so important and laid the groundwork for creating a WebSocket service using WCF 4.5, Visual Studio 11 and Windows Server 8. In this column, I'll walk through the code for the service I started in the last column. I'll use the standard WCF paradigm of defining a set of interfaces, then implementing them.
A WebSockets service consists of two interfaces: one holding the method that receives the client's requests, and another used to send results to the client. Your WCF must implement the first interface—the one with the method that accepts requests.
In my example, I've defined an interface called IRequestOrderStatusData with a method called OrderStatusById that allows the client to request status updates for an order by passing an Order Id to the method. I add a WCF service called OrderStatus to my WCF Service Application and have it implement that interface:
Public Class OrderStatus
Opening the Connection
The method in this interface is first called when the client opens the connection to the service (made through an HTTP request that allows WCF to set up the TCP channel used for all subsequent requests). No parameters are passed when the connection is opened, so at the top of the method, you'll need to see if the parameter passed to the method (which must be a Message object) is empty before attempting to process any parameters. You can do that with the Message object's IsEmpty method:
Public Sub OrderStatusById(OrderIdMessage As Channels.Message)
If Not OrderIdMessage.IsEmpty Then
While this forces the client to always pass a parameter when calling the service's method to distinguish between that opening call and calls to the method, the method's parameter isn't optional when calling the method so this technique works reliably.
Just because no parameter is passed when the connection is opened doesn't mean the client can't pass information to the service when opening the connection. The client uses the URL for your service when opening the connection to your service and can include information in the query string of that URL. While the Message object passed when the connection is opened doesn't contain the method's parameter, the object does allow you to access the querystring information passed during the opening process.
The first step in accessing the querystring is to retrieve the WebSocketMessageProperty object passed in the parameter's Properties collection. Once you have it, you can retrieve its WebSocketContext object, which will let you access, through its RequestUri property, the URL the client used to open the connection to your service.
This code uses the HttpUtility's ParseQueryString to break the querystring (in the RequestUri's Query property) into a dictionary of name value pairs and retrieves the value for the City name from the querystring:
Dim wsProp As Channels.WebSocketMessageProperty =
Dim wsContext As Net.WebSockets.WebSocketContext =
Dim parms =
Dim city As string = parms("City")
Once the client has opened the TCP connection to your service, it can start making calls to your service's method, passing a single parameter value. To retrieve the parameter value passed to your method, you'll need to use the Message object's GetBody method, which will return an array of bytes. I'm assuming that I want to work in an interoperable mode, so I want to use String values when passing data between the client and the server. This code converts the GetBody's array of bytes into a string for later processing:
Dim bytOrderId As Byte()
Dim OrderId As String
bytOrderId = OrderIdMessage.GetBody(Of Byte())()
OrderId = Encoding.UTF8.GetString(bytOrderId)
I'll pass over the processing that retrieves the Order's status and assume it's been retrieved and is ready to be sent to the client. To send data to the client, you need to open a connection to the client using the interface you designed for returning the data. It makes sense to me to create that connection in the service's constructor and store it in a field for the service (if your service may not always return a value, you can defer opening the channel until you need it). If you want, you can even create the connection on each transmission, though I'm not sure what impact that will have on performance.
This code opens a channel in the constructor using the ISendOrderStatus interface defined earlier, and stores the channel in a field:
Private SendOrderStatusData As ISendOrderStatus
Public Sub New()
At this point you're ready to send some data to the client using the method defined in your interface (in my case, that's a method called SendOrderStatus). You have to pass the method a Message object. Creating a Message object is a multi-step process, so it makes sense to create a utility routine to handle that.
I'm still assuming that I want to work in an interoperable, so I still want to return a String value. The first step in creating a message that holds a string is to convert the string data into a byte array. Once you've done that you must instantiate an ArraySegment of bytes, passing your byte array:
Public Shared Function CreateStringMessage(Content As String)
Dim bytContent As Byte()
bytContent = Encoding.UTF8.GetBytes(Content)
Dim asContent As ArraySegment(Of Byte)
asContent = New ArraySegment(Of Byte)(bytContent)
With the ArraySegment create you can create a Message object by passing the ArraySegement to the ByteStreamMessage object's CreateMessage method (you'll need to add a reference to the System.ServiceModel.Channels to use the ByteStreamMessage object):
Dim msg As Channels.Message
msg = Channels.ByteStreamMessage.CreateMessage(asContent)
However, if you're returning a String, you must add a WebSocketMessageProperty object to your Messages's Properties collection. That WebSocketMessageProperty object must have its MessageType property set to WebSocketMessageType.Text and must be added to the Properties collection under the key WebSocketessageProperty. That's what this code does before returning the Message object:
msg = WebSocketsUtlities.CreateStringMessage(OrderStatusData)
Dim wsmProperty = New Channels.WebSocketMessageProperty
wsmProperty.MessageType = Net.WebSockets.WebSocketMessageType.Text
msg.Properties("WebSocketMessageProperty") = wsmProperty
The client can close the connection that your service is using to send data. If so, your connection will be disposed (this is another good reason to create the connection once at the start of the service). It's a good practice to check that your connection still exists before returning data.
Code to convert a String into a Message using my utility and send the resulting Message to the client would look like this:
Dim OrderStatusMessage As Channels.Message
One of the benefits of using WebSockets is that you're not restricted to sending a single status update. Your service can continue to monitor the Order's status and send an update to the client whenever the service feels it's necessary.
In order for this to work, however, you'll need to tweak the bindings for your service in your config file. To support WebSockets, WCF 4.5 provides a new message encoding format which you'll need to specify in a cutomBinding element in your config file's bindings element. Your customBinding element must include the byteStreamMessageEncoding element.
You only need one binding that specifies these settings, so you might as well give a generic name (I used webSocket) and tie it to all of your WebSocket services in your project. In your service's configuration, you'll need to set its binding attribute to customBinding and set the bindingConfiguration to the name of the customBinding you created.
Here are the settings used for my service:
Peter Vogel is a principal in PH&V Information Services, specializing in ASP.NET development with expertise in SOA, XML, database, and user interface design. His most recent book ("rtfm*") is on writing effective user manuals, and his blog on technical writing can be found at rtfmphvis.blogspot.com.