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
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
The sample application in this chapter
In implementing the sample application, we ll discuss and demonstrate the following .NET Remoting
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
Allow clients to choose job assignments
Allow clients to
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
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
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
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
public interface IJobServer { event JobEventHandler JobEvent; void CreateJob(string sDescription); void UpdateJobState(int nJobID, string sUser, string sStatus); ArrayList GetJobs(); }
As its
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
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
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
The JobServerImpl Class
The
JobServerImpl
class is the main class of the JobServer application, which
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
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
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:
Making a type remotable
Choosing a host application domain
Choosing an activation model
Choosing a channel and a port
Choosing how clients will obtain the server s metadata
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
|
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
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
.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
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
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
To help
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
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
|
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
|
|
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
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
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
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.
|
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
|
|
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