DuplexBiDirectionalStreaming VS2012 project DuplexBiDirectionalStreaming.zip
Introduction
With WebSocket transport, it’s possible to use streamed transport on the Duplex Callback – something that was previously a technical limitation with WCF but is now possible using this transport. New to .Net4.5 is the NetHttpBinding (and NetHttpsBinding), which is a new standard binding that leverages the WebSocket transport if it makes sense to do so. You can configure it to always use WebSocket transport, but by default, it will only use it for Duplex channel shapes. Why? Well, what this transport gives you is the ability for the server to push data to the client without an incoming request. If your contract is already request-reply, there isn’t much increase in efficiency by using the WebSocket transport. But with duplex, there isn’t necessarily a 1-1 correspondence with requests and replies. Of course, the binding does give you the ability to change this setting.
Requirements
WebSocket transport is only possible on the Windows8 operating system. I’m using Windows8 for this demo with Visual Studio 2012. IIS needs to be installed, with full support for Http Activation and WebSocket Transport. To run the service in IIS from Visual Studio, you need to run Visual Studio as an administrator.
Walk-Through
First, I created a new Console Project in Visual studio. This will be the client. I’ll leave it alone for now, and add a WCF service:
Next, I’ll define the service contract. I want to be able to upload bytes for as long as I want. Also, I want to be able to download bytes at the same time, and have a way to tell the service to stop sending me bytes. I’ll also put something in there to grab logs from the service implementation. I created this in a new class library so it can be shared between the client and service.
- [ServiceContract(CallbackContract = typeof(IClientCallback))]
- public interface IStreamService
- {
- [OperationContract(IsOneWay = true)]
- void UploadStream(Stream stream);
- [OperationContract(IsOneWay = true)]
- void StartDownloadingStream();
- [OperationContract(IsOneWay = true)]
- void StopDownloadingStream();
- [OperationContract(IsOneWay = true)]
- void DownloadLog();
- }
Remember, this is duplex, so all the service operations return void. Anything that needs to go to the client goes through the callback channel, defined by this:
- public interface IClientCallback
- {
- [OperationContract(IsOneWay = true)]
- void ReceiveStream(Stream stream);
- [OperationContract(IsOneWay = true)]
- void ReceiveLog(List<string> log);
- }
Now, to implement the service. To do this, I’ll need some kind of custom stream implementation. For demonstration purposes, I’ll use a stream implementation that sends random bytes and can be turned on or off manually or configured to send for a given duration. In order to not blow MaxReceivedMessageSize buffers, I’ll make a configurable throttle on it. The code for this FlowControlledStream can be found in the attached project.
See the attached project for the full service implementation, but the new thing here is sending a stream on a callback; previously not possible with WCF. Also in there is the implementation of StopDownloadingStream.
- public void StartDownloadingStream()
- {
- log.Add(string.Format("[{0}] StartDownloadingStream Invoked.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond));
- IClientCallback clientCallbackChannel = OperationContext.Current.GetCallbackChannel<IClientCallback>();
- ThreadPool.QueueUserWorkItem(new WaitCallback(PushStream), clientCallbackChannel);
- }
- private void PushStream(object state)
- {
- IClientCallback pushCallbackChannel = state as IClientCallback;
- localStream = new FlowControlledStream();
- localStream.ReadThrottle = TimeSpan.FromMilliseconds(500);
- pushCallbackChannel.ReceiveStream(localStream);
- }
- public void StopDownloadingStream()
- {
- log.Add(string.Format("[{0}] StopDownloadingStream Invoked.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond));
- localStream.StopStreaming = true;
- }
With that finished, I need to modify the Web.config that was generated for me. I want to use WebSocket Transport, so I’ll use the standard NetHttpBinding. By default, netHttpBinding will use WebSocket transport if the contract is duplex, so I shouldn’t have to explicitly add that. But I will increase the MaxReceivedMessageSize to a ridiculous value. So, I replaced the generated System.ServiceModel section with the following:
- <system.serviceModel>
- <behaviors>
- <serviceBehaviors>
- <behavior name="MyServiceBehavior">
- <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
- <serviceDebug includeExceptionDetailInFaults="true"/>
- </behavior>
- </serviceBehaviors>
- </behaviors>
- <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
- <bindings>
- <netHttpBinding>
- <binding name="MyBinding" maxReceivedMessageSize="67108864" transferMode="Streamed">
- </binding>
- </netHttpBinding>
- </bindings>
- <services>
- <service behaviorConfiguration="MyServiceBehavior" name="DuplexService.StreamingService">
- <endpoint address="" contract="CommonArtifacts.IStreamService" binding="netHttpBinding" bindingConfiguration="MyBinding" />
- </service>
- </services>
- </system.serviceModel>
So, after that, I open the service’s Properties page, go to the Web section, and make sure “Use Local IIS Web server” is selected, and I uncheck “Use IIS Express”. When I save that, and allow VS to create the virtual directory, I can browse to the .svc file and get the standard WCF Service help page. Time to work on the Client code.
The goal for the client is to prove that we are doing true bi-directional streaming. To do this my client will execute the following:
1. Invoke a service operation to start downloading a stream.
2. Signal that bytes are being received.
3. Upload a stream for a few seconds.
4. Signal to the service to stop sending bytes to the receiver.
5. Wait for the end of the stream.
If the service can’t process the incoming stream while sending bytes out, then it will deadlock. The program is in the attached zip file, but here’s a snipped version:
- ClientReceiver receiver = new ClientReceiver();
- string address = "http://localhost/DuplexService/DuplexService.svc";
- NetHttpBinding binding = new NetHttpBinding();
- binding.MaxReceivedMessageSize = 64 * 1024 * 1024;
- binding.TransferMode = TransferMode.Streamed;
- DuplexChannelFactory<IStreamService> factory = new DuplexChannelFactory<IStreamService>(new InstanceContext(receiver), binding, address);
- IStreamService client = factory.CreateChannel();
- client.StartDownloadingStream();
- Console.WriteLine("[{0}] Waiting for the receiver to start reading bytes.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond);
- receiver.ReceiveStreamInvoked.WaitOne();
- Console.WriteLine("[{0}] Client Invoking UploadStream, while the receiver is receiving bytes.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond);
- FlowControlledStream uploadStream = new FlowControlledStream();
- uploadStream.ReadThrottle = TimeSpan.FromMilliseconds(500);
- uploadStream.StreamDuration = TimeSpan.FromSeconds(5);
- client.UploadStream(uploadStream);
- Console.WriteLine("[{0}] Client Invoking StopDownloadingStream.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond);
- client.StopDownloadingStream();
- Console.WriteLine("[{0}] Waiting on ReceiveStreamCompleted from the receiver.", DateTime.Now.Minute + ":" + DateTime.Now.Second + "." + DateTime.Now.Millisecond);
- receiver.ReceiveStreamCompleted.WaitOne();
Running it produces the following log:
[45:21.308] Client Invoking StartDownloadingStream.
[45:21.844] Waiting for the receiver to start reading bytes.
[45:22.861] ReceiveStream invoked.
[45:22.862] Client Invoking UploadStream, while the receiver is receiving bytes.
[45:27.873] Client Invoking StopDownloadingStream.
[45:27.873] Waiting on ReceiveStreamCompleted from the receiver.
[45:28.371] ReceiveStream read 725248 bytes.
[45:28.371] Getting the log from the server.
The following are the logs from the server:
[45:21.854] StartDownloadingStream Invoked.
[45:23.866] UploadStream Invoked.
[45:27.873] UploadStream Read 528640 bytes.
[45:27.874] StopDownloadingStream Invoked.
[45:28.373] DownloadLog Invoked.
Comparing the server log and the client log shows that server read bytes from the client for about 5 seconds while sending bytes to the client at the same time. I made sure to set the ReadThrottle to the same amount on both client and service, so the client should read more bytes than the service, which it does.
0 comments