Marshaling Objects by Reference

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.

graphics/08fig01.gif

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 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.

graphics/08fig02.gif

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 FileAdd 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.



Visual Basic. NET Power Coding
Visual Basic(R) .NET Power Coding
ISBN: 0672324075
EAN: 2147483647
Year: 2005
Pages: 215
Authors: Paul Kimmel

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