Simplifying Communications, Part 2
The Story Continues, Again…
In my last post (here) I modified the server in preparation for building a communications client. This involved adding a new method to handle “generic” messages. This method took in an action name and a request message, and then returns a response.
Now it’s time to build on this and start putting together a communications client.
New vs. Old
The first question is where will the communications code go? At the moment there are two choices – Remote or a new project.
Remote is currently the communications library within CruiseControl.Net, but it also handles a number of other items (e.g. exceptions, events, interfaces, etc.) Additionally it depends on the NetReflector library.
Adding a new project would mean potential duplication, although it would be smaller!
In the end I decided to add a new project in the same folder and link to the existing files. The new project has the same namespace (although a different assembly name). Additionally I modified a couple of files to include compiler directives (#if…#endif) to exclude the NetReflector classes – this removes the dependency on NetReflector.
Now I can use the same classes, but without all the extra baggage. Additionally I can include the new communications classes in Remote later on if I think they are necessary for the main server.
The Design
First off, I want a generic interface to the communications. This will be a single class that handles all communications calls – this way the application does not need to know whether it is HTTP, .NET Remoting or something else.
Secondly, there will be a number of “transport” channels. Each one of these channels contains the actual communications details (e.g. HTTP, .NET Remoting, etc). When the application wants a new client, it instantiates the channel, start a new client and links the two together.
Finally, to simplify the initial set-up, there will be a factory class to do the above step – plus handle some extra steps (auto-detection of the protocol, etc.)
The following diagram shows the various components:
Implementation
The code consists of five classes and one interface. These are:
- IServerConnection
- RemotingConnection
- HttpConnection
- CruiseServerClient
- CommunicationsException
- CruiseServerClientFactory
Most of these should be straight-forward
IServerConnection
IServerConnection defines the channel to the client. It has one method – SendMessage() and one property – ServerName. ServerName is the name of the server that the connection is for. This is used to set the Server property on the request (since this required for web connections). SendMessage() does the actual sending of the message. The input arguments are the action and a ServerRequest, and it returns a Response.
This is all it does, it doesn’t need to perform any validation of the response, as this will be handled by the client.
RemotingConnection and HttpConnection
These are two implementations of IServerConnection – one for .Net Remoting and the other for HTTP. They handle connecting to the server, converting the messages into wire formats and then converting back again.
RemotingConnection is very simple – the only fancy logic in it is generating the URI for the remote server (and even so, is still very simple).
HttpConnection does a bit more work as it converts the request message to XML and then has to convert the response from XML. Internally it uses a WebClient instance, and just calls UploadValues() to send the data (this is a POST request).
CruiseServerClient
This is the biggest of the classes. Each available action on the server has a matching method here. CruiseServerClient takes the incoming arguments and puts them into a request message (of the correct type). This message then gets passed onto the IServerConnection instance, which sends it to the server and returns a response.
When the response is returned CruiseServerClient validates the response. If the response has a status of failed, it will throw an exception (see CommunicationsException below), otherwise it will return the data items (if any).
This class has similar helper methods to what was added to CruiseManager – GetServerRequest(), GetProjectRequest() and ValidateResponse() – to help with this processing.
It also allows the calling application to set a target server. If a target server is not set, then the server name from the server connection is used. Otherwise the target name is used. This is required for HTTP calls where the target server and the connection server can be different.
CommunicationsException
A CommunicationsException is thrown whenever the remote server returns a failure response. This encapsulates the errors and makes it easier for the calling application to differentiate between errors from the remote server and errors on the client side.
CruiseServerClientFactory
The final class is CruiseServerClientFactory. This provides helper methods for starting a new client. Currently there are helper methods for starting a .NET Remoting connection, an HTTP connection and a generic connection.
The generic connection will attempt to detect the connection type from the URI. If the URI starts with http://, then an HTTP connection is started. If the URI starts with tcp:// then a .NET Remoting connection is started. Anything else throws an exception.
This class can be bypassed and the calling application can start its own IServerConnection and wire it to a CruiseServerClient. But this class will help reduce coding in most situations.
An Example
Now that I’ve built the library, now is it used?
First, add the assembly ThoughtWorks.CruiseControl.Communications to the calling project. Just remember that the namespace for this library is actually ThoughtWorks.CruiseControl.Remote – not Communications.
Then instantiate a client. The following shows a few different approaches:
// Generate an HTTP client that targets another serverCruiseServerClient client = CruiseServerClientFactory.GenerateHttpClient("http://buildserver/ccnet", "actualserver");// Generate a .NET remoting client explicityCruiseServerClient client = CruiseServerClientFactory.GenerateRemotingClient("buildserver");// Generate a .NET remoting client implicity (by auto-detection)CruiseServerClient client = CruiseServerClientFactory.GenerateClient("tcp://buildserver:21234");
To then call the server is as simple as the following:
client.ForceBuild("ProjectName");
And that’s it. Any errors will be raised as exceptions, so make sure there is a try-catch block somewhere around the code.
Where To From Here?
What I have done so far is the basic version of the communications client, which covers most of what the current code can do. Now it’s time to start expanding it to cover other scenarios, e.g. security, handling proxies, other communications channels, etc.
Also, I haven’t covered it in this post, but I have updated my CCCmd (command-line remote client) interface, so next step is to document it and include it in the main code base. I’ll also look at CCTray, the dashboard and the triggers and publishers that connect to other servers.
Another area is to make custom versions of this library for different platforms – e.g. Silverlight, Compact framework, etc. However for Silverlight it needs an asynchronous model, and I haven’t figured out what is the best way to do that yet.
Finally, I’m toying with the idea of making the communications library backwards compatible (e.g able to connect to 1.4.3 servers or earlier). Unfortunately this will require a lot more work as the older versions don’t have the generic message handler
So this one might go in the too hard basket.