[ Team LiB ] Previous Section Next Section

19.3 Remoting

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.

19.3.1 Understanding Server Object Types

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, which 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.

19.3.2 Specifying a Server with an Interface

The best way to understand remoting is to walk through an example. Here, 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 /t:library ICalc.cs

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.

19.3.3 Building a Server

To build the server used in this example, create CalcServer.cs in a new project of type C# Console Application (be sure to include a reference to ICalc.dll) 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 /t:exe /r:ICalc.dll CalcServer.cs

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...

and then waits for a client to ask for service.

19.3.4 Building the Client

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 (to compile it, you must include a reference to ICalc.dll as you did with CalcServer.cs).

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.

19.3.5 Using SingleCall

To see the difference that SingleCall makes versus Singleton, change one line in the server's Main( ) method. Here's the existing code:

RemotingConviguration.RegisterWellKnownServiceType
    ( "CalcServerApp","Programming_CSharp.Calculator",
        "theEndPoint",WellKnownObjectMode.Singleton );

Change the object to SingleCall:

RemotingConviguration.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

19.3.6 Understanding RegisterWellKnownServiceType

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.

19.3.7 Understanding Endpoints

What is going on when you register this endpoint? Clearly, the server is associating that endpoint with the object you've created. 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. You will also need to add two using statements to CalcServer.cs:

using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
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 ICalc:

SoapFormatter soapFormatter = 
   new SoapFormatter ( );        
try
{
      ICalc calc=
         (ICalc) soapFormatter.Deserialize (fileStream);

You are now free to invoke methods on the server through that ICalc, which acts 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's Main( ) method is shown in Example 19-7. You also need to add two using statements to this example.

Example 19-7. Replacement of Main( ) from Example 19-4 (the client)
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
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
   {
      ICalc calc= 
         (ICalc) soapFormatter.Deserialize (fileStream);

      // 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.

    [ Team LiB ] Previous Section Next Section