Вы находитесь на странице: 1из 34

All you need to know about .

NET Remoting
Author: Olexandr Malko
Date: 09/29/2008

Introduction
.NET Remoting is available since beginning of .NET intoduction. It’s time to get to
know it well eventually. I hope this topic will help you with this. This document has
many samples attached. It was decided not to overload one project with all features at
once. Even though only couple lines should be changed for switching from one final
application to another, there will be a separate solution to avoid text like “//uncomment
this to gain that”. All samples are introduced with Visual Studio 2003 solutions. So, you
should be able to open them with VS2005 and VS2008.

Sometimes objects on pictures won’t have numbers even though those will be referred
as “second” or “fifth”. I will use such numbering with rules to count from top to bottom
and from left to right.

Content
1. What is .NET Remoting?
2. Simple project to show .NET Remoting
3. Configuration file and configuration in code
4. Types of remote object activation
4.1. Server Side Object Activation. Singleton
4.2. Server Side Object Activation. SingleCall
4.3. Client Side Object Activation
5. What is Lease Time? How to control it?
6. Hide Implementation from Client. Remoting via Interface exposure.
7. Custom types as parameters and return values
8. Custom exceptions through remoting channel
9. Events in .NET Remoting
10. Asynchronous calls
11. Several Services in one Server. Several Server links from single Client app
12. Summary

1. What is .NET Remoting?


“.NET Remoting” are means in .NET Framework for 2 applications to interact over
network (e.g. withing 1 PC, within LAN or even worldwide). Also, in .NET we have
ability to run several Application Domains in one process. .NET Remoting is the way to
interact between these Domains.
There are 2 common types of protocols used in .NET Remoting: tcp for binary stream
and http for SOAP stream. Here in this article all samples will use binary channels, tcp. It
requires less traffic load and better performance as there is no overhead with XML
parsing. For our production projects it is a big plus.

As usual for distributed applications, there is a Server and a Client application. In


.NET Remoting we can have as many clients as we want, and all those Client
applications can use the same Server. .NET remoting is not just a socket with low level
methods. It is framework where you can work remotely with classes with ability to
invoke methods, to pass custom types as parameters and get them as return values, to
throw Exceptions between processes, to pass Callback delegates and have them invoked
later remotely, to do asynchronous calls.

2. Simple project to show .NET Remoting


Remoting interaction requires:

1) service type description that is available for both points on interaction


2) point #1 – host (e.g. Server) that holds the instantiated remoting object of our
service type
3) point #2 – client application that can connect to Server and use remoting object

Now, let’s take a look at picture below. You can see two separate processes. Server is
holding a real instance of MyService. This instance can be used by other processes
over .NET Remoting. Client process is not instantiating the instance of MyService. It just
has some transparent proxy. When Client application invokes methods of MyService
proxy, the proxy redirects those calls to .NET Remoting Layer in Client process. That
remoting layer knows where to send such call – so, call goes over network (e.g. over
.NET Remoting channel) right to our remoted Server process. After that Remoting layer
on Server side knows if it should use already existing instance of MyService or create
new one. It depends on type of activations. Activation types can be configured in *.xml
config file or through code. All this will be described later in this article.
You may find “Simple Remoting” solution in downloads. It consists of three core
projects. Almost all samples in this article will have them:

1) ONXCmn - class library with definition of MyService type


2) ONXServer – executable console application that hosts MyService service.
3) ONXClient – executable console application that shows how to use remoted
MyService sevice.

You can start as many Client applications as you want. All of them will be served by
single Server application. You cannot start several Servers at the same time though. This
is because there is a port to listen for remote Client applications. You cannot initiate
several socket listeners on the same network card and the same port.

Also, I would like to pay your attention at Log and Utils classes. They will be used
with all samples. You will find Log useful to print timestamp with each print out. Also, it
prints id of current thread – so we can easily see if the same thread was used for group of
actions or not. As for Utils class, it dumps information about all registered remoting
service and client types. It helps you to catch some misconfiguration in case something is
not working:

static void Utils.DumpAllInfoAboutRegisteredRemotingTypes()

Collapse
public class MyService : MarshalByRefObject
{
public MyService()
{
Log.Print("Instance of MyService is created");
}

public string func1()


{
Log.Print("func1() is invoked");
return "MyService.func1()";
}
}

Here we describe our remoting type – MyService. It must be derived from


MarshalByRefObject. This parent class tells our MyService class not to be sent by value
– it is referred by reference only. Our MyService has only one service method – “string
func1()”. Whenever we invoke “func1()” we print log message and return value. As you
may guess, we instantiate MyService object in Server application and use it from Client
application. That is why we should expect log message to appear in Server console and
not in Client one. The same about MyService() constructor. Log message about object
creation should appear in Server console.

Now, Server class:

Collapse
class MyServer
{
[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXServer.exe.config");
Utils.DumpAllInfoAboutRegisteredRemotingTypes();

Log.WaitForEnter("Press EXIT to stop MyService host...");


}
}

It might surprise you if you really see .NET Remoting for first time. There is nothing
specific and complex here. Why is it working? The
“Utils.DumpAllInfoAboutRegisteredRemotingTypes()” is simply invoked to print
registered .NET services. The “Log.WaitForEnter(..)” is just user prompt to press
ENTER to close our console application. So, the only line of code that really turns our
regular console application into .NET Remoting Server is
“RemotingConfiguration.Configure("ONXServer.exe.config")”. This method reads *.xml
file and has enough information from there to start socket listener on some port and to
wait for requests from remote Client applications! This is nice approach as you can
change behavior of your application without need to change and recompile our code.
Now, let’s take a look at this configuration file:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
objectUri="MyServiceUri"
mode="SingleCall" />
</service>
<channels>
<channel ref="tcp" port="33000" />
</channels>
</application>
</system.runtime.remoting>
</configuration>

Remoting is configured inside


<configuration><system.runtime.remoting><application> section. This is true for both
Server and Client configuration (yes, Client is also configured through *.xml file). For
Server we have <service> section that might have one or more <wellknown> sections.
This wellknown section is the place where you describe your service to be available for
Client applications. There are 3 attributes for it:
1) full type description – describes, what type to instantiate when we get request for
this welknown type from Client. Full type value consists of type name with full
namespace path and after comma there is the name of assembly where this type is.
2) objectUri – this is unique name that Client application will be requesting by.
Client application usually requests service by “URI” and not by direct type name. You
will know why when you get to “6. Hide Implementation from Client. Remoting via
Interface exposure” topic.
3) This parameter may be either “SingleCall” or “Singleton”. In case of “SingleCall”
every method call that comes from any Client is served by newly created instance of
MyService. In “Singleton” configuration ALL calls from ALL client applications are
served by single instance of MyService object.

If you have several services that should be registered in our application, list
“wellknown” sections one after another inside “service” section.

Also, beside from “service” section there is “channels” section. Here we might have
several channels defined. In our sample we have only “tcp” channel defined. It will be
listening on port 33000.

Now, let’s take a look at Client configuration:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
url="tcp://localhost:33000/MyServiceUri" />
</client>
</application>
</system.runtime.remoting>
</configuration>

You may notice pretty much similarity between Server and Client configurations. The
difference is that in Client configuration we have “<client />” section instead of “<service
/>”. It makes application understand that when we create instance of MyService we
actually want to request this class remotely. Also, wellknown section has “url” attribute
that will connect to “localhost” machine to port 33000 and request named service with
URI MyServiceUri. Attribute “type” says application to use this remoting whenever
Client application code tries to instantiate the MyService object on client side. So, no
actual instance of MyService is created in Client application. We only create Proxy that
knows where to send our call requests whenever we call some method.

And finally here is Client application:

Collapse
class MyClient
{
[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXClient.exe.config");
Utils.DumpAllInfoAboutRegisteredRemotingTypes();

MyService myService = new ONX.Cmn.MyService();


Log.Print("myService.func1() returned {0}", myService.func1());

Log.WaitForEnter("Press ENTER to exit...");


}
}

As you see it is as simple as Server console application. You simply call


“RemotingConfiguration.Configure("ONXClient.exe.config")” to register our MyService
type correctly. Then you dump information about all remote types that were registered so
far. After that you create “instance” on MyService. As you understand now, there will be
only transparent proxy created. And then you call “MyService.func1()” method. This call
will go to Server application, get return value from there, deliver it to Client application
and print in our log on Client side.

Here is what we get in Server and in Client consoles for our sample:

Collapse
SERVER:

[1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -


(BEGIN)---------
[1812] [2008/10/05 21:30:15.595] WellKnownServiceTypeEntry:
type='ONX.Cmn.MyService, ONXCmn'; objectUri=MyServiceUri;
mode=SingleCall
[1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -
(END) ---------
[1812] [2008/10/05 21:30:15.595] Press EXIT to stop MyService host...
[5068] [2008/10/05 21:30:20.876] Instance of MyService is created
[5068] [2008/10/05 21:30:20.876] func1() is invoked
Collapse
CLIENT:

[7388] [2008/10/05 21:30:20.736] ALL REGISTERED TYPES IN REMOTING -


(BEGIN)---------
[7388] [2008/10/05 21:30:20.798] WellKnownClientTypeEntry:
type='ONX.Cmn.MyService, ONXCmn'; url=tcp://localhost:33000/MyServiceUri
[7388] [2008/10/05 21:30:20.798] ALL REGISTERED TYPES IN REMOTING -
(END) ---------
[7388] [2008/10/05 21:30:20.892] myService.func1() returned
MyService.func1()

You can see that Instance of Service is created in Server application even though we
have “new MyService()” in Client application code!

3. Configuration file and configuration in code


All configurations that were performed for our “Simple Remoting” solution can be
done through code without need to have additional *.xml configuration file. Sometimes it
is easier to have it in code, but it makes harder to do quick adjustments or modifications
to configuration. That is why in our article I will continue to use *.xml files as this is
easier to read also. But for security or any other reasons still you may store configuration
in some files or in database, and then teach your application to read that configuration
data and register remoting types inside of your code if you wish.

As a brief example here is code that makes the same configuration as we have for our
Client application in “Simple Remoting” sample in previous topic:

Collapse
//RemotingConfiguration.Configure("ONXClient.exe.config");
RemotingConfiguration.RegisterWellKnownClientType(
typeof(MyService),
"tcp://localhost:33000/MyServiceUri");

You may want to check MSDN to get more details on .NET Remoting configuration in
code.

4. Types of remote object activation


There are 3 types of activation of remote objects: 2 types of Server Side Activation and
1 type of Client Side Activation:
1) Server Side Singleton - Object is created on Server when first request comes from
one of Client applications. Nothing happens on a Server when you "create" instance in
your Client application. Server acts only when Client application invokes first method of
remote object. In Singleton mode all Client applications share SINGLE instance of
remote object that is created on Server. Even if you create several objects in Client
application, still they use the same single object from Server application.

2) Server Side SingleCall - Object is created for EACH method call. So, it does not
matter how many Client applications are running. Every method call from any Client
application has this life-cycle:

- Server creates new instance of remote object


- Server invokes requested method against newly created remote object
- Server releases the remote object. So, now the remote object is available for
Garbage Collection.

3) Client Side Activation - Object is created in Server application with every "new"
operator that is in Client application. Client application has full control over this remote
object and does NOT share it with other Client applications. Also, if you create 2 or more
remote objects in your Client application - yes, there will be created the exact number of
remote objects in Server application. After that you may work with each instance
individually as you would do without .NET remoting involved. The only issue here is
Lease Time that might destroy your remote object on Server application earlier than you
expect. See “5. What is Lease Time? How to control it?”

For Server Activation Object you will need to register “well known type”. For Client
Activation Object you will need to register “activated type”. Let take a look at each type
of activation closer.

4.1. Server Side Object Activation. Singleton


In this type of activation no object is created on a Server until first call comes from one
of Clients. It does not matter how many calls are coming after object is created. It does
not matter how many Client applications are trying to use our Server object – all such
calls are directed to single remote object, e.g. “Instance of MyService” on a picture
below.
Also, I would like to pay your attention that even though you request several instances
of MyService in Client application (see myService1 and myService2 on picture) those 2
variables will still point to single TransparentProxy in Client process. This is because for
“wellknown” type one proxy per process is enough with either “Server Activation
Object” model.

If Lease Time is expired, Singleton might be destroyed on Server. In this case with
new request from Client application new Singleton is created and is used in the same way
– e.g. Single object for all Clients requests. See “5. What is Lease Time? How to control
it?”

To use this type of activation you should configure server with well-known type like
this:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
objectUri="MyServiceUri"
mode="Singleton" />
</service>
<channels>
<channel ref="tcp" port="33000"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>

And client configuration like this:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
url="tcp://localhost:33000/MyServiceUri" />
</client>
</application>
</system.runtime.remoting>
</configuration>

As for sample, locate “SAO Singleton” solution. With client code:

Collapse
class MyClient
{
[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXClient.exe.config");
Utils.DumpAllInfoAboutRegisteredRemotingTypes();
string result;

//create myService1
Log.WaitForEnter("1) Press ENTER to create Remote Service...");
MyService myService1 = new MyService();
Log.Print("myService1 created. Proxy? {0}",
(RemotingServices.IsTransparentProxy(myService1)?"YES":"NO"));

//query myService1.func1()
Log.WaitForEnter("2) Press ENTER to query 1-st time...");
result = myService1.func1();
Log.Print("myService1.func1() returned {0}", result);

//query myService1.func2()
Log.WaitForEnter("3) Press ENTER to query 2-nd time...");
result = myService1.func2();
Log.Print("myService1.func2() returned {0}", result);

//create myService2
Log.WaitForEnter("4) Press ENTER to create another instance of
Remote Service...");
MyService myService2 = new MyService();
Log.Print("myService2 created. Proxy? {0}",
(RemotingServices.IsTransparentProxy(myService2)?"YES":"NO"));

//query myService2.func1()
Log.WaitForEnter("5) Press ENTER to query from our new Remote
Service...");
Log.Print("myService2.func1() returned {0}", myService2.func1());

Log.WaitForEnter("Press ENTER to exit...");


}
}

We get

Collapse
SERVER:

[4424] [2008/10/05 22:31:52.369] Instance of MyService is created,


MyService.id=1
[4424] [2008/10/05 22:31:52.369] func1() is invoked, MyService.id=1
[4424] [2008/10/05 22:31:53.056] func2() is invoked, MyService.id=1
[4424] [2008/10/05 22:31:54.556] func1() is invoked, MyService.id=1
Collapse
CLIENT:

>1) Press ENTER to create Remote Service...


[7076] [2008/10/05 22:31:51.416] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...


[7076] [2008/10/05 22:31:52.400] myService1.func1() returned
MyService#1.func1()

3) Press ENTER to query 2-nd time...


[7076] [2008/10/05 22:31:53.056] myService1.func2() returned
MyService#1.func2()

4) Press ENTER to create another instance of Remote Service...


[7076] [2008/10/05 22:31:53.650] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...


[7076] [2008/10/05 22:31:54.556] myService2.func1() returned
MyService#1.func1()

Here in this sample only 1 MyService instance was created on Server side. It served all
3 calls even though 2 calls came from myService1 and 1 call from myService2.
4.2. Server Side Object Activation. SingleCall
As for creation of object on a Server side, we have the same situation – no object is
created with “new MyService()” on a Client application. But as soon as you invoke ANY
method in Client code, the invocation is directed to Server application. The .NET
Remoting creates NEW instace for each such query. As you can see on a picture below,
there were 5 invocations sent from 2 Client applications. It made .NET Remting create 5
instances of MyService. Each of instances was used only once – for single call. Pay
attention that “Instance of MyService” #3 and #5 were for the same created with the same
call of “myService1.func1()”, but still .NET Remoting created a separate instance for
each call.

Single Trasparent Proxy is created for all MyService objects in Client application (see
second Client).
To use this type of activation you should configure server with well-known type like
you did for SSA Singleton. The only difference is that mode should be set to
“SingleCall”:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
objectUri="MyServiceUri"
mode="SingleCall" />
</service>
<channels>
<channel ref="tcp" port="33000"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>

Client configuration is absolutely the same as for SSA Singleton:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
url="tcp://localhost:33000/MyServiceUri" />
</client>
</application>
</system.runtime.remoting>
</configuration>

As for sample, locate “SAO SingleCall” solution.

Collapse
SERVER:

>[3472] [2008/10/05 22:21:57.662] Instance of MyService is created,


MyService.id=1
[3472] [2008/10/05 22:21:57.662] func1() is invoked, MyService.id=1
[3472] [2008/10/05 22:22:00.381] Instance of MyService is created,
MyService.id=2
[3472] [2008/10/05 22:22:00.381] func2() is invoked, MyService.id=2
[3472] [2008/10/05 22:22:04.849] Instance of MyService is created,
MyService.id=3
[3472] [2008/10/05 22:22:04.849] func1() is invoked, MyService.id=3
Collapse
CLIENT:

1) Press ENTER to create Remote Service...


[7252] [2008/10/05 22:21:54.209] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...


[7252] [2008/10/05 22:21:57.693] myService1.func1() returned
MyService#1.func1()

3) Press ENTER to query 2-nd time...


[7252] [2008/10/05 22:22:00.381] myService1.func2() returned
MyService#2.func2()

4) Press ENTER to create another instance of Remote Service...


[7252] [2008/10/05 22:22:02.756] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...


[7252] [2008/10/05 22:22:04.849] myService2.func1() returned
MyService#3.func1()

In our sample the “id” is the unique id of each instance of MyService object that is
created on Server side. As you can see, we have as many instances created in SERVER
app as number of calls (e.g. 2 calls for myService1 and 1 call for myService2 – in sum we
got 3).

Also, according to timestamps you may conclude that MyService is created right with
“func#()” call.

4.3. Client Side Activation


This is pretty nice type of activation to have as it makes you to work with object like
“there is no remoting at all”. You have distinct instance of object created for each of your
“new” operator. Your instance is created remotely on a Server and it is never shared with
other Client applications. So, for Client application this type of activation is very close to
use case when you create object is a regular way, without .NET Remoting involved.
myService, myService1 and myService2 are real 3 objects that were instantiated on
Server and transparently used by Client applications. Pay attention that among 3 types of
activation described this is the only one where we have more than one proxy created for
Client #2. This is because number of proxies will be equal to number of remote objects
that your Client application has created so far.

To use this type of activation you should configure server with “<activated />”section,
not with well-known type:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<activated type="ONX.Cmn.MyService, ONXCmn" />
</service>
<channels>
<channel ref="tcp" port="33000"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>

Client configuration also uses “<activated />”section:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client url="tcp://localhost:33000">
<activated type="ONX.Cmn.MyService, ONXCmn" />
</client>
</application>
</system.runtime.remoting>
</configuration>

Pay attention that “url” parameter is specified in “<client />” section with this type of
activation. There is no need for objectURI here as .NET Remoting will know what type
to use.

Also, Leasing expiration is involved in this activation type. See

As for sample, locate “CAO” solution. I won’t present text of Client code as it is the
same as for 2 tests from above. The only change is configuration that controls the type of
activation. Now, we get

Collapse
SERVER:

>[6956] [2008/10/05 22:38:47.075] Instance of MyService is created,


MyService.id=3
[6956] [2008/10/05 22:38:49.918] func1() is invoked, MyService.id=3
[6956] [2008/10/05 22:38:52.559] func2() is invoked, MyService.id=3
[6956] [2008/10/05 22:38:54.965] Instance of MyService is created,
MyService.id=4
[6956] [2008/10/05 22:38:57.231] func1() is invoked, MyService.id=4
Collapse
CLIENT:

1) Press ENTER to create Remote Service...


[2280] [2008/10/05 22:38:47.090] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...


[2280] [2008/10/05 22:38:49.918] myService1.func1() returned
MyService#3.func1()
3) Press ENTER to query 2-nd time...
[2280] [2008/10/05 22:38:52.559] myService1.func2() returned
MyService#3.func2()

4) Press ENTER to create another instance of Remote Service...


[2280] [2008/10/05 22:38:54.965] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...


[2280] [2008/10/05 22:38:57.231] myService2.func1() returned
MyService#4.func1()

In this sample MyService instances created on Server side right at the time that Client
application code hits “new MyService()” command. You can see some delay in creation
of myService1 (15 ms). This is because this was first call from our Client application to
Server. It required establishing physical network connection between our applications
and did all other hidden .NET Remoting handshakes. As for myService2 it was created
right at the same millisecond. Also, as you can see, each of our myService# on Client
side was served with corresponding MyService instance on Server side.

5. What is Lease Time? How to control it?


In case of interprocess coordination Server does not know if Client is still going to use
object or not. The easiest way for remoting object in Server application is to count how
much time has passed since object was created or since last time when some Client used
the object (e.g. made some method invocation).

There are means to set lease time through configuration files (showed below) and
through code:

Collapse
using System;
using System.Runtime.Remoting.Lifetime;
...
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(30);
LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(30);
LifetimeServices.LeaseManagerPollTime = TimeSpan.FromMinutes(1);

LeaseTime – is initial lease time span for AppDomain.


RenewOnCallTime - the amount of time by which the lease is extended every time
when call comes in on the server object.
LeaseManagerPollTime - the time interval between each activation of the lease
manager to clean up expired leases.

See MSDN for details.

Here is how it works. For each server object we can get CurrentLeaseTime time from
Lease helper. This CurrentLeaseTime is how much time left for object to live. There is
a .NET Remoting LeaseManager that wakes up periodically and checks every available
server object in Server application. With each check it reduces the CurrentLeaseTime for
each checked object. If object is expired then its reference is removed and that object is
marked for GC to be collected. Every time when remote call comes for server object, this
object’s CurrentLeaseTime is set to RenewOnCallTime time span.

Take a look at “Lease Time” solution. As you can see it uses Server Activation in
Singleton mode. It should make all Clients and all MyService objects in Clients’
application use the same instance of MyService that is on Server.

But we configured lease time to be only 5 seconds:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
...
<lifetime
leaseTime="5S"
renewOnCallTime="5S"
leaseManagerPollTime="1S" />
</application>
</system.runtime.remoting>
</configuration>

It makes .NET Remoting mark remoted object for garbage collection until 5 seconds
passed with no queries from any Client:

Collapse
1) Press ENTER to create Remote Service...
[5044] [2008/10/01 14:17:47.442] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...


[5044] [2008/10/01 14:17:48.552] myService1.func1() returned
MyService#4.func1()

3) Press ENTER to query 2-nd time...


[5044] [2008/10/01 14:18:03.334] myService1.func2() returned
MyService#5.func2()

4) Press ENTER to create another instance of Remote Service...


[5044] [2008/10/01 14:18:04.099] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...


[5044] [2008/10/01 14:18:04.990] myService2.func1() returned
MyService#5.func1()

See “3)” in output. As you can see, we were waiting too long (e.g. >5 seconds) before
we invoked query 2-nd time. It made Server forget about MyService#4 and create new
one – MyService#5. After that in “5)” we invoked func1() within 2 seconds and it was
using MyService#5 as it was not expired yet on Server side.
Here we start our Client application again and press ENTER continuously with no
delays. As you can see, all three invokes use the same MyService instance:

Collapse
1) Press ENTER to create Remote Service...
[5380] [2008/10/01 14:30:39.355] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...


[5380] [2008/10/01 14:30:39.589] myService1.func1() returned
MyService#6.func1()

3) Press ENTER to query 2-nd time...


[5380] [2008/10/01 14:30:39.652] myService1.func2() returned
MyService#6.func2()

4) Press ENTER to create another instance of Remote Service...


[5380] [2008/10/01 14:30:39.808] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...


[5380] [2008/10/01 14:30:39.980] myService2.func1() returned
MyService#6.func1()

We can also make our remoting object never expire. In order to do so we will need to
override one of the MarshalByRefObject methods and make it return “null”:

Collapse
public class MyService : MarshalByRefObject
{
...

public override object InitializeLifetimeService()


{
return null;
}
}

If you add such override to LeaseTime solution, you will see that even though we waited
too long and have <lifetime> parameter specified in configuration – our MyService is not
expired and reused for all calls:

Collapse
2) Press ENTER to query 1-st time...
[3056] [2008/10/01 14:36:51.455] myService1.func1() returned
MyService#1.func1()

>3) Press ENTER to query 2-nd time...


[3056] [2008/10/01 14:37:14.049] myService1.func2() returned
MyService#1.func2()

There is also “sponsoring” mechanism that allows customizing the lease time
according application needs. You can read “sponsors” topic in MSDN to get more
information on this.
6. Hide Implementation from Client. Remoting via
Interface exposure
It is not always a good idea to expose to the world the implementation of your
remoting object. This is due to security reasons and due to size of assembly that has
complex implementation. Also, implementation can use some other assemblies that you
would not want to deploy to client computers. In this case it is a good idea to split our
MyService class into:

1) interface that we will expose to client


2) and to the implementation itself.

At this point we can put out types into separate assemblies and deliver only small part
to client computer:

Then during delivery we need to put only small part of product on Client computers:
You may find “Hide Implementation from Client app” solution to see how it is
implemented. The idea is to request remote type by Uri and cast returned object to
interface. On a server side such Uri request will instantiate our real implementation that is
defined in ServerLib assembly.

Server configuration:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Server.MyService, ONXServerLib"
objectUri="MyServiceUri"
mode="Singleton" />
</service>
<channels>
<channel ref="tcp" port="33000"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>

Client configuration (no need to define wellknown type here):


Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

Client code to access MyService through IMyService:

Collapse
IMyService myService1 =
Activator.GetObject(
typeof(IMyService),
"tcp://localhost:33000/MyServiceUri"
) as IMyService;

string result = myService1.func1();

Note, that there is no way to use Client Activation Object if you decide to go with
hiding implementation behind interface. This is because you need to _instantiate_ object
of class in client side for Client Side activation. But you don’t have type information on
client side – only interface. So, you can do this only with well known type definition (e.g.
Server Activation Object).

7. Custom types as parameters and return values


If you want to pass your own types as parameters to methods of remoted objects… If
you want to get such types as results of functions… The only thing that you should do is
to make your type serializable. This is easy – just add [Serailizable] attribute for your
type description. Note, if your type has members of custom types, those included types
should be also serializable. As for standard types like int, double, string, ArrayList and so
on – most of them are already serializable.

See “Custom Types” solution with example:

Collapse
[Serializable]
public class MyContainer
{
private string str_;
private int num_;

public MyContainer(string str, int num)


{
str_ = str;
num_ = num;
}

public string Str { get{ return str_;} }


public int Num { get{ return num_;} }

public override string ToString()


{
return string.Format("MyContainer[str=\"{0}\",num={1}]", Str, Num);
}
}

public class MyService : MarshalByRefObject


{
public MyContainer func1(MyContainer param)
{
Log.Print("func1() is invoked, got {0}", param);
return new MyContainer("abc", 123);
}
}

With this Client code

Collapse
class MyClient
{
[STAThread]
static void Main(string[] args)
{
MyService myService = new MyService();
Log.Print("myService created. Proxy? {0}",
(RemotingServices.IsTransparentProxy(myService)?"YES":"NO"));

MyContainer container1 = new MyContainer("From Client", 555);

MyContainer container2 = myService.func1(container1);


Log.Print("myService.func1() returned {0}", container2);
}
}

it will give you such Server output:

Collapse
[3660] [2008/10/03 10:05:27.970] func1() is invoked, got
MyContainer[str="From Client",num=555]

and such Client output:

Collapse
[2696] [2008/10/03 10:05:27.892] myService created. Proxy? YES
[2696] [2008/10/03 10:05:27.970] myService.func1() returned
MyContainer[str="abc",num=123]

8. Custom exceptions through remoting channel


There are no limitations on throwing standard Exception class as it already has
everything that is needed. As for custom exceptions here is the list of required TODOs:
1) General rule: All custom exceptions should drive from Exception class or it’s
descentants.
2) It must have [Serializable] attribute for class
3) It must have constructor

MyException(SerializationInfo info, StreamingContext context)

4) It must override

void GetObjectData(SerializationInfo info, StreamingContext context)

5) If your custom exception has some members, those should be taken care to write
and read to/from stream.

Here is our custom exception from “Exceptions” solution:

Collapse
[Serializable]
public class MyException : ApplicationException
{
private string additionalMessage_;

public MyException(string message, string additionalMessage)


:base(message)
{
additionalMessage_ = additionalMessage;
}

public MyException(SerializationInfo info, StreamingContext context)


:base(info, context)
{
additionalMessage_ = info.GetString("additionalMessage");
}

public override void GetObjectData(SerializationInfo info,


StreamingContext context)
{
base.GetObjectData (info, context);
info.AddValue("additionalMessage", additionalMessage_);
}

public string AdditionalMessage { get{ return additionalMessage_;} }


}

We save our member data in “GetObjectData(…)” method. During deserialization we


restore this value in constructor with SerializationInfo as parameter.

With this MyService implementation:

Collapse
public class MyService : MarshalByRefObject
{
public void func1()
{
throw new MyException("Main text for custom ex", "Additional text");
}

public void func2()


{
throw new Exception("Main text for standard ex");
}
}

We simply try to throw both our custom exception and starndard one. Having such
Client implementation:

Collapse
class MyClient
{
[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXClient.exe.config");

MyService myService = new MyService();

try
{
myService.func1();
}
catch(MyException ex)
{
Log.Print("Caught MyException: message=\"{0}\", add.msg=\"{1}\"",
ex.Message, ex.AdditionalMessage);
}

try
{
myService.func2();
}
catch(Exception ex)
{
Log.Print("Caught Exception: message=\"{0}\"",
ex.Message);
}

Log.WaitForEnter("Press ENTER to exit...");


}
}

We get output (stripped):

Collapse
[15:09:39.380] Caught MyException: message="Main text for custom ex",
add.msg="Additional text"
[15:09:39.380] Caught Exception: message="Main text for standard ex"
If we comment out saving and restoring of additionalMessage field in MyException
class – after deserialization we will get default string value. So, no error will be generated
but not full state restoring. If we comment out [Serializable] attribute, we will get runtime
exception.

9. Events in .NET Remoting


Imagine use case. Our remoting object is instantiated in Server. In regular use case
Client applications use remoting object by invoking its methods. What if you want it to
invoke some callback method that is resided inside Client application? You might prepare
some information for Client and wait for client application to use polling mechanism and
to call some remote object method periodically like “Information[]
MyService.IsThereSomeInfoForMe()”. But actually we can use event mechanism. There
are some refinements though:

1) Server application should have runtime type information about type that holds
callback method.
2) This callback method should be public and cannot be static
3) To avoid Server to wait and make sure that callback got recipient, we have to
mark callback with [OneWay] attribute. It makes us unable to return some data neither
through “return” value nor through “ref” of “out” parameters.
4) As instance of this type will instantiated on Client side and will be used on Server
side, it should derive from MarshalByRefObject class.

Take a look at “Events” solution. All these limitations make us to introduce some even
sink and define it in Cmn assembly so it is available for both Server and Client
application:

Collapse
public class EventSink : MarshalByRefObject
{
public EventSink()
{
}

[System.Runtime.Remoting.Messaging.OneWay]
public void EventHandlerCallback(string text)
{
}

public void Register(MyService service)


{
service.EventHandler += new OnEventHandler(EventHandlerCallback);
}

public void Unregister(MyService service)


{
service.EventHandler -= new OnEventHandler(EventHandlerCallback);
}
}
As we want this sink to actually invoke our callback, we cannot use polymorphism and
override some of methods in derived class that would be defined inside code of our Client
application. This will violate rule #1 from above – Server will need to know our type. So
we use delegation mechanism and pass our Client’s callback to EvenSink as constructor
parameter. Here is full code for EventSink class:

Collapse
public class EventSink : MarshalByRefObject
{
private OnEventHandler handler_;

public EventSink(OnEventHandler handler)


{
handler_ = handler;
}

[System.Runtime.Remoting.Messaging.OneWay]
public void EventHandlerCallback(string text)
{
if (handler_ != null)
{
handler_(text);
}
}

public void Register(MyService service)


{
service.EventHandler += new OnEventHandler(EventHandlerCallback);
}

public void Unregister(MyService service)


{
service.EventHandler -= new OnEventHandler(EventHandlerCallback);
}
}

Also, since .NET Framwork v1.1 there are security restriction on deserialization of
some types. In order to override default setting we need to set filterLevel to “Full”. Here
is full Server config file:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
objectUri="MyServiceUri"
mode="Singleton" />
</service>
<channels>
<channel ref="tcp" port="33000">
<serverProviders>
<formatter ref="binary" typeFilterLevel="Full" />
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>

And Client configuration:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown
type="ONX.Cmn.MyService, ONXCmn"
url="tcp://localhost:33000/MyServiceUri" />
</client>
<channels>
<channel ref="tcp" port="0">
<clientProviders>
<formatter ref="binary" />
</clientProviders>
<serverProviders>
<formatter ref="binary" typeFilterLevel="Full" />
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>

It is also possible to configure this through code. See MSDN for details. Take a look at
MyService class now.

Collapse
public delegate void OnEventHandler(string message);

public class MyService : MarshalByRefObject


{
public event OnEventHandler EventHandler;

public string func1()


{
PublishEventAnfScheduleOneMore("Event from Server: func1() is
invoked");
return "MyService.func1()";
}

private void PublishEvent(string message)


{
if (EventHandler != null)
{
EventHandler(message);
}
}

private void PublishEventAnfScheduleOneMore(string text)


{
PublishEvent(text);
Thread t = new Thread(new ThreadStart(PublishEventIn5Seconds));
t.Start();
}

private void PublishEventIn5Seconds()


{
Thread.Sleep(5000);
PublishEvent("5 seconds passed from one of method calls");
}
}

As you can see we invoke callback immediately when some Client called
“MyService.func()” and also we do it one more time from separate thread after 5 seconds
timeframe. It was done for testing purposes to show that events can be invoked at any
time (e.g. not even to answer on call invocation). We span a separate thread and return
control to Client that invoked “func1()”. And then, after 5 seconds our spanned thread
will raise event for all registered event handlers. Once Client registers its event handler -
it will get ALL events from Server.

Here is stripped code for our Client application. Full version is available in “Events”
solution:

Collapse
class MyClient
{
private MyService myService_;
private EventSink sink_;

public MyClient()
{
//create proxy to remote MyService
myService_ = new ONX.Cmn.MyService();

//create event sink that can be invoked by MyService


sink_ = new EventSink(new OnEventHandler(MyEventHandlerCallback));

//register event handler with our event sink


//(after that event sink will invoke our callback)
sink_.Register(myService_);
}

public void MyEventHandlerCallback(string text)


{
Log.Print("Got text through callback! {0}", text);
}
public void Test()
{
Log.Print("myService.func1() returned {0}", myService_.func1());
}

[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXClient.exe.config");

MyClient c = new MyClient();


c.Test();

Log.WaitForEnter("Press ENTER to exit...");


}
}

And here is stripped output from one of test runs:

Collapse
[5412] [09:43:55] myService.func1() returned MyService.func1()
[5412] [09:43:55] Press ENTER to exit...
[7724] [09:43:55] Got … callback! Event from Server: func1() is invoked
[7724] [09:44:00] Got … callback! 5 seconds passed from one of method
calls

As you can see we got initial event right after call to “func1()” and then one more after
5 seconds. Pay attention that callback functions were invoked on a separate thread. So, if
you need to synchronize some data access, beware.

10. Asynchronous calls


This topic does not differ much from simple asynchronous calls without remoting.
Let’s analyze “Async Calls” solution. It has simple implementation of MyService:

Collapse
public class MyService : MarshalByRefObject
{
public string func1(string text)
{
Log.Print("func1(\"{0}\") is invoked", text);
return text+DateTime.Now.ToString("HH:mm:ss.fff");
}
}

And here is the sample of how it is used in Client application:

Collapse
delegate string GetStringHandler(string arg);

class MyClient
{
private const int NUMBER_OF_INVOCATIONS = 5;

private static void OnCallEnded(IAsyncResult ar)


{
GetStringHandler handler = ((AsyncResult)ar).AsyncDelegate as
GetStringHandler;
int index = (int)ar.AsyncState;

string result = handler.EndInvoke(ar);

Log.Print("myService.func1() #{0} is done. Result is \"{1}\"",


index, result);
}

[STAThread]
static void Main(string[] args)
{
RemotingConfiguration.Configure("ONXClient.exe.config");

MyService myService = new MyService();


Log.Print("myService created. Proxy? {0}",
(RemotingServices.IsTransparentProxy(myService)?"YES":"NO"));

for(int index=1;index<=NUMBER_OF_INVOCATIONS;++index)
{
Log.Print("Invoking myService.func1() #{0}...", index);
GetStringHandler handler = new GetStringHandler(myService.func1);
handler.BeginInvoke("from Client", new
AsyncCallback(OnCallEnded), index);
}

Log.WaitForEnter("Press ENTER to exit...");


}
}

As you can see we loop 5 times in “for”. With every iteration we create delegate that
corresponds to prototype of “MyService.func1” method and make asynchronous call with
“BeginInvoke(…)”. As we passed our “OnCallEnded” method as callback, when
asynchronous invocation is done, we get control in our OnCallEnded method. There we
get reference to our delegate and get result by calling “EndInvoke(ar)”.

Here is example out output of Client application:

Collapse
[0216] [2008/10/03 16:39:45.243] myService created. Proxy? YES
[0216] [2008/10/03 16:39:45.243] Invoking myService.func1() #1...
[0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #2...
[0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #3...
[0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #4...
[0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #5...

[0216] [2008/10/03 16:39:45.290] Press ENTER to exit...


[2248] [2008/10/03 16:39:45.290] myService.func1() #2 is done. Result
is "from Client16:39:45.274"
[3868] [2008/10/03 16:39:45.290] myService.func1() #1 is done. Result
is "from Client16:39:45.274"
[2248] [2008/10/03 16:39:45.290] myService.func1() #3 is done. Result
is "from Client16:39:45.290"
[2248] [2008/10/03 16:39:45.290] myService.func1() #5 is done. Result
is "from Client16:39:45.290"
[3868] [2008/10/03 16:39:45.290] myService.func1() #4 is done. Result
is "from Client16:39:45.290"

We were even lucky to get our 5 calls in order, that is different from original – call #2
ends earlier than call #1. The same about calls #4 and #5.

Also notice, that not all calls were running in the same thread. And all of them are
different from thread where we initiated our 5 calls.

11. Several Services in one Server. Several Server links


from single Client app
All the samples in MSDN and internet that I reviewed were showing single Remoting
Object type in Server application. I was wonder how do we introduce several services in
single Server. And how do we use several Servers in single Client application. It
appeared to be not so hard, but still it better to see than to guess.

Let us analyze the case with 2 wellknown types on Server side:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
type="ONX.Cmn.MyService1, ONXCmn"
objectUri="MyService1Uri"
mode="SingleCall" />
<wellknown
type="ONX.Cmn.MyService2, ONXCmn"
objectUri="MyService2Uri"
mode="SingleCall" />
</service>
<channels>
<channel ref="tcp" port="33000"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>

You cannot:
1) Have several channels with the same protocol (e.g. “ref” parameter). Otherwise
you will get exception that such protocol is already registered. But you can specify
several channels if they are for different protocols
2) Each known type should have unique objectUri. Otherwise definition of types
will be overlapped and only on of types will be available

For configuration from above our helper


“Utils.DumpAllInfoAboutRegisteredRemotingTypes();” method gives us:

Collapse
[7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -
(BEGIN)---------
[7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry:
type='ONX.Cmn.MyService2, ONXCmn'; objectUri=MyService2Uri;
mode=SingleCall
[7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry:
type='ONX.Cmn.MyService1, ONXCmn'; objectUri=MyService1Uri;
mode=SingleCall
[7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -
(END) ---------

In our case Client configuration looks like:

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown
type="ONX.Cmn.MyService1, ONXCmn"
url="tcp://localhost:33000/MyService1Uri" />
<wellknown
type="ONX.Cmn.MyService2, ONXCmn"
url="tcp://localhost:33000/MyService2Uri" />
</client>
</application>
</system.runtime.remoting>
</configuration>

If we would want to use Services from different Servers, each Server would listen on
different port. So, there would be different port in each “<wellknown/>” section.

There is “Two Services in single Server” solution if you would like to try it for
yourself.

12. Summary
Thank you for your time. I hope it was spent with use. Any comments are welcome. I
will try to adjust this article as soon as I have some comments and time. Happy remoting!
License
This article, along with any associated source code and files, is licensed under The Code
Project Open License (CPOL)

About the Author


onxonx
Occupation: Software Developer (Senior)
Location: United States

Вам также может понравиться