In addition to being marshaled across context and app domain boundaries, objects can be marshaled across process boundaries, and even across machine boundaries. When an object is marshaled, either by value or by proxy, across a process or machine boundary, it is said to be remoted.
There
are
two types of server objects supported
for remoting in .NET: well-known
and
client-activated
. The communication with
well-known objects is established each time a message is sent by the
client. There is no permanent connection with a well-known object, as
there is with client-activated objects.
Well-known objects come in two varieties: singleton
and
single-call
. With a well-known singleton object,
all messages for the object, from all clients, are dispatched to a
single object running on the server. The object is created when the
server is started and is there to provide service to any client that
can reach it. Well-known objects must have a parameterless
constructor.
With a well-known single-call object, each new message from a client is handled by a new object. This is highly advantageous on server farms, where a series of messages from a given client might be handled in turn by different machines depending on load balancing.
Client-activated objects are typically used by programmers who are creating dedicated servers, created to provide services to a client they are also writing. In this scenario, the client and the server create a connection, and they maintain that connection until the needs of the client are fulfilled.
The best way to understand remoting is to walk through an example. Here, you’ll build a simple four-function calculator class, like the one used in an earlier discussion on web services (see Chapter 16), that implements the interface shown in Example 19-2.
Example 19-2. The Calculator interface
namespace Programming_CSharp { using System; public interface ICalc { double Add(double x, double y); double Sub(double x, double y); double Mult(double x, double y); double Div(double x, double y); } }
Save this in a file named Icalc.cs
and compile
it into a file named ICalc.dll
. To create and
compile the source file in Visual Studio, create a new project of
type C# Class Library, enter the interface definition in the Edit
window, and then select Build → Build on the Visual Studio menu
bar. Alternatively, if you have entered the source code using
Notepad, you can compile the file at the command line by entering:
csc Icalc.cs /t:library
There are tremendous advantages to implementing a server through an interface. If you implement the calculator as a class, the client must link to that class in order to declare instances on the client. This greatly diminishes the advantages of remoting because changes to the server require the class definition to be updated on the client. In other words, the client and server would be tightly coupled. Interfaces help decouple the two objects; in fact, you can later update that implementation on the server, and as long as the server still fulfills the contract implied by the interface, the client need not change at all.
To build the server
used in this example, create CalcServer.cs
in a
new project of type C# Console Application and then compile it by
selecting Build → Build on the Visual Studio menu bar. Or, you
can enter the code in Notepad, save it to a file named
CalcServer.cs
, and enter the following at the
command-line prompt:
csc CalcServer.cs /t:exe
The Calculator
class implements
ICalc
. It derives from
MarshalByRefObject
so that it will deliver a proxy
of the calculator to the client application:
public class Calculator : MarshalByRefObject, ICalc
The implementation consists of little more than a constructor and simple methods to implement the four functions.
In this example, you’ll put the logic for the server into the
Main( )
method of CalcServer.cs
.
Your first task is to create a channel
. Use HTTP
as the transport because it is simple and you don’t need a
sustained TCP/IP connection. You can use the
HTTPChannel
type provided by .NET:
HTTPChannel chan = new HTTPChannel(65100);
Notice that you register the channel on TCP/IP port 65100 (see the discussion of port numbers in Chapter 21).
Next, register the channel with the CLR
ChannelServices
using the static method
RegisterChannel
:
ChannelServices.RegisterChannel(chan);
This step informs .NET that you will be providing HTTP services on port 65100, much as IIS does on port 80. Because you’ve registered an HTTP channel and not provided your own formatter, your method calls will use the SOAP formatter by default.
Now you are ready to ask the
RemotingConfiguration
class to register your well-known object.
You must pass in the type of the object you want to register, along
with an endpoint
. An
endpoint
is a name that RemotingConfiguration
will
associate with your type. It completes the address. If the IP address
identifies the machine and the port identifies the channel, the
endpoint identifies the actual application that will be providing the
service. To get the type of the object, you can call the static
method GetType( )
of the Type
class, which returns a
Type
object. Pass in the full name of the object
whose type you want:
Type calcType = Type.GetType("Programming_CSharp.Calculator");
Also pass in the enumerated type that indicates whether you are
registering a SingleCall
or
Singleton
:
RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint",WellKnownObjectMode.Singleton );
The call to RegisterWellKnownServiceType
does not
put one byte on the wire. It simply uses reflection to build a proxy
for your object.
Now you’re ready to rock and roll. Example 19-3 provides the entire source code for the server.
Example 19-3. The Calculator server
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; namespace Programming_CSharp { // implement the calculator class public class Calculator : MarshalByRefObject, ICalc { public Calculator( ) { Console.WriteLine("Calculator constructor"); } // implement the four functions public double Add(double x, double y) { Console.WriteLine("Add {0} + {1}", x, y); return x+y; } public double Sub(double x, double y) { Console.WriteLine("Sub {0} - {1}", x, y); return x-y; } public double Mult(double x, double y) { Console.WriteLine("Mult {0} * {1}", x, y); return x*y; } public double Div(double x, double y) { Console.WriteLine("Div {0} / {1}", x, y); return x/y; } } public class ServerTest { public static void Main( ) { // create a channel and register it HttpChannel chan = new HttpChannel(65100); ChannelServices.RegisterChannel(chan); Type calcType = Type.GetType("Programming_CSharp.Calculator"); // register our well-known type and tell the server // to connect the type to the endpoint "theEndPoint" RemotingConfiguration.RegisterWellKnownServiceType ( calcType, "theEndPoint", WellKnownObjectMode.Singleton ); // "They also serve who only stand and wait."); (Milton) Console.WriteLine("Press [enter] to exit..."); Console.ReadLine( ); } } }
When you run this program, it prints its self-deprecating message:
Press [enter] to exit...
The client must also register a channel, but because you are not listening on that channel, you can use channel 0:
HTTPChannel chan = new HTTPChannel(0); ChannelServices.RegisterChannel(chan);
The client now need only connect through the remoting services,
passing a Type
object representing the type of the
object it needs (in our case, the
ICalc
interface)
and the URI (Uniform Resource Identifier) of
the implementing class:
MarshalByRefObject obj = RemotingServices.Connect (typeof(Programming_CSharp.ICalc), "http://localhost:65100/theEndPoint");
In this case the server is assumed to be running on your local
machine, so the URI is http://localhost, followed by the port for
the server, 65100
, followed in turn by the
endpoint you declared in the server, theEndPoint
.
The remoting service should return an object representing the
interface you’ve requested. You can then cast that object to
the interface and begin using it. Because remoting cannot be
guaranteed (the network might be down, the host machine may not be
available, and so forth), you should wrap the usage in a
try
block:
try { Programming_CSharp.ICalc calc = obj as Programming_CSharp.ICalc; double sum = calc.Add(3,4);
You now have a proxy of the Calculator operating on the server, but usable on the client, across the process boundary and, if you like, across the machine boundary. Example 19-4 shows the entire client.
Example 19-4. The remoting Calculator client
namespace Programming_CSharp { using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; public class CalcClient { public static void Main( ) { int[] myIntArray = new int[3]; Console.WriteLine("Watson, come here I need you..."); // create an Http channel and register it // uses port 0 to indicate won't be listening HttpChannel chan = new HttpChannel(0); ChannelServices.RegisterChannel(chan); // get my object from across the http channel MarshalByRefObject obj = (MarshalByRefObject) RemotingServices.Connect (typeof(Programming_CSharp.ICalc), "http://localhost:65100/theEndPoint"); try { // cast the object to our interface Programming_CSharp.ICalc calc = obj as Programming_CSharp.ICalc; // use the interface to call methods double sum = calc.Add(3.0,4.0); double difference = calc.Sub(3,4); double product = calc.Mult(3,4); double quotient = calc.Div(3,4); // print the results Console.WriteLine("3+4 = {0}", sum); Console.WriteLine("3-4 = {0}", difference); Console.WriteLine("3*4 = {0}", product); Console.WriteLine("3/4 = {0}", quotient); } catch( System.Exception ex ) { Console.WriteLine("Exception caught: "); Console.WriteLine(ex.Message); } } } } Output on client:: Watson, come here I need you... 3+4 = 7 3-4 = -1 3*4 = 12 3/4 = 0.75 Output on server: Calculator constructor Press [enter] to exit... Add 3 + 4 Sub 3 - 4 Mult 3 * 4 Div 3 / 4
The server starts up and waits for the user to press Enter to signal that it can shut down. The client starts and displays a message to the console. The client then calls each of the four operations. You see the server printing its message as each method is called, and then the results are printed on the client.
It is as simple as that; you now have code running on the server and providing services to your client.
To see the difference that
SingleCall
makes versus Singleton
, change one line in the
server’s Main( )
method. Here’s the existing code:
RemotingServices. RegisterWellKnownServiceType ( "CalcServerApp","Programming_CSharp.Calculator", "theEndPoint",WellKnownObjectMode.Singleton );
Change the object to SingleCall
:
RemotingServices. RegisterWellKnownServiceType
( "CalcServerApp","Programming_CSharp.Calculator",
"theEndPoint",WellKnownObjectMode.SingleCall );
The output reflects that a new object is created to handle each request:
Calculator constructor Press [enter] to exit... Calculator constructor Add 3 + 4 Calculator constructor Sub 3 - 4 Calculator constructor Mult 3 * 4 Calculator constructor Div 3 / 4
When you called the
RegisterWellKnownServiceType()
method on the server, what actually
happened? Remember that you created a Type
object
for the Calculator
class:
Type.GetType("Programming_CSharp.Calculator");
You then called RegisterWellKnownServiceType()
,
passing in that Type
object along with the
endpoint and the Singleton
enumeration. This
signals the CLR to instantiate your Calculator
and
then to associate it with an endpoint.
To do that work yourself, you would need to modify Example 19-3, changing Main( )
to
instantiate a Calculator
and then passing that
Calculator
to the Marshal( )
method of RemotingServices
with the endpoint to
which you want to associate that instance of
Calculator
. The modified Main( )
is shown in Example 19-5 and, as you can
see, its output is identical to that of Example 19-3.
Example 19-5. Manually instantiating and associating Calculator with an endpoint
public static void Main( ) { // create a channel and register it HttpChannel chan = new HttpChannel(65100); ChannelServices.RegisterChannel(chan); // make your own instance and call Marshal directly Calculator calculator = new Calculator( ); RemotingServices.Marshal(calculator,"theEndPoint"); // "They also serve who only stand and wait."); (Milton) Console.WriteLine("Press [enter] to exit..."); Console.ReadLine( ); }
The net effect is that you have instantiated a calculator object, and associated a proxy for remoting with the endpoint you’ve specified.
What is going on when you register this endpoint? Clearly, the server is associating that endpoint with the object you’ve created, and when the client connects that endpoint is used as an index into a table so that the server can provide a proxy to the correct object (in this case, the Calculator).
If you don’t provide an endpoint for the client to talk to, you can instead write all the information about your calculator object to a file and physically give that file to your client. For example, you could send it to your buddy by email, and he could load it on his local computer.
The client can deserialize the object and reconstitute a proxy which it can then use to access the calculator on your server! (The following example was suggested to me by Mike Woodring of DevelopMentor, who uses a similar example to drive home the idea that the endpoint is simply a convenience for accessing a marshaled object remotely.)
To see how you can invoke an object without a known endpoint, modify
the Main( )
method of
Example 19-3 once again. This time, rather than
calling Marshal( )
with an endpoint, just pass in the
object:
ObjRef objRef = RemotingServices.Marshal(calculator)
Marshal( )
returns an ObjRef
object. An ObjRef
object stores all the
information required to activate and communicate with a remote
object. When you do supply an endpoint, the server creates a table
that associates the endpoint with an objRef
so
that the server can create the proxy when a client asks for it.
ObjRef
contains all the information needed by the
client to build a proxy, and ObjRef
itself is
serializable.
Open a file stream for writing to a new file and create a new SOAP
formatter. You can serialize your ObjRef
to that
file by invoking the Serialize( )
method on the
formatter, passing in the file stream and the
ObjRef
you got back from
Marshal
. Presto! You have all the information you
need to create a proxy to your object written out to a disk file. The
complete replacement for Main( )
is shown in Example 19-6.
Example 19-6. Marshaling an object without a well-known endpoint
public static void Main( ) { // create a channel and register it HttpChannel chan = new HttpChannel(65100); ChannelServices.RegisterChannel(chan); // make your own instance and call Marshal directly Calculator calculator = new Calculator( ); ObjRef objRef = RemotingServices.Marshal(calculator); FileStream fileStream = new FileStream("calculatorSoap.txt",FileMode.Create); SoapFormatter soapFormatter = new SoapFormatter( ); soapFormatter.Serialize(fileStream,objRef); fileStream.Close( ); // "They also serve who only stand and wait."); (Milton) Console.WriteLine( "Exported to CalculatorSoap.txt. Press ENTER to exit..."); Console.ReadLine( ); }
When you run the server, it writes the file
calculatorSoap.txt
to the disk. The server then
waits for the client to connect. It might have a long wait.
You can take that file to your client and reconstitute it on the
client machine. To do so, again create a channel and register it.
This time, however, open a fileStream
on the file
you just copied from the server:
FileStream fileStream = new FileStream ("calculatorSoap.txt", FileMode.Open);
Then instantiate a SoapFormatter
and call
Deserialize( )
on the formatter, passing in the
filename and getting back an ObjRef
:
SoapFormatter soapFormatter = new SoapFormatter ( ); try { ObjRef objRef = (ObjRef) soapFormatter.Deserialize (fileStream);
You are ready to unmarshall that ObjRef
, getting
back an ICalc
reference:
ICalc calc = (ICalc) RemotingServices.Unmarshal(objRef);
You are now free to invoke methods on the server through that
ICalc
, which will act as a proxy to the calculator
object running on the server that you described in the
calculatorSoap.txt
file. The complete
replacement for the client is shown in Example 19-7.
Example 19-7. Replacement of Main( ) from Example 19-4 (the client)
public static void Main( ) { int[] myIntArray = new int[3]; Console.WriteLine("Watson, come here I need you..."); // create an Http channel and register it // uses port 0 to indicate you won't be listening HttpChannel chan = new HttpChannel(0); ChannelServices.RegisterChannel(chan); FileStream fileStream = new FileStream ("calculatorSoap.txt", FileMode.Open); SoapFormatter soapFormatter = new SoapFormatter ( ); try { ObjRef objRef = (ObjRef) soapFormatter.Deserialize (fileStream); ICalc calc = (ICalc) RemotingServices.Unmarshal(objRef); // use the interface to call methods double sum = calc.Add(3.0,4.0); double difference = calc.Sub(3,4); double product = calc.Mult(3,4); double quotient = calc.Div(3,4); // print the results Console.WriteLine("3+4 = {0}", sum); Console.WriteLine("3-4 = {0}", difference); Console.WriteLine("3*4 = {0}", product); Console.WriteLine("3/4 = {0}", quotient); } catch( System.Exception ex ) { Console.WriteLine("Exception caught: "); Console.WriteLine(ex.Message); }
When the client starts up, the file is read from the disk and the proxy is unmarshaled. This is the mirror operation to marshaling and serializing the object on the server. Once you have unmarshaled the proxy, you are able to invoke the methods on the calculator object running on the server.
3.133.127.37