Microsoft .NET Remoting (Pro-Developer) - page 16

Summary

In this chapter, we took a high-level view of each of the major architectural components and concepts of the .NET Remoting infrastructure. Out of the box, .NET Remoting supports distributed object communications over the TCP and HTTP transports by using binary or SOAP representation of the data stream. Furthermore, .NET Remoting offers a highly extensible framework for building distributed applications. At almost every point in the processing of a remote method call, the architecture allows you to plug in customized components. Chapters 5 through 8 will show you how to exploit this extensibility in your .NET Remoting applications.

Now that we’ve discussed the .NET Remoting architecture, we can proceed to the subject of Chapter 3: using .NET Remoting to build distributed applications.

Chapter 3

Building Distributed Applications with .NET Remoting

In Chapter 2, “Understanding the .NET Remoting Architecture,” we discussed the overall architecture of .NET Remoting, explaining each of the major architectural components that make up the .NET Remoting infrastructure. In this chapter, we’ll show you how to use .NET Remoting to build a distributed job assignment system.

The sample application in this chapter demonstrates how to apply the various aspects of the .NET Remoting technology discussed in Chapter 2 to the distributed application development concepts discussed in Chapter 1, “Understanding Distributed Application Development.” In the second part of this book, we’ll use this application to demonstrate the extensibility of .NET Remoting by developing a custom proxy, channel, and formatter.

In implementing the sample application, we’ll discuss and demonstrate the following .NET Remoting tasks:

  • Defining remote types

  • Hosting remote objects

  • Handling events over .NET Remoting boundaries

  • Publishing and consuming remote objects

  • Exposing a remote object as a Web Service

  • Packaging metadata to minimize dependencies

Designing a Distributed Job Assignment Application

The job assignment application consists of two parts, a server and a client. The client will communicate with only a single server, whereas the server will communicate with any number of clients. The server application must maintain state for all clients as well as perform the following tasks:

  • Allow clients to choose job assignments

  • Allow clients to indicate a job is complete

  • Notify clients of new jobs in real time

  • Track all jobs assigned and completed by each client

The client’s main purpose is data entry; therefore, a user interface is required. The main screen of the user interface should show a list of all jobs currently on the server and should contain controls that allow the user to create, assign, and update jobs. The client should meet the following additional requirements:

  • Allow the user to choose job assignments from available jobs

  • Allow the user to indicate a job is complete

  • Handle real-time notification of new jobs

  • Handle real-time notification of job assignments

Implementing the JobServer Application

The main purpose of the JobServer application is to host our remote object JobServerImpl. Notice in the code listings in this section that the interfaces, structs, and classes do not contain any .NET Remoting references. The unobtrusive nature of .NET Remoting is one of its major strengths.

Although it’s not shown in this chapter, when developing this chapter’s sample code listings we originally started with a simple client/server application that had both the client and server in the same application domain. One benefit of this approach is that you can ensure the proper functioning of your application before introducing more areas that might cause failures. In addition, debugging is easier in a single application domain.

Implementing the JobServer Application Logic

The JobServer application consists of the JobInfo struct, the IJobServer interface, the JobEventArgs class, and the JobServerImpl class. The server application, which we’ll discuss shortly, publishes an instance of the JobServerImpl class as a remote object; the remaining types support the JobServerImpl class.

The JobInfo Struct

The following listing defines the JobInfo struct, which encapsulates a job’s unique identifier, description, assigned user, and status:

public struct JobInfo
{
    public JobInfo(int nID, string sDescription, 
                   string sAssignedUser, string sStatus)
    {
        m_nID           = nID;
        m_sDescription  = sDescription;
        m_sAssignedUser = sAssignedUser;
        m_sStatus       = sStatus;
    }

    public int      m_nID;
    public string   m_sDescription;
    public string   m_sAssignedUser;
    public string   m_sStatus;
}

The IJobServer Interface

The following listing defines the IJobServer interface, which defines how clients interact with the JobServerImpl instance:

public interface IJobServer
{
    event       JobEventHandler JobEvent;
    void        CreateJob(string sDescription);
    void        UpdateJobState(int nJobID, 
                               string sUser, 
                               string sStatus);
    ArrayList   GetJobs();
}

As its name implies, the IJobServer.CreateJob method allows clients to create new jobs by specifying a job description. The IJobServer.UpdateJobState method allows a client to set the job status based on the job identifier and user. The status of a job can be either “Assigned” or “Completed.” The IJobServer.GetJobs method returns a list of all JobInfo instances currently defined.

An IJobServer implementation should raise the JobEvent whenever a client creates a new job or updates the status of an existing job. We’ll discuss implementing the IJobServer interface shortly, in the section “The JobServerImpl Class.”

The JobEventArgs Class

The JobEventArgs class passes new and updated job information to any subscribed JobEvent handlers. We define JobEventArgs as follows:

public class JobEventArgs : System.EventArgs
{
    public enum ReasonCode { NEW, CHANGE };
    private ReasonCode      m_Reason;
    private JobInfo         m_JobInfo;

    public JobEventArgs( JobInfo NewJob, ReasonCode Reason )
    {
        m_JobInfo   = NewJob;
        m_Reason    = Reason;
    }

    public JobInfo Job
    {
        get
        { return m_JobInfo; }

        set
        { m_JobInfo = value; }
    }

    public ReasonCode Reason
    {
        get
        { return m_Reason; }
    }
}

Because our implementation of the IJobServer interface will raise the JobEvent whenever a client adds or updates a job, we’ll use the m_Reason member to indicate whether the client has created or updated the JobInfo instance in m_JobInfo.

Notice in this listing that the JobEventArgs class derives from System.Event­Args. Deriving from System.EventArgs isn’t a requirement, but it’s recommended if the event sender needs to convey event-specific information to the event receiver. We’ll discuss the JobEventArgs class in more detail in later sections of this chapter.

The JobServerImpl Class

The JobServerImpl class is the main class of the JobServer application, which hosts an instance of this class as a remote object. The following listing shows the JobServerImpl class definition:

public class JobServerImpl : IJobServer
{
    private int        m_nNextJobNumber;
    private ArrayList  m_JobArray;

    public JobServerImpl()
    {
        m_nNextJobNumber = 0;
        m_JobArray       = new ArrayList();
    }

    // Helper function to raise IJobServer.JobEvent
    private void NotifyClients(JobEventArgs args)
    {
        // Defined later...
    }

    // Implement the IJobServer interface.
    public event JobEventHandler    JobEvent;

    public ArrayList GetJobs()
    {
        // Defined later...
    }

    public void CreateJob( string sDescription )
    {
        // Defined later...
    }

    public void UpdateJobState( int    nJobID, 
                                string sUser, 
                                string sStatus )
    {
        // Defined later...
    }
}

The JobServerImpl.m_JobArray member stores each JobInfo instance. The JobServerImpl.m_nNextJobNumber member uniquely identifies each newly created job.

The following listing shows the implementation of the GetJobs method:

public ArrayList GetJobs()
{
    return m_JobArray;
}

Both the CreateJob and UpdateJobState methods rely on a helper method named NotifyClients to raise the JobEvent when a client creates a new job or updates an existing job. The following listing shows the implementation of the NotifyClients method:

private void NotifyClients(JobEventArgs args)
{
    //
    // Manually invoke each event handler to
    // catch disconnected clients.
    System.Delegate[] invkList = JobEvent.GetInvocationList();

    IEnumerator ie = invkList.GetEnumerator();
    while(ie.MoveNext())
    {
        JobEventHandler handler = (JobEventHandler)ie.Current;
        try
        {
            IAsyncResult ar = 
                        handler.BeginInvoke( this, args, null, null);
        }
        catch(System.Exception e)
        {
            JobEvent -= handler;
        }
    }
}

Note that instead of using the simple form of raising the event, the NotifyClients method enumerates over the event’s invocation list, manually invoking each handler. This guards against the possibility of a client becoming unreachable since subscribing to the event. If a client becomes unreachable, the JobEvent invocation list will contain a delegate that points to a disconnected remote object. When the code invokes the delegate, the runtime will throw an exception because it can’t reach the remote object. This prevents the invocation of any remaining delegates in the invocation list and can lead to clients not receiving event notifications. To prevent this problem from occurring, we must manually invoke each delegate and remove any delegates that throw an exception. In production code, it’s better to watch for specific errors so that you can handle them appropriately.

The following listing shows the JobServerImpl class implementation of the CreateJob method, which allows the user to create a new job:

public void CreateJob( string sDescription )
{
    // Create a new JobInfo instance.
    JobInfo oJobInfo = new JobInfo( m_nNextJobNumber,
                                    Description,
                                    "",
                                    "" );
    // Increment the next job number.
    m_nNextJobNumber++;

    // Add the JobInfo instance to our JobArray.
    m_JobArray.Add( oJobInfo );

    // Notify any attached clients of the new job.
    NotifyClients( new JobEventArgs( oJobInfo,
                                     JobEventArgs.ReasonCode.NEW ));
}

The following listing shows the implementation of the UpdateJobState method, which allows clients to update the user and status for a job:

public void UpdateJobState( int nJobID, 
                            string sUser, 
                            string sStatus )
{
    // Get the specified job from the array.
    JobInfo oJobInfo = ( JobInfo ) m_JobArray[ nJobID ];

    // Update the user and status fields.
    oJobInfo.m_sAssignedUser = sUser;
    oJobInfo.m_sStatus = sStatus;

    // Update the array element because JobInfo is a value type.
    m_JobArray[ nJobID ] = oJobInfo;

    // Notify any attached clients of the new job.
    NotifyClients( new JobEventArgs( oJobInfo,
                                     JobEventArgs.ReasonCode.CHANGE));
}

Adding .NET Remoting

So far, we’ve implemented some types without regard to .NET Remoting. Now let’s walk through the steps required to add .NET Remoting to the JobServer application:

  1. Making a type remotable

  2. Choosing a host application domain

  3. Choosing an activation model

  4. Choosing a channel and a port

  5. Choosing how clients will obtain the server’s metadata

  6. Configuring the server for .NET Remoting

Making a Type Remotable

To prepare the JobServerImpl class and its supporting constructs for .NET Remoting, we need to enhance their functionality in several ways. Let’s start with the JobInfo struct. The JobServerImpl class passes JobInfo structure instance information to the client; therefore, the structure must be serializable. With the .NET Framework, making an object serializable is as simple as applying the [serializable] pseudocustom attribute.

Next we must derive the JobServerImpl class, which is our remote object, from System.MarshalByRefObject. As you might recall from Chapter 2, an instance of a type derived from System.MarshalByRefObject interacts with objects in remote application domains via a proxy. Table 3-1 lists the public methods of System.MarshalByRefObject.

Table 3-1. Public Methods of System.MarshalByRefObject

Public Method

Description

CreateObjRef

Virtual method that returns a System.Runtime.Remoting.ObjRef instance used in marshaling a reference to the object instance across .NET Remoting boundaries. You can override this function in your derived types to return a custom version of the ObjRef.

GetLifetimeService

Use this method to obtain an ILease interface reference on the Marshal­ByRefObject instance’s associated lease.

InitializeLifetimeService

The .NET Remoting infrastructure calls this virtual method during activation to obtain an object of type ILease. As explained in Chapter 2 and demonstrated later in this chapter in “Adding a Sponsor to the Lease,” this method can be overridden in derived classes to control the object instance’s initial lifetime policy.

The following listing shows how we override the InitializeLifetimeService method:

public override object InitializeLifetimeService()
{
    return null;
}

Returning null tells the .NET Remoting infrastructure that the object instance should live indefinitely. We’ll see an alternative implementation of this method later in this chapter in the “Configuring the Client for Remoting Client-Activated Objects” section.

Choosing a Host Application Domain

The next step is to decide how to expose instances of JobServerImpl to the client application. The method you choose depends on your answers to the following questions:

  • Will the application run all the time?

  • Will the server and clients be on an intranet?

  • Will the server application provide a user interface?

  • Will you be exposing the remote type as a Web Service?

  • How often will the application run?

  • Will clients have access to the application only through a firewall?

Fortunately, we have many options at our disposal, including the following:

  • Console applications

  • Windows Forms

  • Windows Services

  • Internet Information Services (IIS)/ASP.NET

  • COM+

Because of their simplicity, you’ll probably prefer to use console applications as the hosting environment for doing quick tests and developing prototypes. Console applications are full-featured .NET Remoting hosts, but they have one major drawback for real-world scenarios: they must be explicitly started. However, for testing and debugging, the ability to start and stop a host as well as monitor console output might be just what you want.

.NET Remoting hosts have the same benefits and limitations as console applications, only .NET Remoting hosts have a graphical display. Windows Forms applications are usually thought of as client applications rather than server applications. Using a GUI application for a .NET Remoting host underscores how you can blur lines between client and server. Of course, all the .NET Remoting hosts can simultaneously function as a client and a server or simply as a server.

Production-level hosting environments need to provide a way to register channels and listen for client connections automatically. You might be familiar with the DCOM server model in which the COM Service Control Manager (SCM) automatically launches DCOM servers in response to a client connection. In contrast, .NET Remoting hosts must be running prior to the first client connection. The remaining hosting environments in this discussion provide this capability.

Windows Services make an excellent choice for implementing a constantly available host because they can be configured to start automatically and don’t require a user to be logged on to the machine. The downside of using Windows Services as .NET Remoting hosts is that they require more development effort and require that you run an installation utility to deploy the service.

The simplest .NET Remoting host to write is the one that’s already written: IIS. Because IIS is a service, it’s a constantly running remote object host. IIS also provides some unique features, such as allowing for easy security configuration for remote applications and enabling you to change the server’s configuration file without restarting the host. The biggest drawback of using IIS as a .NET Remoting host is that IIS supports only the HttpChannel (discussed in Chapter 2), although you can increase performance by choosing the binary formatter.

Finally, if you need access to enterprise services, you can use COM+ services to host remote objects. In fact, .NET objects that use COM+ services are automatically remotable because the required base class for all COM+ objects (System.EnterpriseServices.ServicedComponent) ultimately derives from System.MarshalByRefObject. The litmus test for deciding whether to use COM+ as the hosting environment is whether you need access to COM+ services, such as distributed transactions and object pooling. If you don’t need these services, you probably won’t want to incur the performance penalties of running under COM+ services.

To help illustrate various .NET Remoting concepts, we’ve chosen the easiest host to create, run, and debug: a console application for the JobServer application. We’ll use Windows Forms to develop the JobClient application in the next section of the chapter. We’ll also use IIS as the hosting environment later in this chapter when we discuss Web Services.

The following code listing shows the entry point for the JobServer application:

namespace JobServer
{
    class JobServer
    {
        /// <summary>
        /// The main entry point for the application
        /// </summary>
        static void Main(string[] args)
        {
            // Insert .NET Remoting code.

            // Keep running until told to quit.
            System.Console.WriteLine( "Press Enter to exit" );

            // Wait for user to press the Enter key.
            System.Console.ReadLine();
        }
    }
}

As we discuss .NET Remoting issues later in this section, we’ll replace the “Insert .NET Remoting code” comment with code. Near the end of this section, you’ll see a listing of the completed version of the Main method. You’ll be surprised at how simple it remains.

Choosing an Activation Model

In Chapter 2, we discussed the two types of activation for marshal-by-reference objects: server activation and client activation. For our application, we want the client to create the remote object once, and we want the remote object to remain instantiated, regardless of which client created it. Recall from Chapter 2 that the client controls the lifetime of client-activated objects. We clearly don’t want our object to be client activated. This leads us to selecting server activation. We have one more decision to make: selecting an activation mode, Singleton or SingleCall. In Chapter 2, we learned that in SingleCall mode, a separate instance handles each request. SingleCall mode won’t work for our application because it persists data in memory for a particular instance. Singleton mode, however, is just what we want. In this mode, the application creates a single JobServerImpl instance when a client first accesses the remote object.

The .NET Remoting infrastructure provides a class named RemotingConfiguration that you use to configure a type for .NET Remoting. Table 3-2 lists the public members of the RemotingConfiguration class.

Table 3-2. Public Members of System.Runtime. Remoting.RemotingConfiguration 

Member

Member Type

Description

ApplicationId

Read-only property

A string containing a globally unique identifier (GUID) for the application.

ApplicationName

Read/write property

A string representing the application’s name. This name forms a portion of the Uniform Resource Identifier (URI) for remote objects.

Configure

Method

Call this method to configure the .NET Remoting infrastructure by using a configuration file.

GetRegisteredActivatedClientTypes

Method

Obtains an array of all currently registered client-activated types consumed by the application domain.

GetRegisteredActivatedServiceTypes

Method

Obtains an array of all currently registered server-activated types published by the application domain.

GetRegisteredWellKnownClientTypes

Method

Obtains an array of all currently registered server-activated types consumed by the application domain.

GetRegisteredWellKnownServiceTypes

Method

Obtains an array of all currently registered server-activated types published by the application domain.

IsActivationAllowed

Method

Determines whether the currently configured application domain supports client activation for a specific type.

IsRemotelyActivatedClientType

Method

Returns an ActivatedClientTypeEntry instance if the currently configured application domain has registered the specified type for client activation.

IsWellKnownClientType

Method

Returns a WellKnownClientTypeEntry instance if the currently configured application domain has registered the specified type for server activation.

ProcessId

Read-only property

A string in the form of a GUID that uniquely identifies the process that’s currently executing.

RegisterActivatedClientType

Method

Registers a client-activated type consumed by the application domain.

RegisterActivatedServiceType

Method

Registers a client-activated type published by the application domain.

RegisterWellKnownClientType

Method

Registers a server-activated type consumed by the application domain.

RegisterWellKnownServiceType

Method

Registers a server-activated type published by the application domain.

The following code snippet demonstrates configuring the JobServerImpl type as a server-activated type, published with a URI of JobURI and published by using the Singleton activation mode:

RemotingConfiguration.RegisterWellKnownServiceType( 
        typeof( JobServerImpl ), 
        "JobURI", 
        WellKnownObjectMode.Singleton );

Choosing a Channel and a Port

As we stated in Chapter 2, the .NET Framework provides two stock channels, HttpChannel and TcpChannel. Selecting the proper channel transport is generally an easy choice because, in many environments, it makes no difference which transport you select. Here are some influencing factors on channel selection:

  • Whether your channel will transmit through a firewall

  • Whether sending data as plain text raises security concerns

  • Whether you require the .NET Remoting security features of IIS

In Chapter 4 we’ll examine the message flow between client and server. To facilitate this, we’ll use the HttpChannel (which by default uses the SOAPFormatter) so that the messages will be in a human-readable form. The following snippet shows how easy it is to configure a channel:

HttpChannel oJobChannel = new HttpChannel( 4000 );
ChannelServices.RegisterChannel( oJobChannel );

First, we create an instance of the HttpChannel class, passing its constructor the value 4000. Thus, 4000 is the port on which the server listens for the client. Creating a channel object isn’t enough to enable the channel to accept incoming messages. You must register the channel via the static method Channel­Services.RegisterChannel. Table 3-3 lists a subset of the public members of the ChannelServices class; the other public members are used in more advanced scenarios, which we’ll cover in Chapter 7.

Table 3-3. Public Members of System.Runtime.Remoting. Channels.ChannelServices

Member

Member Type

Description

GetChannel

Method

Obtains an object of type IChannel for the registered channel specified by name

GetUrlsForObject

Method

Obtains an array of all the URLs at which a type is reachable

RegisterChannel

Method

Registers a channel for use in the application domain

RegisteredChannels

Read-only property

Gets an array of IChannel interfaces for all registered channels within the application domain

UnregisterChannel

Method

Unregisters a channel for use in the application domain

Choosing How Clients Will Obtain the Server’s Metadata

A client of a remote type must be able to obtain the metadata describing the remote type. The metadata is needed for two main reasons:

  • To enable the client code that references the remote object type to compile

  • To enable the .NET Framework to generate a proxy class that the client uses to interact with the remote object

Several ways to achieve this result exist, the easiest of which is to use the assembly containing the remote object’s implementation. From the perspective of a remote object implementer, allowing the client to access the remote object’s implementation might not be desirable. In that case, you have several options for packaging metadata, which we’ll discuss later in the “Metadata Dependency Issues” section. For now, however, the client will access the JobServerLib assembly containing the JobServerImpl type’s implementation.

Configuring the Server for Remoting

At this point, we’ve programmatically configured the JobServer application for remoting. The following code snippet shows the body of the JobServer application’s Main function:

{
// Register a listening channel.
HttpChannel oJobChannel = new HttpChannel( 4000 );										
ChannelServices.RegisterChannel( oJobChannel );

// Register a well-known type.
RemotingConfiguration.RegisterWellKnownServiceType( 
        typeof( JobServerImpl ), 
        "JobURI", 
        WellKnownObjectMode.Singleton );
}

This looks great, but what if you want to change the port number? You’d need to recompile the server. You might be thinking, “I could just pass the port number as a command-line parameter.” Although this will work, it won’t solve other problems such as adding new channels. You need a way to factor these configuration details out of the code and into a configuration file. Using a configuration file allows the administrator to configure the application’s remoting behavior without recompiling the code. The best part of this technique is that you can replace all the previous code with a single line of code! Look at our new Main function:

{
RemotingConfiguration.Configure( @"..\..\JobServer.exe.config" );
}

Our new version of Main is a single line. All the .NET Remoting configuration information is now in the JobServer.exe.config configuration file.

NOTE
By convention, the name of your configuration file should be the application’s binary file name plus the string .config.

The following code listing shows the JobServer.exe.config configuration file:

<configuration>
    <system.runtime.remoting>
        <application name="JobServer">
            <service>
                <wellknown mode="Singleton" 
                    type="JobServerLib.JobServerImpl, JobServerLib" 
                    objectUri="JobURI" />
            </service>
            <channels>
                <channel ref="http"
                    port="4000" />
            </channels>
        </application>
    </system.runtime.remoting>
</configuration>

Notice the correlation between the configuration file and the remoting code added in the previous steps. The element <channel> contains the same information to configure the channel as we used in the original code snippet showing programmatic configuration. We’ve also replaced the programmatic registration of the well-known object by adding a <wellknown> element entry to the configuration file. The information for registering server-activated objects is under the <service> element. Both the <service> and <channel> elements can contain multiple elements. As you can see from this snippet, the power of configuration files is quite amazing.