In addition to being marshaled across context and app domain boundaries, objects can be marshaled across process boundaries, and even across machine boundaries. When an object is marshaled, either by value or by proxy, across a process or machine boundary, it is said to be remoted.
There are two types of server objects supported for remoting in .NET: well-known and client-activated. The communication with well-known objects is established each time a message is sent by the client. There is no permanent connection with a well-known object, as there is with client-activated objects.
Well-known objects come in two varieties: singleton and single-call. With a well-known singleton object, all messages for the object, from all clients, are dispatched to a single object running on the server. The object is created when the server is started and is there to provide service to any client that can reach it. Well-known objects must have a default (parameterless) constructor.
With a well-known single-call object, each new message from a client is handled by a new object. This is highly advantageous on server farms, where a series of messages from a given client might be handled in turn by different machines depending on load balancing.
Client-activated objects are typically used by programmers who are creating dedicated servers, which provide services to a client they are also writing. In this scenario, the client and the server create a connection, and they maintain that connection until the needs of the client are fulfilled.
The best way to understand remoting is to walk through an example. We will build a simple four-function calculator class, like the one used in an earlier discussion on web services (see Chapter 16), that implements the interface shown in Example 19-2.
Option Strict On Imports System Namespace Programming_VBNET Public Interface ICalc Function Add(x As Double, y As Double) As Double Function Subtract (x As Double, y As Double) As Double Function Mult(x As Double, y As Double) As Double Function Div(x As Double, y As Double) As Double End Interface 'ICalc End Namespace 'Programming_VBNET
Save this in a file named ICalculator.vb and compile it into a file named ICalculatorDLL.dll. To create and compile the source file in Visual Studio, create a new project of type Visual Basic .NET Class Library, enter the interface definition in the Edit window, and then select Build->Build Solution on the Visual Studio menubar. Alternatively, if you have entered the source code using Notepad, you can compile the file at the command line by entering:
vbc /t:library ICalculatorDLL.vb
There are tremendous advantages to implementing a server through an interface. If you implement the calculator as a class, the client must link to that class in order to declare instances on the client. This greatly diminishes the advantages of remoting because changes to the server require the class definition to be updated on the client. In other words, the client and server would be tightly coupled. Interfaces help decouple the two objects; in fact, you can later update that implementation on the server, and as long as the server still fulfills the contract implied by the interface, the client need not change at all.
To build the server used in this example, create CalcServer.vb in a new project of type Visual Basic .NET Console Application (be sure to include a reference to ICalc.dll) and then compile it by selecting Build->Build on the Visual Studio menu bar. Or, you can enter the code in Notepad, save it to a file named CalcServer.vb, and enter the following at the command-line prompt:
vbc /t:exe /r:ICalculatorDLL.dll CalcServer.vb
The Calculator class implements ICalc. It derives from MarshalByRefObject so that it will deliver a proxy of the calculator to the client application:
Public Class Calculator Inherits MarshalByRefObject Implements ICalculatorDLL.Programming_VBNET.ICalc
The implementation consists of little more than a constructor and simple methods to implement the four functions.
In this example, you'll put the logic for the server into the Main( ) method of CalcServer.vb.
Your first task is to create a channel. Use HTTP as the transport because it is simple and you don't need a sustained TCP/IP connection. You can use the HTTPChannel type provided by .NET:
Dim chan As New HttpChannel(65100)
|
Notice that you register the channel on TCP/IP port 65100 (see the discussion of port numbers in Chapter 20).
Next, register the channel with the CLR ChannelServices using the static method RegisterChannel:
ChannelServices.RegisterChannel(chan)
This step informs .NET that you will be providing HTTP services on port 65100, much as IIS does on port 80. Because you've registered an HTTP channel and not provided your own formatter, your method calls will use the SOAP formatter by default.
Now you are ready to ask the RemotingConfiguration class to register your well-known object. You must pass in the type of the object you want to register, along with an endpoint. An endpoint is a name that RemotingConfiguration will associate with your type. It completes the address. If the IP address identifies the machine and the port identifies the channel, the endpoint identifies the actual application that will be providing the service. To get the type of the object, you can call the static method GetType( ) of the Type class, which returns a Type object. Pass in the full name of the object whose type you want:
Dim calcType As Type = _ Type.GetType("CalcServer.Programming_VBNET.Calculator")
Also pass in the enumerated type that indicates whether you are registering a SingleCall or Singleton:
RemotingConfiguration.RegisterWellKnownServiceType( _ calcType, "theEndPoint", WellKnownObjectMode.Singleton)
The call to RegisterWellKnownServiceType does not put one byte on the wire. It simply uses reflection to build a proxy for your object.
Now you're ready to rock and roll. Example 19-3 provides the entire source code for the server.
Option Strict On Imports System Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Http Namespace Programming_VBNET ' implement the calculator class Public Class Calculator Inherits MarshalByRefObject Implements ICalculatorDLL.Programming_VBNET.ICalc Public Sub New( ) Console.WriteLine("Calculator constructor") End Sub 'New ' implement the four functions Public Function Add( _ ByVal x As Double, ByVal y As Double) As Double _ Implements ICalculatorDLL.Programming_VBNET.ICalc.Add Console.WriteLine("Add {0} + {1}", x, y) Return x + y End Function 'Add Public Function Subtract( _ ByVal x As Double, ByVal y As Double) As Double _ Implements ICalculatorDLL.Programming_VBNET.ICalc.Subtract Console.WriteLine("Sub {0} - {1}", x, y) Return x - y End Function 'Sub Public Function Mult( _ ByVal x As Double, ByVal y As Double) As Double _ Implements ICalculatorDLL.Programming_VBNET.ICalc.Mult Console.WriteLine("Mult {0} * {1}", x, y) Return x * y End Function 'Mult Public Function Div( _ ByVal x As Double, ByVal y As Double) As Double _ Implements ICalculatorDLL.Programming_VBNET.ICalc.Div Console.WriteLine("Div {0} / {1}", x, y) Return x / y End Function 'Div End Class 'Calculator Public Class ServerTest Public Shared Sub Main( ) ' create a channel and register it Dim chan As New HttpChannel(65100) ChannelServices.RegisterChannel(chan) Dim calcType As Type = _ Type.GetType("CalcServer.Programming_VBNET.Calculator") ' register our well-known type and tell the server ' to connect the type to the endpoint "theEndPoint" RemotingConfiguration.RegisterWellKnownServiceType( _ calcType, "theEndPoint", WellKnownObjectMode.Singleton) ' "They also serve who only stand and wait." (Milton) Console.WriteLine("Press [enter] to exit...") Console.ReadLine( ) End Sub 'Main End Class 'ServerTest End Namespace 'Programming_VBNET
When you run this program, it prints its self-deprecating message:
Press [enter] to exit...
and then waits for a client to ask for service.
The client must also register a channel, but because you are not listening on that channel, you can use channel 0:
Dim chan As New HttpChannel(0) ChannelServices.RegisterChannel(chan)
The client now need only connect through the remoting services, passing a Type object representing the type of the object it needs (in our case, the ICalc interface) and the URI (Uniform Resource Identifier) of the implementing class:
Dim obj As MarshalByRefObject = _ CType(RemotingServices.Connect( _ GetType(ICalculatorDLL.Programming_VBNET.ICalc), _ "http://localhost:65100/theEndPoint"), _ MarshalByRefObject)
In this case the server is assumed to be running on your local machine, so the URI is http://localhost, followed by the port for the server (65100), followed in turn by the endpoint you declared in the server (theEndPoint).
The remoting service should return an object representing the interface you've requested. You can then cast that object to the interface and begin using it. Because remoting cannot be guaranteed (the network might be down, the host machine may not be available, and so forth), you should wrap the usage in a try block:
Try Dim calc As ICalculatorDLL.Programming_VBNET.ICalc = obj Dim sum As Double = calc.Add(3.0, 4.0) Dim difference As Double = calc.Subtract(3, 4) Dim product As Double = calc.Mult(3, 4) Dim quotient As Double = calc.Div(3, 4) Console.WriteLine("3+4 = {0}", sum) Console.WriteLine("3-4 = {0}", difference) Console.WriteLine("3*4 = {0}", product) Console.WriteLine("3/4 = {0}", quotient) Catch ex As System.Exception Console.WriteLine("Exception caught: ") Console.WriteLine(ex.Message) End Try
You now have a proxy of the Calculator operating on the server, but usable on the client, across the process boundary and, if you like, across the machine boundary. Example 19-4 shows the entire client (to compile it, you must include a reference to ICalc.dll as you did with CalcServer.vb).
Option Strict On Imports System Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Http Namespace Programming_VBNET Public Class CalcClient Public Shared Sub Main( ) Dim myIntArray(3) As Integer Console.WriteLine("Watson, come here I need you...") ' create an Http channel and register it ' uses port 0 to indicate won't be listening Dim chan As New HttpChannel(0) ChannelServices.RegisterChannel(chan) ' get my object from across the http channel ' uses GetType operator Dim obj As MarshalByRefObject = _ CType(RemotingServices.Connect( _ GetType(ICalculatorDLL.Programming_VBNET.ICalc), _ "http://localhost:65100/theEndPoint"), _ MarshalByRefObject) Try ' cast the object to our interface Dim calc As ICalc.Programming_VBNET.ICalc = obj ' use the interface to call methods Dim sum As Double = calc.Add(3.0, 4.0) Dim difference As Double = calc.Subtract(3, 4) Dim product As Double = calc.Mult(3, 4) Dim quotient As Double = calc.Div(3, 4) ' print the results Console.WriteLine("3+4 = {0}", sum) Console.WriteLine("3-4 = {0}", difference) Console.WriteLine("3*4 = {0}", product) Console.WriteLine("3/4 = {0}", quotient) Catch ex As System.Exception Console.WriteLine("Exception caught: ") Console.WriteLine(ex.Message) End Try End Sub 'Main End Class 'CalcClient End Namespace 'Programming_VBNET
The server starts up and waits for the user to press Enter to signal that it can shut down. The client starts and displays a message to the console. The client then calls each of the four operations. You see the server printing its message as each method is called, and then the results are printed on the client.
It is as simple as that; you now have code running on the server and providing services to your client.
Using Fully Qualified NamesNote that in Example 19-4 we use the GetType operator. You can also call the GetType( ) method on the TypeClass, but you must pass in a fully qualified name. The correct syntax for a fully qualified type name is: TopNamespace.SubNamespace.ContainingClass+NestedClass, AssemblyName The fully qualified name for the Calc type would therefore be: "ICalculatorDLL.Programming_VBNET.ICalc, ICalc" You could therefore write: Dim calcType As Type = _
Type.GetType( _
"ICalculatorDLL.Programming_VBNET.ICalc, ICalculatorDLL")
Dim obj As MarshalByRefObject = _
CType(RemotingServices.Connect( _
calcType, "http://localhost:65100/theEndPoint"), _
MarshalByRefObject)
Either approach will work; the latter approach allows you to examine the Type object you get back from the Assembly. |
To see the difference that SingleCall makes versus Singleton, change one line in the server's Main( ) method. Here's the existing code:
RemotingConfiguration.RegisterWellKnownServiceType( _ calcType, "theEndPoint", WellKnownObjectMode.Singleton)
Change the object to SingleCall:
RemotingConfiguration.RegisterWellKnownServiceType( _ calcType, "theEndPoint", WellKnownObjectMode.SingleCall)
The output reflects that a new object is created to handle each request:
Press [enter] to exit.. Calculator constructor Calculator constructor Add 3 + 4 Calculator constructor Sub 3 - 4 Calculator constructor Mult 3 * 4 Calculator constructor Div 3 / 4
When you called the RegisterWellKnownServiceType( ) method on the server, what actually happened? Remember that you created a Type object for the Calculator class:
Dim calcType As Type = _ Type.GetType("CalcServer.Programming_VBNET.Calculator")
You then called RegisterWellKnownServiceType( ), passing in that Type object along with the endpoint and the Singleton enumeration. This signals the CLR to instantiate your Calculator and then to associate it with an endpoint.
To do that work yourself, you would need to modify Example 19-3, changing Main( ) to instantiate a Calculator and then passing that Calculator to the Marshal( ) method of RemotingServices with the endpoint to which you want to associate that instance of Calculator. The modified Main( ) is shown in Example 19-5 and, as you can see, its output is identical to that of Example 19-3.
Public Shared Sub Main( ) Dim chan As New HttpChannel(65100) ChannelServices.RegisterChannel(chan) Dim calc As Calculator = New Calculator( ) RemotingServices.Marshal(calc, "theEndPoint") ' "They also serve who only stand and wait." (Milton) Console.WriteLine("Press [enter] to exit...") Console.ReadLine( ) End Sub 'Main
The net effect is that you have instantiated a calculator object, and associated a proxy for remoting with the endpoint you've specified.
What is going on when you register this endpoint? Clearly, the server is associating that endpoint with the object you've created. When the client connects, that endpoint is used as an index into a table so that the server can provide a proxy to the correct object (in this case, the Calculator).
If you don't provide an endpoint for the client to talk to, you can instead write all the information about your calculator object to a file and physically give that file to your client. For example, you could send it to your buddy by email, and he could load it on his local computer.
The client can deserialize the object and reconstitute a proxy, which it can then use to access the calculator on your server! (The following example was suggested to me by Mike Woodring of DevelopMentor, who uses a similar example to drive home the idea that the endpoint is simply a convenience for accessing a marshaled object remotely.)
To see how you can invoke an object without a known endpoint, modify the Main( ) method of Example 19-3 once again. This time, rather than calling Marshal( ) with an endpoint, just pass in the object:
Dim myObjRef As ObjRef = RemotingServices.Marshal(calculator)
Marshal( ) returns an ObjRef object. An ObjRef object stores all the information required to activate and communicate with a remote object. When you do supply an endpoint, the server creates a table that associates the endpoint with an ObjRef so that the server can create the proxy when a client asks for it. ObjRef contains all the information needed by the client to build a proxy, and objRef itself is serializable.
Open a file stream for writing to a new file and create a new SOAP formatter. You can serialize your ObjRef to that file by invoking the Serialize( ) method on the formatter, passing in the file stream and the ObjRef you got back from Marshal. Presto! You have all the information you need to create a proxy to your object written out to a disk file. The complete replacement for Main( ) is shown in Example 19-6. You will also need to add two using statements to CalcServer.vb:
Imports System.IO Imports System.Runtime.Serialization.Formatters.Soap
Make sure you add references in your Server project to System.Runtime.Remoting and System.Runtime.Serializaiton.Formatters.Soap.
Public Shared Sub Main( ) ' create a channel and register it Dim chan As New HttpChannel(65100) ChannelServices.RegisterChannel(chan) ' make your own instance and call Marshal directly Dim calculator As New Calculator( ) Dim myObjRef As ObjRef = RemotingServices.Marshal(calculator) Dim fileStream As New FileStream( _ "calculatorSoap.txt", FileMode.Create) Dim soapFormatter As New SoapFormatter( ) soapFormatter.Serialize(fileStream, myObjRef) fileStream.Close( ) ' "They also serve who only stand and wait." (Milton) Console.WriteLine( _ "Exported to CalculatorSoap.txt. Press ENTER to exit...") Console.ReadLine( ) End Sub 'Main
When you run the server, it writes the file calculatorSoap.txt to the disk. The server then waits for the client to connect. It might have a long wait.
You can take that file to your client and reconstitute it on the client machine. To do so, copy the file into the bin directory of your client. Again create a channel and register it. This time, however, open a fileStream on the file you just copied from the server:
Dim fileStream As New FileStream( _ "calculatorSoap.txt", FileMode.Open)
Then instantiate a SoapFormatter and call Deserialize( ) on the formatter, passing in the filename and getting back an ICalc:
Dim soapFormatter As New SoapFormatter( ) Try Dim calc As ICalculatorDLL.Programming_VBNET.ICalc = _ CType( _ soapFormatter.Deserialize(fileStream), _ ICalculatorDLL.Programming_VBNET.ICalc)
You are now free to invoke methods on the server through that ICalc, which acts as a proxy to the calculator object running on the server that you described in the calculatorSoap.txt file. The complete replacement for the client is shown in Example 19-7. You will also need to add two using statements to CalcClient.vb:
Imports System.IO Imports System.Runtime.Serialization.Formatters.Soap
Make sure you add references in your Client project to System.Runtime.Remoting and System.Runtime.Serializaiton.Formatters.Soap.
Option Strict On Imports System Imports System.IO Imports System.Runtime.Serialization.Formatters.Soap Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Http Namespace Programming_VBNET Public Class CalcClient Public Shared Sub Main( ) Dim myIntArray(3) As Integer Console.WriteLine("Watson, come here I need you...") ' create an Http channel and register it ' uses port 0 to indicate you won't be listening Dim chan As New HttpChannel(0) ChannelServices.RegisterChannel(chan) Dim fileStream As New FileStream( _ "calculatorSoap.txt", FileMode.Open) Dim soapFormatter As New SoapFormatter( ) Try Dim calc As ICalculatorDLL.Programming_VBNET.ICalc = _ CType( _ soapFormatter.Deserialize(fileStream), _ ICalculatorDLL.Programming_VBNET.ICalc) ' use the interface to call methods Dim sum As Double = calc.Add(3.0, 4.0) Dim difference As Double = calc.Subtract(3, 4) Dim product As Double = calc.Mult(3, 4) Dim quotient As Double = calc.Div(3, 4) ' print the results Console.WriteLine("3+4 = {0}", sum) Console.WriteLine("3-4 = {0}", difference) Console.WriteLine("3*4 = {0}", product) Console.WriteLine("3/4 = {0}", quotient) Catch ex As System.Exception Console.WriteLine("Exception caught: ") Console.WriteLine(ex.Message) End Try End Sub 'Main End Class 'CalcClient End Namespace 'Programming_VBNET
When the client starts up, the file is read from the disk and the proxy is unmarshaled. This is the mirror operation to marshaling and serializing the object on the server. Once you have unmarshalled the proxy, you are able to invoke the methods on the calculator object running on the server.
Top |