Section 19.3. Remoting


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 the first time a client attempts to connect to it, 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.[1]

[1] Client-activated objects can be less robust. If a call fails to a client-activated object, the developer must assume that the object has been lost on the server and must regenerate the object from scratch.

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 15) that implements the interface shown in Example 19-2.

Example 19-2. The Calculator interface
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace Calculator {    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 Calculator.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 on the Visual Studio menu bar. Alternatively, if you have entered the source code using Notepad or another text editor, 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 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 Example 19-3, create CalculatorServer.cs in a new project of type C# Console Application (be sure to include a reference to Calculator.dll) and then compile it by selecting Build on the Visual Studio menu bar.

The CalculatorServer class implements ICalc. It derives from MarshalByRefObject so that it will deliver a proxy of the calculator to the client application:

class CalculatorServer : MarshalByRefObject, Calculator.ICalc

The implementation consists of little more than a constructor and simple methods to implement the four functions.

In Example 19-3, you'll put the logic for the server into the Main() method of CalculatorServer.cs.

Your first task is to create a channel. Use HTTP as the transport mechanism. 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're 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 indicates the exact service. To get the type of the object, you can use typeof, which returns a Type object. Pass in the full name of the object whose type you want:

Type calcType =    typeof( "CalculatorServerNS.CalculatorServer" );

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 creates the server-side sink chain. 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
#region Using directives using System; using System.Collections.Generic; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using System.Text; #endregion namespace CalculatorServerNS {    class CalculatorServer : MarshalByRefObject, Calculator.ICalc    {       public CalculatorServer( )       {          Console.WriteLine( "CalculatorServer 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( "CalculatorServerNS.CalculatorServer" );          // 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

While the CLR will preregister the TCP and HTTP channel, you will need to register a channel on the client if you want to receive callbacks or you are using a nonstandard channel. For this example, 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 Uniform Resource Identifier (URI) of the service.

Object 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 can't 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 Calculator.dll as you did with CalcServer.cs).

Example 19-4. The remoting Calculator client
#region Using directives using System; using System.Collections.Generic; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using System.Text; #endregion namespace CalculatorClient {    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);          Object obj = RemotingServices.Connect             (typeof(Calculator.ICalc),              "http://localhost:65100/theEndPoint");          try          {             // cast the object to our interface             Calculator.ICalc calc = obj as Calculator.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:

RemotingConfiguration.RegisterWellKnownServiceType     ( calcType,       "theEndPoint",        WellKnownObjectMode.Singleton );

Change the object to SingleCall:

RemotingConfiguration.RegisterWellKnownServiceType     ( calcType,       "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 obtain a Type object for the Calculator class:

Type.GetType("CalculatorServerNS.CalculatorServer");

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() {    HttpChannel chan = new HttpChannel( 65100 );    ChannelServices.RegisterChannel( chan );    CalculatorServerNS.CalculatorServer  calculator =        new CalculatorServer( );     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 (see the "Understanding Endpoints," section later in this chapter).

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-6. You also need to add two using statements to this example.

Example 19-6. 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.

19.3.7. Understanding Endpoints

What is going on when you register the endpoint in Example 19-5 (the server)? Clearly, the server is associating that endpoint with the type. 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, formerly 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, instead of 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 Example 19-5s Main( ) is shown in Example 19-7. You'll also need to add three using statements to CalcServer.cs, as shown.

Example 19-7. Marshaling an object without a well-known endpoint
using System; using System.IO; using System.Runtime.Serialization.Formatters.Soap; 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 filesystem. The server then waits for the client to connect. It might have a long wait. (Though after about 10 minutes, it shuts itself down.)



Programming C#(c) Building. NET Applications with C#
Programming C#: Building .NET Applications with C#
ISBN: 0596006993
EAN: 2147483647
Year: 2003
Pages: 180
Authors: Jesse Liberty

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net