Doing asynchronous distributed request/response service calls without WCF
In my previous blog post I hopefully was able to demonstrate how low the entry barrier is to asynchronous remote communication. It´s as easy as hosting a service like this
10 using(var serverSpace = new CcrSpace().ConfigureAsHost("wcf.port=8000"))
11 {
12 serverSpace.HostPort(
13 serverSpace.CreateChannel<string>(t => Console.WriteLine(t)),
14 "MyService");
and connecting to such a service like this:
16 using(var clientSpace = new CcrSpace().ConfigureAsHost("wcf.port=0"))
17 {
18 var s = clientSpace.ConnectToPort<string>("localhost:8000/MyService");
19
20 s.Post("hello, world!");
Under the hood this is net.tcp WCF communication like Microsoft wants you to do it. But on the surface the CCR Space in conjunction with the Xcoordination Application Space provides you with an easy to use asynchronous API based on *Microsoft´s Concurrency Coordination Runtime.
Request/Response
Calling a service and not expecting a response, though, is not what you want to do most of the time. Usually you call a service to have it process some data and return a result to the caller (client). So the question is, how can you do this in an asynchronous communication world? WCF and the other sync remote communication APIs make that a no brainer. That´s what you love them for. So in order to motivate you to switch to an async API I need to prove that such service usage won´t become too difficult, I guess.
What do you need to do to have the server not only dump the message you sent it to the console, but to return it to the caller? How to write the most simple echo service? Compared to the ping service you saw in my previous posting this is not much of a difference. Here´s the service implementation:
11 public static string Echo(string text)
12 {
13 Console.WriteLine("SERVER - Processing Echo Request for '{0}'", text);
14 return text;
15 }
Yes, that´s right. It´s a method with a return value like you would have used it in a WCF service. Just make sure the request and the response message types are [Serializable].
And this is the code to host this service:
19 using(var serverSpace = new CcrSpace().ConfigureAsHost("wcf.port=8000"))
20 {
21 var chService = serverSpace.CreateChannel<string, string>(Echo);
22 serverSpace.HostPort(chService, "Echo");
The only difference is in the two type parameters to CreateChannel(). They specify this is request/response channel. The first type is for the request type, the second for the response type.
When does it start to become difficult, you might ask? Async communication is supposed to be difficult. Well, check out the client code. First it needs to connect to the service:
40 using(var clientSpace = new CcrSpace().ConfigureAsHost("wcf.port=0"))
41 {
42 var chServiceProxy = clientSpace.ConnectToPort<string, string>("localhost:8000/Echo");
This isn´t difficult, or is it? Like before for the one-way ping service the client just needs to specify the address of the service. But of course the local channel needs to match the remote channel´s signature. So the client passes in two type parameters for the request and response message types.
And now the final part: sending the remote service a message and receiving a response. Sure, this looks a bit different from your usual remote function call. But I´d say it´s not all too inconvenient:
44 chServiceProxy
45 .Request("hello!")
46 .Receive(t => Console.WriteLine("Answer from echo service: {0}", t));
You send the request by calling the Request() method with the request message. Then you wait for the response using the Receive() method passing it a continuation. It´s a method to be called once the response arrives at the caller.
Remember: This is asynchronous communication. That means the code after the Receive() will be very likely executed before (!) that of the continuation. So if you want to have a conversation between client and service you´d need to be a bit careful how you model it. Don´t just write channel.Request().Receive() statements one after another. But let that be a topic for a future posting.
For now I hopefully was able to show you how easy request/response communication can be even in the async world. There are of course other ways to accomplish this, but here I wanted to show you the most simple way so you can get started without jumping through too many mental async hoops.
Request/Response in WinForms Applications
This is all well and good – but it won´t work in your WinForms application. Because when the response arrives it arrives on a different thread than the request had been issued from. And that means you can just simply display the result in a WinForms control. That can only be done on the WinForms thread.
I take up the challenge and demonstrate to you this is easy too using the CCR Space. Check out this tiny WinForms application:
Whatever you type into the text box gets sent to the echo service and returned in upper case letters only. You can guess how easy such a service will look:
17 using (var serverSpace = new CcrSpace().ConfigureAsHost("wcf.port=8000"))
18 {
19 serverSpace.HostPort(
20 serverSpace.CreateChannel<string, string>(t => t.ToUpper()),
21 "Echo");
Also the client is as easy as before:
23 using (var clientSpace = new CcrSpace().ConfigureAsHost("wcf.port=0"))
24 {
25 var chService = clientSpace.ConnectToPort<string, string>("localhost:8000/Echo");
But then to let the client form know about the service, the service channel is injected like any other ordinary dependency:
29 Application.Run(new Form1(chService));
…
15 public partial class Form1 : Form
16 {
17 private Port<CcrsRequest<string, string>> echoService;
18
19 public Form1(Port<CcrsRequest<string, string>> echoService)
20 {
21 InitializeComponent();
22
23 this.echoService = echoService;
24 }
And how does the client form interact with the service so there will be no cross-thread calling problem?
26 private void button1_Click(object sender, EventArgs e)
27 {
28 this.echoService
29 .Request(this.textBox1.Text)
30 .Receive(t => listBox1.Items.Add(t),
31 CcrsHandlerModes.InCurrentSyncContext);
32 }
It´s the same pattern as above: call Request() on the channel and append Receive() to this call. However, as the final parameter to Receive() pass in CcrsHandlerModes.InCurrentSyncContext. This advises the client Space to switch to the WinForms synchronization context before executing the response message handler.
Notice, however, the signature of the remote service port. It´s Port<CcrsRequest<TRequest, TResponse>>. By wrapping the application message in a CcrsRequest<,> message the channel.Request().Receive() magic is possible.
Summary
Asynchronous communication certainly is different from synchronous. It takes some time wrap your head around it. And you certainly don´t want to start with it slinging threads yourself. But with a pretty high level async API like the CCR and the CCR Space provides, it´s not beyond the average developers reach, I´d say.
What´s next? I guess you want to know how errors are handled in distributed async scenarios. Let that be the topic of my next posting. Until then trust me: It´s much easier than with WCF ;-)