Previous Section  < Day Day Up >  Next Section

14.2. Remoting

At its core, remoting is as a way to permit applications in separate AppDomains to communicate and exchange data. This is usually characterized as a client-server relationship in which the client accesses resources or objects on a remote server that agrees to provide access. The way in which this agreement between client and server is implemented is what remoting is all about. The physical proximity of the AppDomains does not matter: They may be in the same process, in different processes, or on different machines on different continents.

Remoting is often portrayed as a concept that is difficult to understand and implement梡articularly when compared to Web Services. This sentiment is misleading and simply not true for many applications.

Consider the steps required to enable a client to access an object on a remote server:

  • Create a TCP or HTTP connection between the client and server.

  • Select how the messages sent between server and client are formatted.

  • Register the type that is to be accessed remotely.

  • Create the remote object and activate it from the server or the client.

.NET takes care of all the details. You don't have to understand the underlying details of TCP, HTTP, or ports梛ust specify that you want a connection and what port to use. If HTTP is selected, communications use a Simple Object Access Protocol (SOAP) format; for TCP, binary is used. The registration process occurs on both the server and client. The client selects a registration method and passes it a couple of parameters that specify the address of the server and the type (class) to be accessed. The server registers the types and ports that it wants to make available to clients, and how it will make them available. For example, it may implement the object as a singleton that is created once and handles calls from all clients; or it may choose to create a new object to handle each call.

Figure 14-3 depicts the learning curve that developers new to remoting can expect to encounter. By hiding much of the underlying communications details, .NET enables a developer to quickly develop functioning applications that can access remote objects. The complexity often associated with remoting comes into play as you move further up the learning curve to take advantage of advanced remoting techniques such as creating sink providers and custom transport channels. Knowledge of these advanced techniques enables one to customize the way distributed applications communicate. For information on these topics, refer to a book on advanced .NET remoting, such as Advanced .NET Remoting by Ingo Rammer.[1]

[1] Advanced .NET Remoting, Second Edition, by Ingo Rammer and Mario Szpuszta; Apress, 2005.

Figure 14-3. The learning curve for developing remoting applications


This chapter focuses on topics to the left of the advanced remoting line. You'll learn how to design applications that permit remote objects to be created by either the server or the client, how to control the lifetime of these objects, and how to design and deploy assemblies that best take advantage of the remoting architecture. There are quite a few code examples whose purpose is to present prototypes that you can use to implement a wide range of remoting applications. Included are examples of a server that streams requested images to clients, and a message server that both receives messages and sends them to the targeted recipient upon request.

Remoting Architecture

When a client attempts to invoke a method on a remote object, its call passes through several layers on the client side. The first of these is a proxy梐n abstract class that has the same interface as the remote object it represents. It verifies that the number and type of arguments in the call are correct, packages the request into a message, and passes it to the client channel. The channel is responsible for transporting the request to the remote object. At a minimum, the channel consists of a formatter sink that serializes the request into a stream and a client transport sink that actually transmits the request to a port on the server. The sinks within a channel are referred to as a sink chain. Aside from the two standard sinks, the channel may also contain custom sinks that operate on the request stream.

On the server side, the process is reversed, as the server transport sink receives the message and sends it up the chain. After the formatter rebuilds the request from the stream, .NET creates the object on the server and executes the requested method.

Figure 14-4 illustrates the client-server roles in a remoting architecture. Let's examine its three key components: proxies, formatter classes, and channel classes.

Figure 14-4. High-level view of .NET remoting architecture


Proxies

When a client attempts to communicate with a remote object, its reference to the object is actually handled by an intermediary known as a proxy. For .NET remoting, there are two types of proxies: a transparent proxy that the client communicates with directly, and a real proxy that takes the client request and forwards it to the remote object.

The transparent proxy is created by the CLR to present an interface to the client that is identical to the remote class. This enables the CLR to verify that all client calls match the signature of the target method梩hat is, the type and number of parameters match. Although the CLR takes care of constructing the transparent proxy, the developer is responsible for ensuring that the CLR has the metadata that defines the remote class available at compile time and runtime. The easiest way is to provide the client with a copy of the server assembly that contains the class. But, as we discuss later, there are better alternatives.

After the transparent proxy verifies the call, it packages the request into a message object梐 class that implements the IMessage interface. The message object is passed as a parameter to the real proxy's Invoke method, which passes it into a channel. There, a formatter object serializes the message and passes it to a channel object that physically sends the message to the remote object.

Core Note

The real proxy that is responsible for sending a message to a remote object is an implementation of the RealProxy class that is generated automatically by the CLR. Although this meets the needs of most remoting applications, a developer has the option of creating a custom implementation.


Formatters

Two formatters are included as part of the .NET Remoting classes: a binary formatter and a SOAP formatter. SOAP, which is discussed in detail in the Web Services chapter, serializes messages into an XML format. Binary produces a much smaller message stream than SOAP, because it sends the message as a raw byte stream.

By default, SOAP is used when the HTTP protocol is selected and binary is used with the TCP protocol. However, you can also choose to send SOAP over TCP and binary over HTTP. Although not as efficient as the binary format, the combination of SOAP and HTTP has become a de facto standard for transmitting data through firewalls whether using remoting or Web Services. The binary format is recommended for cases where firewalls are not an issue.

Channels

Channel objects are created from classes that implement the IChannel interface. .NET comes with two that handle most needs: HttpChannel and TcpChannel. It is the responsibility of the remote host to register the channel over which it is willing to provide access; similarly, the client registers the channel it wants to issue its calls on.

In its simplest form, registration on the host consists of creating an instance of the channel object and registering it by passing it as a parameter to the static RegisterChannel method of the ChannelServices class. This example registers an HTTP channel on port 3200:


// Channel Registration

HttpChannel c = new HttpChannel(3200);  // Port 3200

ChannelServices.RegisterChannel(c);


The only difference in registering on the client side is that the port number does not have to be specified:


HttpChannel c = new HttpChannel();

ChannelServices.RegisterChannel(c);


There are some rules to keep in mind when registering channels on the client and server:

  • Both the host and client can register multiple channels; however, the client must register a channel that matches one the host has registered.

  • Multiple channels cannot use the same port.

  • By default, HTTP and TCP channels are given the names http and tcp, respectively. If you attempt to register multiple HTTP or TCP channels using the default name, you will receive an exception. The way around this is to create the channels using a constructor (described shortly) that accepts a channel name parameter.

As an alternative to embedding the channel and protocol information within code, .NET permits it to be specified in configuration files associated with the client and host assemblies. For example, if the host assembly is named MessageHost.exe, we could have a configuration file name MessageHost.exe.config containing the following port and formatting specification.


<application>

   <channels>

      <channel ref="http" port="3200"/>

   </channels>

</application>


A program uses this file by passing the file name to the static Configure method of the RemotingConfiguration class:


RemotingConfiguration.Configure("MessageHost.exe.config");


The configuration file must be in the same directory as the assembly referencing it.

Assigning a Name to a Channel

To open multiple channels using the same protocol, a host must assign a name to each channel. To do so, it must use this form of the HttpChannel constructor:


HttpChannel(IDictionary properties,

            IClientChannelSinkProvider csp,

            IserverChannelSinkProvicer ssp )


Only the first parameter is of interest for naming purposes. The second or third can be used to specify the formatter used on the client or server side:


IDictionary chProps = new Hashtable();

chProps["name"] = "Httpchannel01";

chProps["port"] = "3202";

ChannelServices.RegisterChannel(new HttpChannel(chProps,

                                null, null));


Types of Remoting

Recall that the parameters in a C# method may be passed by value or by reference. Remoting uses the same concept to permit a client to access objects梐lthough the terminology is a bit different. When a client gets an actual copy of the object, it is referred to as marshaling by value (MBV); when the client gets only a reference to the remote object, it is referred to as marshaling by reference (MBR). The term marshaling simply refers to the transfer of the object or request between the client and server.

Marshaling by Value

When an object is marshaled by value, the client receives a copy of the object in its own application domain. It can then work with the object locally and has no need for a proxy. This approach is much less popular than marshaling by reference where all calls are made on a remote object. However, for objects that are designed to run on a client as easily as on a server, and are called frequently, this can reduce the overhead of calls to the server.

As an example, consider an object that calculates body mass index (BMI). Instead of having the server implement the class and return BMI values, it can be designed to return the BMI object itself. The client can then use the object locally and avoid further calls to the server. Let's see how to implement this.

For an object to be marshaled by value, it must be serializable. This means that the class must either implement the ISerializable interface or梩he easier approach梙ave the [Serializable] attribute. Here is the code for the class on the server:


[Serializable]

public class BMICalculator

{

   // Calculate body mass index

   public decimal inches;

   public decimal pounds;

   public decimal GetBMI()

   {

      return ((pounds*703* 10/(inches*inches))/10);

   }

}


The HealthTools class that is marshaled by reference returns an instance of BMICalculator:


public class HealthTools: MarshalByRefObject

{

   // Return objects to calculate BMI

   public BMICalculator GetBMIObj(){

      return new BMICalculator();

   }


The client creates an instance of HealthTools and calls the GetBMIObj method to return the calculator object:


HealthMonitor remoteObj = new HealthMonitor();

BMICalculator calc= remoteObj.GetBMIObj();

calc.pounds= 168M;

calc.inches= 73M;

Console.WriteLine(calc.GetBMI());


It is important to understand that this example uses both marshaling by value and marshaling by reference: an MBR type (HealthTools) implements a method (GetBMIObj) that returns an MBV type (BMICalculator). You should recognize this as a form of the factory design pattern discussed in Chapter 4, "Working with Objects in C#."

Marshaling by Reference

Marshaling by reference (MBR) occurs when a client makes a call on an object running on a remote server. The call is marshaled to the server by the proxy, and the results of the call are then marshaled back to the client.

Objects accessed using MBR must inherit from the MarshalbyRefObject class. Its most important members, InitializeLiIfetimeServices and GetLifeTimeServices, create and retrieve objects that are used to control how long a remoting object is kept alive on the server. Managing the lifetime of an object is a key feature of remoting and is discussed later in this section.

MarshalByRefObjects come in two flavors: client-activated objects (CAO) and server-activated objects (SAO)梐lso commonly referred to as well-known objects (WKO). Server-activated objects are further separated into single call and singleton types. A server may implement both client-activated and server-activated objects. It's up to the client to choose which one to use. If the client selects SAO, the server makes the determination as to whether to use server-activated single call or server-activated singleton objects.

The choice of activation mode profoundly affects the overall design, performance, and scalability of a remoting application. It determines when objects are created, how many objects are created, how their lifecycle is managed, and whether objects maintain state information. Let's look at the details.

Client-Activated Objects

The use and behavior of a client-activated object (CAO) resembles that of a locally created object. Both can be created using the new operator; both may have parameterized constructors in addition to their default constructor; and both maintain state information in properties or fields. As shown in Figure 14-5, they differ in that the CAO runs on a host in a separate application domain and is called by a proxy.

Figure 14-5. Client-activated objects: client retains control of object


The fact that the object resides in another AppDomain means that it is subject to Garbage Collection there and can be destroyed even though the remote client is still using it. .NET handles this potential problem by assigning a lease to each object that can be used to keep it alive. Leases are discussed in detail later in this chapter.

Server-Activated Objects

A server-activated object (SAO) may be implemented as a singleton or single call object. The former is best suited for sharing a single resource or collaborative operation among multiple users. Examples include a chat server and class factory. Single call mode is used when clients need to execute a relatively short operation on the server that does not require maintaining state information from one call to the next. This approach is the most scalable solution and has the added advantage of working well in an environment that uses load balancing to direct calls to multiple servers.

Server-Activated Singleton

Figure 14-6 illustrates how a single object is used to handle all calls in singleton-based design. The server creates the object when the first client attempts to access it梟ot when it tries to create it. Because the object is created only once, efforts by other clients to create an instance of it are ignored; instead, they are all given a reference to the same singleton object. Each time a client invokes the object, the CLR allocates a new thread from the thread pool. For this reason, it is the responsibility of the developer to ensure the server code is thread-safe. This also limits scalability because there is usually only a finite number of threads available.

Figure 14-6. Server-activated singleton object: one object handles all calls


Server-Activated Single Call

In single call activation mode, the server creates a new object each time a call is made on an object. After the call has been handled, the object is deactivated. Figure 14-7 illustrates how multiple calls are handled: The first call has been completed and the object created for it destroyed; at this point, the proxy of the client that made this call references a null value. The second call comes from another client and results in the creation of the second server object. Finally, the first client makes its second call and a third object is created.

Figure 14-7. Server-activated single call: one object is created for each request


The advantage of single call activation is that resources are made available as soon as a call is completed. This is in contrast to the client-activated call where the client holds on to the resources until it has finished using the object. The disadvantage of the single call is that it does not inherently maintain state information between calls. If you do want to take advantage of single call scalability, but require that information about previous calls be maintained, you can design the server to maintain its own state information in a file or database.

Type Registration

An application may support multiple activation modes and multiple objects. A client indicates the object(s) it wants to access on a server and whether to use client or server-activation mode. The server, on the other hand, indicates which objects it wants to make available to remote clients and the activation mode that is required to access them. This is done using a mechanism known as type registration. As a complement to channel registration, which tells .NET how to transport messages, type registration specifies the objects that can be remotely accessed and the activation mode to use. It's the final part of the agreement that permits a client in one AppDomain to access objects on a host in another AppDomain.

Registering Server-Activated Objects

A host assembly uses the RegisterWellKnowServiceType method of the RemotingConfiguration class to register a type. The method has three parameters: the type of the object, a string representing the object's URI (universal resource identifier), and a WellKnownObjectMode enum that indicates whether the object is implemented as a singleton or single call object. This code segment registers a MessageManager object to execute as a singleton.


// Server Registration: Server-Activated Objects

Type ServerType = typeof(SimpleServer.MessageManager);

RemotingConfiguration.RegisterWellKnownServiceType(

      ServerType,                    // Type of Object

      "MyObject",                    // Arbitrary name

      WellKnownObjectMode.Singleton );


Replace Singleton with SingleCall to register the object to run in single call mode.

To access an SAO, a client uses the RegisterWellKnownClientType method. It takes two parameters: the object type and a string containing the URL where the object can be located. Note that the client does not have any say in whether it uses a singleton or single call object梚t uses whichever the server provides.


// Client Registration: Server-Activated Objects

Type ServerType = typeof(SimpleServer.MessageManager);

string url= "http://localhost:3200/MyObject";

// Register type for Server Activation Mode

RemotingConfiguration.RegisterWellKnownClientType(

      ServerType,

      url);

MessageManager mm = new MessageManager();


When the client uses new to create an instance of the object, .NET recognizes that the object is registered and uses its URL to locate it.

Registering Client-Activated Objects

A host uses the RegisterActivatedServiceType method to register a CAO. It requires only one parameter梩he object type:


// Server Registration: Client-ativated Objects

Type ServerType = typeof(ImageServer);  // ImageServer class

RemotingConfiguration.RegisterActivatedServiceType(

      ServerType);


The client registration is almost as easy. It invokes the RegisterActivatedClientType method and passes it the object type and URL where the object can be located:


// Client Registration: Client-ativated Objects

Type ServerType = typeof(ImageServer);

RemotingConfiguration.RegisterActivatedClientType(

      ServerType,

      "tcp://localhost:3201");


Type Registration Using a Configuration File

As with channels, the type registration instructions can be placed in an assembly's configuration file. These two code segments illustrate how to register the SAO from the preceding example on the server and client:


//Server: Register MessageManager object as a singleton

<application >

   <service>

      <wellknown

       mode="Singleton"

       type="SimpleServer.MessageManager, msgserver"

       objectUri="MyObject"/>

   </service>

</application>



//Client: Register MessageManager object on port 3200

<application >

   <client >

      <wellknown

       type="SimpleServer.MessageManager, msgserver"

       url="http://localhost:3200/MyObject" />

   </client>

</application>


Observe that the registration information is represented as attributes in the <wellknown> tag, and that the type attribute denotes the object by its namespace and name as well as the assembly containing it.

Client-activated registration uses an <activated> tag to specify the remote object in both the server and client configuration file.


//Server: Register ImageServer to be client-activated

<application >

   <service >

      <activated type="ImageServer,caoimageserver"/>

   </service>

</application>


The client also includes a url attribute to provide the address of the remote object:


//Client: Register ImageServer to be client-activated

<application >

   <client url="tcp://localhost:3201" >

      <activated type="ImageServer,caoimageserver"/>

   </client>

</application>


Remoting with a Server-Activated Object

With an understanding of how to register channels and types, you're ready to implement a remoting application. Our first example builds an application that permits users to post and retrieve messages from other users. It's based on SAO, and we'll look at two ways to design it. Our second example uses CAO to retrieve images from an image server.

A Message Server Example

The minimum requirements for a remoting application are an assembly containing the client code, and an assembly that runs as a server and provides the implementation code for the remoting objects. As Figure 14-8 shows, the more common model uses three assemblies: a client, a host that performs channel and type registration, and a server that contains code for the objects. This is the model used for our initial message server project.

Figure 14-8. Three assemblies are used in message server remoting example


Before examining the code, we need to clarify the terminology used to describe the assemblies. In this chapter, server refers to an assembly that declares and implements the remote classes; host or listener refers to an assembly that contains the code to perform type and channel registration. If the host and server functions are combined, the assembly is referred to as a server or host/server. In the world of remoting literature, you'll find that some authors reverse this meaning of host and server, whereas others refer to the host as general assembly.

Our message server application consists of three source files that are compiled into the msgserver.dll, msghost.exe, and msgclient.exe assemblies:


csc /t:library       msgserver.cs

csc /r:msgserver.dll msghost.cs

csc /r:msgserver.dll msgclient.cs


Note that the server code is packaged as a library (DLL) and must be referenced by both the host and client during compilation.

Server Assembly

Listing 14-2 contains the code for a MessageManager class that is made available to clients as a server-activated singleton. Aside from the required MarshalByRefObject inheritance, the class is indistinguishable from a non-remoting class. It exposes two methods, SetMessage and FetchMessages, which are used to post and retrieve messages, respectively. A call to SetMessage contains the ID of the sender and recipient along with the message. This information is packaged into an instance of the Envelope class and stored in an array. Clients retrieve messages by invoking FetchMessages with their client ID. The method searches the array of messages and returns a string containing all messages for that ID.

Listing 14-2. Remoting Server

// msgserver.cs  (DLL)

using System;

using System.Collections;

namespace SimpleServer{

   public class MessageManager: MarshalByRefObject

   {

      ArrayList Messages = new ArrayList();



      public MessageManager()

      {

         Console.WriteLine("Message Object Created.");

      }

      // Concatenate all messages and return string to client

      public string FetchMessages(string clientID)

      {

         string msgList= "";

         for (int i=Messages.Count-1;i>=0;i--)

         {

            Envelope env= (Envelope)Messages[i];

            if(env.RecipientID== clientID)

            {

               msgList+= env.SenderID+": "+env.Message+"\n";

               Messages.RemoveAt(i);  // Remove message

            }

         }

         Console.WriteLine("Sending:\n {0}",msgList);

         return(msgList);

      }

      // Accept message from client and store in memory

      public void SetMessage(string msg, string sender,

                             string recipient)

      {

         // Save Message received from client as object

         // in an array

         Envelope env= new Envelope();

         env.Message= msg;

         env.SenderID= sender;

         env.RecipientID= recipient;

         Messages.Add(env);           // add message to array

         Console.WriteLine("Received:\n{0}", msg);

      }

   }

   // Messages are stored as instances of Envelope

   public class Envelope

   {

      public string Message;

      public string SenderID;

      public string RecipientID;

   }

}


Host Assembly

The host assembly, shown in Listing 14-3, performs channel and type registration. The channel is configured to use HTTP over port 3200; and the MessageManager object is designated to run as a singleton. Keep in mind that the port number, which is essentially an address associated with the application, should be greater than 1024 so as not to conflict with reserved port IDs.

Listing 14-3. Remoting Host/Listener

// msghost.cs  (exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using SimpleServer;                       // Namespace of server

namespace SimpleHost

{

   public class MessageHost

   {

      static void Main()

      {

         Console.WriteLine("Host Started.");

         // Channel Registration

         HttpChannel c = new HttpChannel(3200);

         ChannelServices.RegisterChannel(c);

         // Type Registration朥se server-activated object (SAO)

         // Type is specified as (namespace.class)

         Type ServerType = typeof(SimpleServer.MessageManager);

         RemotingConfiguration.RegisterWellKnownServiceType(

            ServerType,                    // Type of Object

            "MyObject",                    // Arbitrary name

            WellKnownObjectMode.Singleton );

         Console.Read();  // Keep host running

      }

   }

}


After registration is completed, this assembly continues running and monitors port 3200 for calls to MessageManager. Any messages received are passed on to the object.

Client Assembly

The code for the client class is shown in Listing 14-4. It is run from the command line and takes an optional parameter that is used as the client ID:


> msgclient 005


The client first registers the type and channel. The latter must specify the same port (3200) as registered by the host assembly. Following registration, an instance of MessageManager is created using the new operator. When the user types an R, the object retrieves messages; to send a message, an S is entered at one prompt and the recipient ID and message at the next prompt.

Listing 14-4. Remoting Client

// msgclient.cs  (exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Collections;

using SimpleServer;   // Namespace of server

namespace SimpleClient

{

   public class MessageClient

   {

      static void Main(string[] args)

      {

         string myID;

         // Client ID is passed as command line argument

         if(args.Length>0) myID= args[0]; else my;

         Console.WriteLine("Client Started.");

         // (1) Channel Registration

         HttpChannel c = new HttpChannel();

         ChannelServices.RegisterChannel(c);

         // (2) Type Registration: SAO using port 3200

         Type ServerType = typeof(SimpleServer.MessageManager);

         string url= "http://localhost:3200/MyObject";

         // Register type for Server Activation Mode

         RemotingConfiguration.RegisterWellKnownClientType(

               ServerType,url);

         // (3) Create instance of Remote Object

         MessageManager mm = new MessageManager();

         string msg;

         string oper="";

         // Allow user to send or receive a message

         while(oper !="Q")

         {

            Console.WriteLine("(S)end, (R)eceive, (Q)uit");

            oper= Console.ReadLine();

            oper = oper.ToUpper();

            if(oper=="S"){

              Console.WriteLine("enter Recipient ID: messsage");

               msg= Console.ReadLine();

               // : Separates ID and message

               int ndx= msg.IndexOf(":");

               if(ndx>0) {

                 string recipientID=msg.Substring(0,ndx).Trim();

                  msg= msg.Substring(ndx+1);

                  mm.SetMessage(msg, myID, recipientID);

               }

            } else

            {

               if (oper=="R"){

                  Console.WriteLine(mm.FetchMessages(myID));

               }

            }

         }   // while

      }   // method

   }  // class

}  // namespace


Figure 14-9 shows the interactive dialog on the client screen and the corresponding output on the server/host screen. Observe that Message Object Created, which is inside the constructor, occurs when the first call is made to the remote object梟ot when the host begins executing. Also, the constructor is only executed once because this is a singleton object.

Figure 14-9. Client interacting with remote MessageServer configured as singleton


MessageServer Configuration Files

Both msghost and msgclient contain code to perform explicit channel and type registration. As mentioned previously in this section, an alternative is to place channel and/or type registration information in an assembly's configuration file. The in-line registration code is then replaced with a call to a static Configure method that reads the configuration file and performs the registration:


RemotingConfiguration.Configure("MsgHost.exe.config");


Here is how the configuration for msghost.exe is expressed in XML:


//msghost.exe.config

<configuration>

   <system.runtime.remoting>

       <application name = "SimpleHost">

          <service>

            <wellknown

             mode="Singleton"

             type="SimpleServer.MessageManager, msgserver"

             objectUri="MyObject"/>

          </service>

          <channels>

                   <channel ref="http" port="3200"/>

          </channels>

      </application>

   </system.runtime.remoting>

</configuration>


If you compare this with the source code, it's obvious that information in the wellknown tag corresponds to the parameters in the RegisterWellKnownServiceType method; and the channel tag provides information encapsulated in the HttpChannel object. The client configuration file shows a similar correspondence between these tags and source code:


//msgclient.exe.config

<configuration>

   <system.runtime.remoting>

       <application name = "SimpleClient">

          <client >

            <wellknown

             type="SimpleServer.MessageManager, msgserver"

             url="http://localhost:3200/MyObject" />

          </client>

          <channels>

               <channel ref="http"/>

          </channels>

      </application>

   </system.runtime.remoting>

</configuration>


Note that the file contains no tag to specify the type of formatting to be used梥o the default applies. You can specify a different formatter by extending the channel block:


<channel ref="http">

   <clientProviders>

      <formatter ref="Binary" />

   </clientProviders>

</channel>


Using an Interface Assembly with Server-Activated Objects

In the preceding example, the msgserver assembly is packaged as a DLL and must be deployed on both the host and client machine. On the server, it provides the actual code that is executed when a call to the remote object occurs. The client requires the assembly's metadata to compile and build a proxy at runtime, but has no use for the Intermediate Language (IL). Rather than placing this code on each client's machine梕xposing it to anyone with a disassembler梩he application can be redesigned to use an interface assembly to provide the metadata required by the client.

To do this, we'll combine the code in the msghost and msgserver file to create an assembly that both implements the object and performs registration. The third assembly, now called msggeneral, contains an interface that defines the methods supported by the MessageServer class (see Figure 14-10). The client then gets its metadata from this lightweight assembly rather than the full server assembly.

Figure 14-10. MessageServer redesigned to use an interface to provide remote object metadata


The simple interface shown here defines the signature of the two methods exposed by the remote class.


// msggeneral.cs  (DLL)

namespace SimpleServer

{

   // Define an interface to provide method descriptions

   // used by client and implemented by server.

   public interface IMessageManager

   {

      string FetchMessages(string clientID);

      void   SetMessage(string message, string sender,

                        string recipient);

   }

}


The server assembly contains the same object implementation code as in the first example. However, it now inherits from the newly defined IMessageManager interface and includes the registration code from the host/listener assembly. This code is contained in a new class StartServer that provides an entry point to the assembly so that it can be compiled into an .exe file.


// msgserverv2.cs (exe)

public class MessageManager: MarshalByRefObject, IMessageManager

// Code for MessageManager and Envelope class goes here ...

// Class to provide entry point to assembly and perform

// registration

class StartServer

{

   static void Main()

   {

      Console.WriteLine("Host Started.");

      // Channel Registration

      HttpChannel c = new HttpChannel(3200);

      ChannelServices.RegisterChannel(c);

      // Type Registration

      Type ServerType = typeof(SimpleServer.MessageManager);

      RemotingConfiguration.RegisterWellKnownServiceType(

         ServerType,    // Type of Object

         "MyObject",    // Arbitrary name

         WellKnownObjectMode.Singleton);

      Console.Read();

   }

}


Changes are required in the client code to account for the fact that the description of the remote object now comes from an interface; and because it's not possible to directly instantiate an interface, another way must be found to gain a reference to the remote object. The solution is to use Activator.GetObject to return an instance of the interface. This is an important point: .NET returns interface information to the client but creates the actual object on the server where it runs as a server-activated object. As this code segment illustrates, Activator.GetObject performs registration and returns an object in one step:


Type ServerType = typeof(IMessageManager);

// Activate Remote Object and perform registration

object remoteObj = Activator.GetObject(

      ServerType,

      "http://localhost:3200/MyObject");

IMessageManager mm = (IMessageManager) remoteObj;


No other changes are required in the msgclient.cs code.

The three source files are compiled into the msgserver.exe, msggeneral.dll, and msgclient.exe assemblies:


csc /t:library         msggeneral.cs

csc /r:msggeneral.dll  /out:msgserver.exe msgserverv2.cs

csc /r:msggeneral.dll  msgclient.cs


Remoting with a Client-Activated Object (CAO)

In the preceding server-activated singleton example, each call to the remote object is handled by the same object. In the client-activated model, each client creates its own object and can make multiple calls to that object (refer to Figure 14-5 on page 650). The model does not scale particularly well because a large number of concurrent client-created objects can deplete system resources. However, when the expected number of users is small and there is a need to maintain state information between calls, this model should be considered.

An Image Server Example

This project implements a remote server that serves up images to clients upon request. It loads the requested file from local storage and delivers it as a byte stream to the client. After being received by the client, the bytes are then reassembled into the original image.[2]

[2] For comparison, this image server is implemented in Chapter 18, "XML Web Services," as a Web Service.

Prior to examining the code, let's consider the rationale behind implementing the server using client activation, rather than one of the server-activation modes. A singleton can be ruled out; because each client's request is independent of any other client's request, there is no need to maintain state information. The choice between single call activation and client activation is less clear-cut and depends on expected client behavior. If a large number of clients requesting a single image are expected, single call is preferable because it manages resources better by discarding the server object as soon as the request is completed. If a client is expected to make several image requests during a session, client activation is preferred. It allows a client to create and reuse one object for several calls梠bviating the need to build and tear down objects with each request.

Host Assembly

The host/listener assembly has the familiar task of registering the channel(s) and type. Because this application revolves around streaming raw bytes of data, binary formatting is selected over SOAP. This does not have to be specified directly, because the choice of the TCP protocol assigns binary formatting by default.

The other noteworthy change from the server-activated example is that RegisterActivatedServiceTypeCode is used for type registration instead of RegisterWellKnownServiceType.


using System.Runtime.Remoting.Channels.Tcp;



// Channel Registration for TCP

TcpChannel c = new TcpChannel(3201);

ChannelServices.RegisterChannel(c);

// Type Registration for CAO

Type ServerType = typeof(ImageServer);

RemotingConfiguration.RegisterActivatedServiceType(

      ServerType);


Server Assembly

Listing 14-5 provides the implementation of the ImageServer class, which exposes two methods: GetFiles and GetMovieImage. The former returns an array containing the name of all available image files. GetMovieImage is the heart of the system. It receives a string containing the name of a requested image, opens the corresponding file as a memory stream, and converts it to an array of bytes that is returned to the client. (See Chapter 5, "C# Text Manipulation and File I/O," for a refresher on memory streams.)

Listing 14-5. Using Remoting to Implement an Image Server

// caoimageserver.cs  (DLL)

using System;

using System.Runtime.Remoting;

using System.Collections;

using System.Drawing;

using System.IO;

public class ImageServer: MarshalByRefObject

{

   // Return list of available images as a string array

   public ArrayList GetFiles()

   {

      ArrayList a = new ArrayList();

      string dir=@"c:\images\";

      foreach(string fileName in Directory.GetFiles(dir))

      {

         // Strip path from file name

         int ndx= fileName.LastIndexOf("\\");

         string imgName = fileName.Substring(ndx+1);

         a.Add(imgName);

      }

      return a;

   }

   // Return requested image as byte stream

   public byte[] GetMovieImage(string imageName)

   {

      int imgByte;

      imageName= "c:\\images\\"+imageName;

      FileStream s = File.OpenRead(imageName);

      MemoryStream ms = new MemoryStream();

      while((imgByte =s.ReadByte())!=-1)

      {

         ms.WriteByte(((byte)imgByte));

      }

      return ms.ToArray();

   }

}


Client Assembly

The registration steps in Listing 14-6 enable the client to communicate with the host using TCP on port 3201. The call to RegisterActivatedClientType, which registers the ImageServer type, corresponds to the RegisterActivatedServiceType call on the host. After registration, the new operator is used to give the client a reference to the remote object (via a proxy).

Listing 14-6. Using a Client-Activated Object to Access ImageServer

// caoimageclient.cs  (.exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

using System.Collections;

using System.Drawing;

using System.IO;

namespace SimpleImageClient

{

   class ImageClient

   {

      static void Main(string[] args)

      {

         // (1) Register channel for TCP/Binary

         TcpChannel c = new TcpChannel();

         ChannelServices.RegisterChannel(c);

         // (2) Register remote type for CAO

         Type ServerType = typeof(ImageServer);

         RemotingConfiguration.RegisterActivatedClientType(

               ServerType,

              "tcp://localhost:3201");

         ImageServer imgMgr=null;

        bool serverOK=true;    // Indicates whether server is up

         // (3) Create instance of remote object

         try{

            imgMgr = new ImageServer();

         } catch (Exception ex) {

            Console.WriteLine(ex.Message);

            serverOK=false;

         }

         if(serverOK)

         {

            string oper="";

            while(oper !="Q")

            {

               Console.WriteLine(

                     "(L)ist files,(R)etrieve,(Q)uit");

               oper= Console.ReadLine();

               oper = oper.ToUpper();

               if(oper=="R"){

                  Console.WriteLine(

                        "Enter image name to retrieve:");

                  string fname= Console.ReadLine();

                // Exception is handled if image cannot be found

                  try

                  {

                     // Request image from server

                     byte[] image = imgMgr.GetMovieImage(fname);

                     MemoryStream memStream = new

                           MemoryStream(image);

                     Console.WriteLine("Image Size: {0}",

                                       memStream.Length);

                     // Convert memory stream to a Bitmap object

                     Bitmap bm = new Bitmap(memStream);

                     // Save image on local system

                     bm.Save("c:\\cs\\"+fname,

                       System.Drawing.Imaging.ImageFormat.Jpeg);

                  } catch (Exception ex) {

                     Console.WriteLine(ex.Message);

                  }

               }

               else

               {

                  if (oper=="L")   // List image file names

                  {

                     try

                     {

                        ArrayList images = imgMgr.GetFiles();

                        for (int i=0;i<images.Count;i++)

                        {

                           Console.WriteLine(images[i]);

                        }

                     } catch (Exception ex) {

                        Console.WriteLine(ex.Message);

                     }

                  }

               }

            }   // while

         }   // serverok

      }   // Main

   }    // class

}    // namespace


The code implementation is based on a simple command-line menu. When L is entered, a list of available images is displayed on the console; when R is entered, the program prompts for the name of a file to be downloaded. This image name is sent to the server, which returns the image梚f it exists梐s an array of bytes. A Bitmap object is created and its Save method is used to store the image on the client's disk.

Deploying the CAO Application

Use the C# compiler to create caoimagelistener.exe, caoimageserver.dll, and caoimageclient.exe:


csc /t:library             caoimageserver.cs

csc /r:caoimageserver.dll  caoimagelistener.cs

csc /r:caoimageserver.dll  caoimageclient.cs


Note that this implementation requires packaging caoimageserver.dll with caoimageclient.exe on the client's machine. As discussed earlier, this exposes the server code on the client machine. For the SAO application described earlier, using an interface implementation solved this problem. A similar approach can be used with client-activated objects, but requires creating a "factory" that returns an interface for the server object. Another solution is to use the .NET SoapSuds utility to extract metadata from the server assembly into a DLL that can be deployed instead of the server assembly. This utility is executed at the command-line prompt:


soapsuds 杋a:caoimageserver 杘a:serverstub.dll 杗owp



-ia:  specifies the input assembly; do not include .dll or .exe

-oa:  specifies the output assembly that will contain the metadata.

-nowp specifies that a nonwrapped proxy is to be created. A wrapped

       proxy can be used only with SOAP channels and is designed

       for working with Web Services.


The output assembly, serverstub.dll, is now referenced as the client is compiled and is deployed on the client's machine along with the client assembly.


csc /r:serverstub.dll caoimageclient.cs


You now have two approaches that can be used to avoid deploying an entire server implementation assembly on the client's machine: an interface assembly or an assembly created from SoapSuds generated metadata. Which is better? In general, the interface approach is recommended. SoapSuds works well, but doesn't work for all cases. For example, if an assembly contains a class that implements ISerializable or has the [Serializable] attribute, it does not generate metadata for any of the class's properties. However, if you have a relatively simple server assembly as in the preceding example, SoapSuds can be used effectively.

Design Considerations in Creating a Distributed Application

One of the first decisions required in designing a remoting application is whether to use server- or client-activated objects. Here are some general guidelines:

  • If the application requires that the server maintain state information梐s was the case in our message server example梐n SAO singleton is the obvious choice. If the application does not require a singleton, do not use it. Scalability and synchronization can be a problem when accommodating a large number of users.

  • If each call to a remote object is independent of other calls, an SAO single call model is the best choice. This is also the most scalable solution because it does not maintain state information and is destroyed as soon as it's no longer needed.

  • The third choice梩he client-activated object梐llows the client to create the object and maintain state information in instance variables between calls. One drawback is that the model does not support the use of a shared interface assembly to provide the metadata required to create a proxy. Instead, a class-factory approach or SoapSuds must be used.

Configuring Assemblies

After the remoting model has been chosen, there remains the choice of how to design the assemblies required on the client and server side of the application. As discussed earlier, the client side of the application requires the client application, plus an assembly, to provide the necessary information for .NET to construct a proxy. The server implementation assembly is not a good choice because it includes code. Alternatives are a metadata assembly provided by the SoapSuds utility or an assembly that includes only an interface for the server class. Figure 14-11 summarizes the most common design choices.

Figure 14-11. Assembly combinations that can be used to deploy a remoting application


Assembly Definitions:

SoapSuds

Metadata

An assembly created by running SoapSuds against a server assembly.

Interface

An assembly containing an interface that defines the server class.

Server Code

A DLL containing the implementation of the remote class.

Server/Host

An .exe file containing code to implement the remote class and perform type and channel registration.

Host

An .exe file that performs type and channel registration.


Each row shown in Figure 14-11 indicates the type of assemblies to be used in an application. For example, the second row shows a possible configuration for a server-activated object design. A DLL containing an interface inherited by the remote class is deployed on the client along with the client's executable assembly; the server side shares the same interface assembly and contains the interface implementation, as well as the registration code in a single server/host assembly.

This figure represents only a starting point in the design process, because many remoting applications are hybrids that may combine both SAO and CAO. However, an understanding of these core design techniques provides the foundation needed to implement and deploy more complex applications.

    Previous Section  < Day Day Up >  Next Section