Introduction
Updated on 18th January, 2006 - I received many positive comments about this article and several requests to add some descriptive code to the contents. Based on this feedback, I have reworked on the article to provide a more detailed discussion - with code - of the remoting server and client setup. Please enjoy!
I have spent a lot of time learning how to create and use basic remoting services in my applications. Code Project has been very helpful in this. Although the various articles and tutorials provided me with the information I needed, I never did find a simple, bare bones, use-it today remoting example. This article will attempt to solve that problem.
Once you have mastered the basics presented here, you can refer to the following Code Project articles for more detailed information:
Many other interesting articles are also available at CodeProject.
Goal
To provide you with a very simple, easy to understand implementation of a client/server system using .NET remoting.
Once you understand this code, you can immediately enhance it for use in your own applications. I have left out everything except the minimum code required to setup and test a remoting client and server.
Using the code
The included zip file contains a Visual Studio .NET 2003 solution with three projects. Each project is described below:
- Client - A simple client that will use the server created by the solution. It supports TCP and HTTP connections to the server through a set of
#define
s at the top of MainForm.cs.
- Server - The server application. This application provides two services. I wanted to demonstrate how a single server application can provide multiple, independent services to the clients. Like the client, the server can use TCP or HTTP connections depending on which
#define
is set in the ServerStart.cs file.
- Interfaces - The interfaces project contains an interface for each remote service. I chose interfaces as the method to decouple the client and the server. Since all instantiation of remote server objects occurs through the interfaces, you will not need to rebuild your clients if you change the server implementation.
As configured, you should be able to build the solution and run the client and server on your local machine. To get started:
- Build the solution.
- Manually start the server application.
- Manually start the client, or run the client in the debugger.
- Test the system by clicking the buttons on the client form.
- Observe the messages displayed in the client and server windows.
Let's remote!
The code is simple and well commented. You should look at it to get the big picture. A few items need closer examination.
Server side
On the server side, you need to complete three key steps to get a server working. The steps are:
Create an interface implementation
Let us look at the ICalculatorImplemention
class in the server project. It is small enough so that the whole thing can be shown here:
public class ICalculatorImplementation: MarshalByRefObject,
ICalculator
{
public ICalculatorImplementation()
{
Console.WriteLine(
"ICalculatorImplementation constructor.");
}
#region ICalculator Members
public int Add(int A, int B)
{
Console.WriteLine("Calculator service: Add() called.");
return (A + B);
}
#endregion
}
The most important thing is to inherit from MarshalByRefObject
and to implement the interface you wish to provide to the client. MarshalByRefObject
is the key to remoting in .NET; it provides the support needed to access an object across application domain boundaries - in other words, something running on the other end of a wire, or even in another process on your machine. For a simple service like this, you can pretty much ignore MarshalByRefObject
. It is worth reading about when you are ready for more.
Create an application to host and start the service
Next, we need to create some type of application to hold our service. Any application is suitable; console, Windows and NT services are all useful in various situations. For this example, I used a standard C# console application created with the project wizard. All the work is done in the application entry point, static void Main()
.
Here is an excerpt from ServerStart.cs. I have removed some of the code and comments for clarity:
[STAThread]
static void Main(string[] args)
{
ChannelServices.RegisterChannel(chan);
Type iCalc = Type.GetType(
"InterfaceTest.Server.ICalculatorImplementation");
RemotingConfiguration.RegisterWellKnownServiceType(
iCalc,
"ICalcEndPoint",
WellKnownObjectMode.Singleton
);
}
I will look at the connection setup in a minute, so for now that section of the code has been removed. Once the connection (TCP or HTTP) has been setup, you register then configures the service. The code above is pretty much boiler plate so you can substitute your own interface implementations into the lines above.
When you get the type of your interface implementation, you need to include the complete namespace as well as class name to get the correct type return. In this case, it is InerfaceTest.Server.ICalculatorImplementation
.
The RemotingConfiguration
class is a static
class that provides all the methods you need to configure your remoting service.
When you move beyond this sample, you will want to investigate the WellKnownObjectMode
enumeration. This enumeration controls how your service is started and shared among clients.
Singleton
mode indicates that a single instance of ICalculatorImplemention
will be started and it will be shared among all the clients making requests. You could use WellKnownObjectMode.SingeCall
if you want your service to be stateless and provide an instance per client call.
Note: You can host multiple services in a single application. The provided sample code demonstrates this by hosting the ICalculatorImplemention
and ITestImplementation
classes as services. The two services share the same channel, port and communications method (TCP or HTTP). Each service URL differs by the end point.
For example, the calculator service URL in the sample code is:
TCP://LocalHost:65101/ICalcEndPoint
And the test service URL is:
TCP://LocalHost:65101/ITestEndPoint
Setup a TCP or HTTP communications channel
Remoting can use HTTP or TCP for communications. HTTP is very simple to setup. Here is an example for a remoting server:
HttpChannel chan = new HttpChannel(65101);
That's it! Create an HttpChannel
instance and set a port number - in this case, 65101.
A TCP channel is more complex to setup on the client and server. The performance benefits might be worth it depending on your application needs. TCP remoting is faster than HTTP remoting.
Here is how you create a TCP channel on the server. See ServerStart.cs for all the details:
BinaryClientFormatterSinkProvider clientProvider = null;
BinaryServerFormatterSinkProvider serverProvider =
new BinaryServerFormatterSinkProvider();
serverProvider.TypeFilterLevel =
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props["port"] = 65101;
props["typeFilterLevel"] =
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
TcpChannel chan =
new TcpChannel(props, clientProvider, serverProvider);
Although there are many more steps required for TCP setup, you can use this code as boiler plate. Just replace the props["port"] = 65101
with a port of your choice.
Client side
On the client side, when you want to gain access to a remote service you have to do two things. They are:
Create a channel to the remote service
Like the server, you need to create a TCP or HTTP channel. The steps are almost identical.
For HTTP the statement: HttpChannel chan = new HttpChannel(0);
is all that is required. Notice that you don't specify a port number. Since this is a client, it is not necessary.
The TCP code is also similar to the server. Here is an excerpt from MainForm.cs in the client project:
url = @"tcp://LocalHost:65101/";
BinaryClientFormatterSinkProvider clientProvider =
new BinaryClientFormatterSinkProvider();
BinaryServerFormatterSinkProvider serverProvider =
new BinaryServerFormatterSinkProvider();
serverProvider.TypeFilterLevel =
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props["port"] = 0;
props["name"] = System.Guid.NewGuid().ToString();
props["typeFilterLevel"] =
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
TcpChannel chan =
new TcpChannel(props, clientProvider, serverProvider);
Unlike the server code, this code creates BinartClient
and BinaryServer
sink providers. Like the HTTP example, it sets the port number to 0.
Register and connect
Once you have created a communications channel you register the channel, setup remote access to the server using MarshalByRefObject
and create a local instance of the remote class you wish to use. This excerpt demonstrates getting and using an instance of ICalculatorImplementation
:
ChannelServices.RegisterChannel(chan);
MarshalByRefObject calc =
(MarshalByRefObject)RemotingServices.Connect(
typeof(Interfaces.ITest), url + "ICalcEndPoint"
);
calcReference = calc as Interfaces.ICalculator;
int test = calcReference.Add(10,20);
Console.WriteLine("Add(10,20) returned " + test.ToString());
Once calcReference
is created, you use it exactly like a regular instance of ICalculatorImplementation
.
Conclusion
This is the simplest remoting example I was able to create. The code is well commented and hopefully you can use it as is while you become more familiar with remoting concepts. The sections above highlight the most important remoting steps. Below I have listed a few key points and areas of interest that you should also think about when dealing with remoting.
Interfaces
If you are not familiar with using interfaces, I suggest you review the topic. Interfaces are the mechanism I use to provide remote services to the client and as far as I know, it is the most common method in use.
The interface allows you to treat a remote instance of some class as if it was a local object; it also decouples the client from the server.
You will notice that I have created a separate interface class library to hold all interfaces shared by the client and server applications. Simply put, this means that changing the implementation of the client or server does not force you to rebuild the other.
Interfaces also provide transparency. Once you create a communications channel to your server and create an interface instance of the service you want to use, the use of the remote object is completely transparent to your client's code.
HTTP versus TCP connections
As you examine the code, you will see that, by far, the HTTP connection is the simplest to setup. This does come at some cost. The HTTP transport is much slower than the TCP/Binary connection. The advantage is that HTTP will work through a firewall and provide built-in security mechanisms if you need them. It is also compatible with non .NET code.
The TCP connection is more complex, but faster and only compatible with other .NET applications.
There is a wealth of information available on Code Project and other sites about TCP and HTTP remoting. Once you have the basics down you should spend some time reading these articles. See the links I have provided in the Overview section of this article.
Error handling
I have left out all error handling to simplify the code. In a real world application you should, at minimum, add some exception handling to the client.
Many real world clients will wrap every call to a remote method in a try
/catch
block. I do not generally do this. Instead, I place a try
/catch
around the client code that creates a reference to the service.
My servers always have an Init()
or Test()
method that the client can use to verify that the communication channel and the service are working. I wrap my Test()
call in a try
/catch
and report any exception to the user.
I find that this technique makes the code easier to read and improves performance. However, my way is not the best way so use it at your own risk.