- Introduction
- Understanding .NET Remoting
- Marshaling Objects by Reference
- Marshaling Objects by Value
- Writing to the Event Log
- Handling Remote Events
- Other Remoting Subjects
- Summary
Remoting handles data between client and server in two ways: (1) marshaling the data by reference and (2) marshaling the data by value. Marshaling by reference is analogous to having a pointer, and marshaling by value is analogous to having a copy. If we change a reference object, the original is changed, and changes to a copy have no effect on the original. To get your feet wet let's start with a quick marshal-by-reference example. (The Marshaling Objects by Value section later in this chapter talks about the other way data is moved back and forth.)
NOTE
Occasionally I will be accused of writing or saying something condescending. That is never my intent. That said, depending on your level of comfort with technical jargon, words like marshal may sound ominous. This is an advanced book, but if you are not comfortable with COM and DCOM, the word marshal may trouble you. An easier term might be shepherd, as in herding sheep. Because remoting moves data across a network, the data must be packaged and unpackaged in an agreed-upon format and shepherded between the application that has the data (the server) and the application that wants the data (the client).
Several graphics on the Web depict this relationship—marshaling between client and server—but I am not sure if they aid understanding or add to confusion. Rather than repeat those images, I encourage you to think of the code that does the shepherding as the responsibility of .NET. These codified shepherds are referred to as proxies. The Remoting namespace contains the proxy code.
Hello, Remote World!
Rather than torture you with another Hello, World! application, I will use a sample application with a little more meat (not much but a bit more).
Suppose you work in the information technology department of a large insurance company. This company owns several broker dealers that sell mutual funds. As a result you are tasked with tracking all customer purchases of mutual funds, life and health products, and annuities. You can cobble together a solution that requires the remote broker dealer offices to run batch programs at night that upload data and combine the mutual fund trades with Universal Life payments, mixing and matching the client PC's database programs with your UDB, SQL Server, or Oracle databases. When you are finished you have VB6 applications on the client workstations running ObjectRexx dial-up scripts to FTP servers late at night. Or, you can use remoting and .NET to get everybody working together. Throw out the Perl, .cmd, .bat, and .ftp scripts; toss the various and sundry import and export utilities written in C, VB6, and Modula; and get everything working in real time.
Okay. We won't have enough time to tackle all of that in this section, but we can create a client application that requests a customer and a server application that simulates servicing that request. Because the code would take up a lot of space, we will simulate the client reading from the database. However, after you read Chapters 11, 12, and 16 on ADO.NET, you will be able to incorporate the code to read from the database too. Figure 8.1 shows a UML model of the design we will be using here. (I used Rational XDE, integrated into .NET, to create the UML class diagram.)
Figure 8.1. The class diagram for our server application.
The class diagram accurately depicts the code that resides in the client and server. An assembly named Interface contains the two interfaces: IFactory and ITrade. The assembly named Server implements (realizes in the vernacular of the UML) IFactory and ITrade in Factory and Trade, respectively, and the assembly named Client is dependent on the two interfaces. Note that there is no dependency on the actual implementations of IFactory and ITrade in Client. If all the code on the server were on the client, then arguably the server would not be needed. (This isn't precisely true but logically makes sense.) Listings 8.1 and 8.2 contain the code for the Interface and Server assemblies, in that order.
Listing 8.1 The Interface.vb File Containing the IFactory and ITrade Interfaces
Public Interface IFactory Function GetTrade(ByVal customerId As Integer) As ITrade End Interface Public Interface ITrade Property NumberOfShares() As Double Property EquityName() As String Property EquityPrice() As Double ReadOnly Property Cost() As Double Property Commission() As Double Property RepId() As String End Interface
Listing 8.2 The ServerCode.vb File Containing the Implementation of ITrade and IFactory
Imports System Imports [Interface] Imports System.Reflection Public Class Factory Inherits MarshalByRefObject Implements IFactory Public Function GetTrade( _ ByVal customerId As Integer) As ITrade _ Implements IFactory.GetTrade Console.WriteLine("Factory.GetTrade called") Dim trade As Trade = New Trade() trade.Commission = 25 trade.EquityName = "DYN" trade.EquityPrice = 2.22 trade.NumberOfShares = 1000 trade.RepId = "999" Return trade End Function End Class Public Class Trade Inherits MarshalByRefObject Implements ITrade Private FCustomerId As Integer Private FNumberOfShares As Double Private FEquityName As String Private FEquityPrice As Double Private FCommission As Double Private FRepId As String Public Property NumberOfShares() As Double _ Implements ITrade.NumberOfShares Get Return FNumberOfShares End Get Set(ByVal Value As Double) FNumberOfShares = Value End Set End Property Public Property EquityName() As String _ Implements ITrade.EquityName Get Return FEquityName End Get Set(ByVal Value As String) Console.WriteLine("EquityName was {0}", FEquityName) FEquityName = Value Console.WriteLine("EquityName is {0}", FEquityName) Console.WriteLine([Assembly].GetExecutingAssembly().FullName) End Set End Property Public Property EquityPrice() As Double _ Implements ITrade.EquityPrice Get Return FEquityPrice End Get Set(ByVal Value As Double) FEquityPrice = Value End Set End Property ReadOnly Property Cost() As Double _ Implements ITrade.Cost Get Return FEquityPrice * _ FNumberOfShares + FCommission End Get End Property Property Commission() As Double _ Implements ITrade.Commission Get Return FCommission End Get Set(ByVal Value As Double) FCommission = Value End Set End Property Property RepId() As String _ Implements ITrade.RepId Get Return FRepId End Get Set(ByVal Value As String) FRepId = Value End Set End Property End Class
The code in both listings is pretty straightforward. Listing 8.1 defines the two interfaces IFactory and ITrade. Listing 8.2 provides an implementation for each of these interfaces.
After scanning the code you might assume that all we need to do is add a reference in the client to each of the two assemblies containing the code in Listings 8.1 and 8.2 and we're finished. And you'd be right if we were building a single application. However, we are building two applications: client and server.
Suppose for a moment that we did add a reference to the Interface and Server assemblies. .NET would load all three assemblies—client, interface, and server—into the same application domain (AppDomain), and the client could create Trade and Factory objects directly or by using the interfaces. This is a valid model of programming, but it is not distributed. It works because .NET uses AppDomain for application isolation. All referenced assemblies run in the same AppDomain. However, when we run a client application and a separate server, we have two applications, each running in its own AppDomain. .NET Remoting helps us get data across application domains.
In our distributed example, Client.exe is an executable with a reference to Interface.dll. Both of these assemblies run in the AppDomain for Client.exe. Server.exe also has a reference to Interface.dll, and Server.exe and Interface.dll run in the AppDomain for Server.exe. The code we have yet to add is the code that creates the object on the client by making a remote request to the server.
Getting Client and Server Talking
Thus far we have written vanilla interface and class code. To get the client and server talking we have to use some code in the System.Runtime.Remoting namespace. The first step is to inherit from MarshalByRefObject. Listing 8.2 shows that both Factory and Trade inherit from MarshalByRefObject, which enables the classes to talk across application boundaries. The second piece of the puzzle is to tell the server to start listening, permitting the client to start making requests.
Listing 8.3 contains the code that instructs the server to start listening, and Listing 8.4 contains the code to get the client to start making requests. Both client and server are implemented as console applications (.exe) for simplicity. You can use .NET Remoting with a variety of hosting styles. (Refer to the Choosing a Host for Your Server subsection near the end of this chapter for more information.)
Listing 8.3 Telling the Server Application to Begin Listening for Requests
1: Imports System.Runtime.Remoting 2: Imports System.Runtime.Remoting.Channels 3: Imports System.Runtime.Remoting.Channels.Http 4: 5: Public Class Main 6: 7: Public Shared Sub Main(ByVal args() As String) 8: 9: Dim channel As HttpChannel = New HttpChannel(9999) 10: ChannelServices.RegisterChannel(channel) 11: RemotingConfiguration.RegisterWellKnownServiceType( _ 12: GetType(Factory), "Factory.soap", _ 13: WellKnownObjectMode.Singleton) 14: 15: RemotingConfiguration.RegisterWellKnownServiceType( _ 16: GetType(Trade), "Trade.soap", _ 17: WellKnownObjectMode.Singleton) 18: 19: Console.WriteLine("Server is running...") 20: Console.ReadLine() 21: Console.WriteLine("Server is shutting down...") 22: End Sub 23: 24: End Class
From the code and the shared Main method you can tell that Listing 8.3 comes from a .NET console application. Lines 1 through 3 import namespaces relevant to remoting.
The first thing we need to do is declare a channel. I elected to use the HTTP protocol, and the HttpChannel constructor takes a port number. This is the port number on which the server will listen. If you want the server to automatically choose an available port, send 0 to the HttpChannel constructor. There are about 65,500 ports. If you want to specify a port number, just avoid obvious ports that are already in use like 80 (Web server), 23 (Telnet), 20 and 21 (FTP), and 25 (mail). Picking a port that is being used by another application will yield undesirable results. After we have elected a channel we need to call the shared method RegisterChannel (line 10).
Next we register the server as a well-known service type. (Inside the CLR there is a check to make sure that the service inherits from MarshalByRefObject.) We pass the Type object of the type to register, the Uniform Resource Identifier (URI) for the service, and the way we want the service instantiated. When you read URI, think URL. The URI identifies the service; by convention we use the class name and .soap or .rem for the URI. You can use any convention, but Internet Information Services (IIS) maps the .soap and .rem extensions to .NET Remoting. This is important when hosting remote servers in IIS. (Refer to the Choosing a Host for Your Server subsection near the end of this chapter.) You can pass the WellKnownObjectMode.Singleton or WellKnownObjectMode.SingleCall enumerated values to the registration method. Singleton is used to ensure that one object is used to service requests, and SingleCall will cause a new object to be created to service each request. (SingleCall causes a remoted server to respond like a Web application. The server has no knowledge of previous calls.)
The Factory type is registered in lines 11 through 13 and the Trade type in lines 15 through 17. After the server types are registered we use Console.ReadLine to prevent the server from exiting. To quit the server application, set the focus on the console running the server and hit the carriage return; the server will respond until then. Listing 8.4 contains the code that prepares the client to send requests to the server.
Listing 8.4 Preparing the Client Application to Begin Making Requests
1: Private Sub Form1_Load(ByVal sender As System.Object, _ 2: ByVal e As System.EventArgs) Handles MyBase.Load 3: 4: Dim channel As HttpChannel = New HttpChannel() 5: ChannelServices.RegisterChannel(channel) 6: 7: Dim instance As Object = _ 8: Activator.GetObject(GetType(IFactory), _ 9: "http://localhost:9999/Factory.soap") 10: 11: Dim factory As IFactory = _ 12: CType(instance, IFactory) 13: 14: Dim trade As ITrade = _ 15: factory.GetTrade(1234) 16: 17: End Sub
The client declares, creates, and registers a channel in lines 4 and 5. We don't need the port here when we register the channel; we will indicate the port when we request an instance of the object from the server. Lines 7 through 9 use the shared Activator.GetObject class to request an instance of the Factory class defined in the server. The URL (line 9) indicates the domain and port of the server and the name we registered the server with. Lines 11 and 12 convert the instance type returned by Activator to the interface type we know it to be, and lines 14 and 15 use the factory instance to request a Trade object.
To see that the value of the trade object (line 14) is actually a proxy, place a breakpoint in line 17 and use QuickWatch to examine the value of the trade variable (Figure 8.2).
Figure 8.2. The local variable trade is an instance of the TransparentProxy class, indicating the unusual remoted relationship between client and server.
Using Server-Activated Objects
In the example above we created what is known as a server-activated object (SAO). When you construct an SAO—for example, with Activator.GetObject—only a proxy of the object is created on the client. The actual object on the server isn't created until you invoke an operation on that type via the proxy. (The proxy is transparent; thus the invocation occurs in the background when you call a method or access a property.) The lifetime of an SAO is controlled by the server, and only default constructors are called.
In a production application it is more than likely that you will want to permit the operator to manage the configuration of the server without having to recompile the server application. This can be handled in an application configuration file. Example3\Client.sln defines an application configuration file for server.vbproj. You can add an application configuration file by accessing the File|Add New Item menu in Visual Studio .NET and selecting the Application Configuration File template from the Add New Item dialog. Listing 8.5 contains the externalized XML settings used to register the server. The revision to the Main class in Listing 8.3, which accommodates the application configuration file, is provided in Listing 8.6.
Listing 8.5 An Application Configuration That Externalizes Server Registration Settings
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="8080" /> </channels> <service> <wellknown mode="Singleton" type="Server.Factory, Server" objectUri="Factory.soap" /> </service> </application> </system.runtime.remoting> </configuration>
The first statement describes the XML version and the text encoding (8-bit Unicode in the example). The configuration element indicates this is a configuration file. Typically, XML elements have an opening tag—for example, <configuration>—and a matching closing tag with a whack (/) inside the tag. Sometimes this is abbreviated to />, as demonstrated with the wellknown element in Listing 8.5.
The third element indicates the relevant namespace, system.runtime.remoting. The channel element indicates the channel type and port. The wellknown element indicates the WellKnownObjectMode (Singleton in the example), the type information for the type we are registering (Server.Factory, in Listing 8.6), and the URI (Factory.soap). This is precisely the same information we provided in Listing 8.3, programmatically. Now, however, if we find that port 8080 is in use by a proxy server or another HTTP server, we can reconfigure the channel without recompiling.
Having modified the server application to store the server registration information in the .config file, we can modify Listing 8.3 to simplify the registration of WellKnownServiceType. Listing 8.6 shows the shorter, revised code.
Listing 8.6 Revised Code after Moving Registration Settings to Server.exe.config
Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Http Public Class Main Public Shared Sub Main(ByVal args() As String) RemotingConfiguration.Configure("Server.exe.config") Console.WriteLine("Server is running...") Console.ReadLine() Console.WriteLine("Server is shutting down...") End Sub End Class
In the example we have removed the channel construction and calls to the shared method RemotingConfiguration.RegisterWellKnownServiceType that appeared in Listing 8.3. All we need to do now is pass the name of our .config file to the RemotingConfiguration.Configure method in Listing 8.6.
Keep in mind that when you add the Application Configuration File template to your project you will see an App.config file in the Solution Explorer with the rest of your source. When you compile your application, the applicationname.exe.config file is written to the directory containing the executable. While in the debug configuration mode, for example, you will see the Server.exe.config file written to the .\bin directory.
Using Client-Activated Objects
Client-activated objects (CAOs) are registered and work a bit differently than server-activated objects. A CAO is created on the server as soon as you create an instance of the CAO, which you can do by using Activator.CreateInstance or the New constructor. The biggest difference between SAOs and CAOs is that CAOs do not use shared interfaces; rather, a copy of the shared code must exist on both the client and the server. Deploying code to client and server will mean more binaries on the clients, a more challenging deployment, and possible versioning problems.
To preclude re-reading all the code, I have reused the same Factory and Trade classes for our CAO example. However, I have gotten rid of the interfaces, placed the Factory class in the client (since we don't really need two server-side classes to demonstrate CAO), and shared the Trade class between client and server. Instead of literally sharing the Trade class in a third DLL assembly, I defined the Trade class in the Server assembly and used soapsuds.exe (a utility that ships with VS .NET) to generate the shared DLL. We'll go through each of these steps in the remaining parts of this section. (The code for this section can be found in the Example2\Client.sln solution.)
Implementing the Server for the CAO Example
The Server.vbproj file contains the same Trade class shown in Listing 8.3, so I won't relist that code here. The Factory class has been moved to the client (see the Implementing the Client subsection below). What's different about the server is how we register it. The revision to the Main class is shown in Listing 8.7.
Listing 8.7 Registering a Server for Client Activation
1: Imports System.Runtime.Remoting 2: Imports System.Runtime.Remoting.Channels 3: Imports System.Runtime.Remoting.Channels.Http 4: 5: Public Class Main 6: 7: Public Shared Sub Main(ByVal args() As String) 8: 9: Dim channel As HttpChannel = New HttpChannel(9999) 10: ChannelServices.RegisterChannel(channel) 11: 12: ' Code needed for client activation 13: RemotingConfiguration.ApplicationName = "Server" 14: RemotingConfiguration. _ 15: RegisterActivatedServiceType(GetType(Trade)) 16: 17: Console.WriteLine("Server is running...") 18: Console.ReadLine() 19: Console.WriteLine("Server is shutting down...") 20: End Sub 21: 22: End Class
Registration for client activation is much simpler. We provide a name for the application and register the type we will be remoting. The application name is provided in line 13 and the Trade class is registered in lines 14 and 15 using the shared method RemotingConfiguration.RegisterActivatedServiceType, passing the type of the class to register. Recall that we actually have the implementation of the type—Trade—defined on the server.
That's all we need to do to the server's Main class—change the registration code.
Exporting the Server Metadata for the Trade Class
To construct an instance of a class in the client using the new operator, we need a class. Calling New on an interface—as in Dim T As ITrade = New ITrade()—won't work because interfaces don't have code. You can create a third assembly and share that code in both the client and server, or you can use the soapsuds.exe utility to generate a C# source code or a DLL that can be referenced in your client application. I implemented a batch file mysoapsuds.bat in the Example2\Server\bin directory that will create a DLL named server_metadata.dll. Here is the single command in that batch file.
soapsuds -ia:server -nowp -oa:server_metadata.dll
In this code, soapsuds is the name of the executable. The –ia switch is the name of the input assembly. (Note that the assembly extension—.exe for this example—is left off.). The –nowp switch causes soapsuds to stub out the implementations, permitting a dynamic transparent proxy to handle the method calls. The –oa switch indicates the output assembly name. In the example an assembly named server_metadata.dll will be generated. Next we will add a reference to this assembly in our client application.
Implementing the Client
The client application needs a definition of the interface and the type for client activation. We can actually share the code between client and server and use parameterized constructors for client activation; or, in our example, we use soapsuds.exe to generate a metadata DLL and give up parameterized constructors for remoted objects.
On the user's PC we need some kind of application as well as remoting registration code, and we can use a factory on the client to simulate constructor parameterization (if we are using soapsuds-generated metadata.) As a general rule it is preferable to use soapsuds to generate metadata and a factory for convenience, as opposed to shipping the server executable to every client. Listing 8.8 shows a Windows Forms implementation of the CAO client and a factory for the Trade class.
Listing 8.8 Implementing a Client-Activated Object and a Factory
1: Imports System 2: Imports System.Runtime.Remoting 3: Imports System.Runtime.Remoting.Channels 4: Imports System.Runtime.Remoting.Channels.Http 5: Imports System.Runtime.Remoting.Activation 6: Imports System.Reflection 7: Imports Server 8: 9: Public Class Form1 10: Inherits System.Windows.Forms.Form 11: 12: [ Windows Form Designer generated code ] 13: 14: Private Generator As Generator 15: 16: Private Sub Form1_Load(ByVal sender As System.Object, _ 17: ByVal e As System.EventArgs) Handles MyBase.Load 18: 19: Dim channel As HttpChannel = New HttpChannel() 20: ChannelServices.RegisterChannel(channel) 21: 22: ' Client-activated object code 23: RemotingConfiguration.RegisterActivatedClientType( _ 24: GetType(Trade), _ 25: "http://localhost:9999/Server") 26: 27: Dim Factory As Factory = New Factory() 28: Dim Trade As Trade = Factory.GetTrade(5555) 29: Trade.Commission = 25 30: Trade.EquityName = "CSCO" 31: Trade.EquityPrice = 11.0 32: Trade.NumberOfShares = 2000 33: Trade.RepId = 999 34: 35: Generator = New Generator(Me, _ 36: GetType(Trade), Trade) 37: Generator.AddControls() 38: 39: End Sub 40: 41: End Class 42: 43: Public Class Factory 44: 45: Public Function GetTrade( _ 46: ByVal customerId As Integer) As Trade 47: Console.WriteLine("Factory.GetTrade called") 48: 49: Dim trade As Trade = New Trade() 50: trade.CustomerId = 555 51: trade.Commission = 25 52: trade.EquityName = "DYN" 53: trade.EquityPrice = 2.22 54: trade.NumberOfShares = 1000 55: trade.RepId = "999" 56: 57: Return trade 58: End Function 59: 60: End Class
Listing 8.8 contains two classes: the Windows Forms class Form1 and the Factory class. The Form1 class creates and registers a channel in lines 19 and 20. Instead of using the Activator class to create the remote object, we call the shared method RemotingConfiguration.RegisterActivatedClientType, passing the type to register and the URI of the server-registered type (lines 23 through 25).
After the type that can be activated on the server is registered, we use the Factory class to create an instance of that type (lines 27 and 28). To provide you with additional calls to the server, I changed the values set by the Factory class. There is no requirement here, just extra code.
The Generator class used on lines 35 through 37 is extra code I added to create a Windows user interface. This code is included with the downloadable remoting example and creates a simple user interface comprised of text boxes and labels, created by reflecting the remote type.