Excerpt from Professional VB.NET 2003
When we implement an application using remoting, we'll have three key components to the application: the Client (the application calling the server); the Server Library (the DLL containing the objects to be called by the client); and the Host (the application running on the server that hosts remoting and the Server Library).
Basically, we create our server-side objects in a Visual Basic .NET Class Library project. Then, we expose the classes in that DLL from our server-side remoting host application. With the objects exposed on the server, we can then create client applications that call the objects in the Server Library DLL.
We might also have some other optional components to support various scenarios.
|Interface||A DLL containing interfaces that are implemented by the objects in the Server Library|
|Proxy||A DLL containing generated proxy code based on the objects in the Server Library|
|Shared Library||A DLL containing serializable objects that must be available to both the Server Library and the client|
Let's get into some code and see how remoting works.
A Simple Example
To start with, let's create a simple remoting application. It will consist of a library DLL that contains the server-side code, a remoting host application, and a client to call the library DLL on the server.
Both the host and the client need access to the type information that describes the classes in the library DLL. The type information includes the name of the classes in the DLL and the methods exposed by those classes.
The host needs the information because it will be exposing the library DLL to clients via remoting. However, the client needs the information in order to know which objects to create and what methods are available on those objects.
Since we know that the library DLL will be on the server, it is easy enough for the host application to just reference the DLL to get the type information. The client is a bit trickier though, since the library DLL won't necessarily be on the client machine.
There are three options for getting the type information to the client.
|Reference the library DLL||This is the simplest approach, since the client just references the DLL directly and, thus, has all the type information. The drawback is that the DLL must be installed on the client along with the client application|
|Use an interface DLL||This approach is more complex. The classes in the library DLL must implement formal interfaces as defined in this interface DLL. The client can then reference just the interface DLL, so the library DLL doesn't need to be installed on the client machine. The way the client invokes the server is different when using interfaces|
|Generate a proxy DLL||This approach is of moderate complexity. The server must expose the objects via HTTP so we can run the soapsuds.exe command line utility. The utility creates an assembly containing the type information for the library DLL classes exposed by the server. The client then references this proxy assembly rather than the library DLL|
The simplest option to implement is referencing the library DLL directly from the client application.
To begin, let's create the library DLL. This is just a regular
Class Library project, so open Visual Studio .NET (VS.NET) and create a new Class Library named
Remove Class1.vb and add a new class named
Calculator. Since we're creating a wellknown remoting object, it must inherit from
Public Class Calculator Inherits MarshalByRefObject End Class
That's really all there is to it. At this point the
Calculator class is ready to be exposed from a server via remoting. Of course, we need to add some methods that clients can call.
Any and all
Public methods we write in the
Calculator class will be available to clients. How we design the methods depends entirely on whether we plan to expose this class as
SingleCall Singleton, or
SingleCall we know that an instance of
Calculator will be created for each method call, so there's absolutely no point in using any class-level variables. After all, they'll be destroyed along with the object when each method call is complete.
It also means that we can't have the client call a sequence of methods on our object. Since each method call gets its own object, each method call is entirely isolated from any previous or subsequent method calls. In short, each method must stand alone.
For illustration purposes, we need to prove that the server-side code is running in a different process from the client code. The easiest way to prove this is to return the
thread ID where the code is running. We can compare this
thread ID to the
thread ID of the client process. If they are different, then we are sure that the server-side code really is running on the server (or at least in another process on our machine).
Add the following method:
Public Function GetThreadID() As Integer Return AppDomain.GetCurrentThreadId End Function
You can add other
Public methods as well if you'd like, for instance:
Public Function Add(ByVal a As Integer, ByVal b As Integer) As Integer Return a + b End Function
Since this is a calculator class, it only seems appropriate that it should do some calculations.
At this point, we have a simple, but functional,
Calculator class. Build the solution to create the DLL. Our remoting host application will use this DLL to provide the calculator functionality to clients.
With the server-side library complete, we can create a remoting host. Most applications use IIS as a remoting host, but it is quite possible to create a custom host, as well. Let's see how we can create a custom host in a
Console Application for testing.
Most custom hosts are created as a Windows service so the host can run on the server even when no user is logged into the machine. However, for testing purposes a Console Application is easier to create and run.
The advantage to a custom host is that we can host a remoting server on any machine that supports the .NET Framework. This includes Windows 98 and up. If we use IIS as a host, we can only host on Windows 2000 and up, which is a bit more restrictive.
The drawback to a custom host is that it isn't as robust and capable as IIS, at least, not without a lot of work on our part. For our example, in this chapter we're not going to attempt to make our host as powerful as IIS. We'll just stick with the basic process of creating a custom host.
Setting Up the Project
Create a new solution in VS.NET, with a Console Application named
Since the remoting host will be interacting with remoting, we need to reference the appropriate framework DLL. Use the
Add Reference dialog box to add a reference to
System.Runtime.Remoting. as in Figure 1.
Then, in Module1 we need to import the appropriate namespace:
At this point we can configure and use remoting. However, before we do that, we need to have access to the DLL containing the classes we plan to expose via remoting — in our case, this is
Referencing the Library DLL
There are two ways to configure remoting, via a configuration file or via code. If we opt for the configuration file approach then the only requirement is that
SimpleLibrary.dll be in the same directory as our host application. We don't even need to reference
SimpleLibrary.dll from the host. However, if we opt to configure remoting via code then our host must reference
Even if we go with the configuration file approach, referencing
SimpleLibrary.dll from the host project allows VS.NET to automatically keep the DLL updated in our project directory, and it means that any setup project we might create will automatically include
SimpleLibrary.dll. In general, it is a good idea to reference the library DLL from the host and that's what we'll do here.
Add a reference to
SimpleLibrary.dll by clicking the Browse button in the Add References dialog box and navigating to the
SimpleLibrary\bin directory, as shown in Figure 2.
All that remains now is to configure remoting.
The typical way to do this is with a configuration file. Using the Project --> Add New Item menu option, add a new Application Configuration File. Make sure to use the default name of
App.config. That will ensure that VS.NET can automatically copy the file into our bin directory and rename it to
SimpleServer.exe.config so the .NET runtime can find it.
In this config file we'll add a section to configure remoting. Remember that XML is case-sensitive, so the slightest typo here will prevent remoting from being properly configured:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <!- the following section defines the classes we're exposing to clients from this host -> <service> <wellknown mode="SingleCall" objectUri="Calculator.rem" type="SimpleLibrary.Calculator, SimpleLibrary" /> </service> <channels> <channel ref="tcp" port="49341" /> </channels> </application> </system.runtime.remoting> </configuration>
Notice that all our configuration is within the <system.runtime.remoting> element, and then within an <application> element. The real work happens first inside the <service> element. The <service> element tells remoting that we're configuring server-side components. Within this block is where we define the classes we want to make available to clients. We can define both wellknown and activated classes here. In our case we're defining a wellknown class:
<wellknown mode="SingleCall" objectUri="Calculator.rem" type="SimpleLibrary.Calculator, SimpleLibrary" />
The mode will be either
Singleton as we discussed earlier in the chapter.
objectUri is the "end part" of the URL that clients will use to reach our server. We'll revisit this in a moment, but this is basically how it fits (depending on whether we're using the TCP or HTTP protocol):
The ".rem" extension on the
objectUri is important. This extension indicates that remoting should handle the client request, and is used by the networking infrastructure to route the request to the right location. We can optionally use the ".soap" extension to get the same result. The ".rem" and ".soap" extensions are totally equivalent.
Finally, the type defines the full type name and assembly where the actual class can be found. Remoting uses this information to dynamically load the assembly and create the object when requested by a client.
We can have many
<wellknown> blocks here to expose all the server-side classes we want to make available to clients.
The other key configuration block is where we specify which remoting channel (protocol) we want to use. We can choose between the TCP and HTTP channels.
|TCP||Slightly faster than HTTP, defaults to the faster binary serialization of data, can be hard to test|
|HTTP||Slightly slower than TCP, defaults to the slower SOAP serialization of data, is more easily tested|
Since we'll look at the HTTP channel later, we're using the TCP channel now. Either way, we need to specify the IP port number on which we'll be listening for client requests. When choosing a port for a server we should keep the following port ranges in mind:
- 0-1023-Wellknown ports reserved for specific applications such as Web servers, mail servers, and so on
- 1024-49151-Registered ports that are reserved for various widely used protocols such as DirectPlay
- 49152-65535-Intended for dynamic or private use, such as for applications that might be performing remoting with .NET
We're setting remoting to use a TCP channel, listening on port 49341:
<channels> <channel ref="tcp" port="49341" /> </channels>
With the config file created, the only thing remaining is to tell remoting to configure itself based on this information. To do this we need to add code to
Sub Main() RemotingConfiguration.Configure( _ AppDomain.CurrentDomain.SetupInformation.ConfigurationFile) Console.Write("Press <enter> to exit") Console.Read() End Sub
Console.Read statements are there to ensure that the application stays running until we are ready for it to terminate. The line that actually configures remoting is:
RemotingConfiguration.Configure( _ AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
We are calling the
Configure method, which tells remoting to read a config file and to process the
<system.runtime.remoting> element in that file. We want it to use our application configuration file, so we pass that path as a parameter. Fortunately, we can get the path from our AppDomain object so we don't have to worry about hard-coding the filename.
Configuring Remoting via Code
Our other option is to configure the remoting host via code. To do this we'd write different code in
Sub Main() RemotingConfiguration.RegisterWellKnownServiceType( _ GetType(SimpleLibrary.Calculator), _ "Calculator.rem", _ WellKnownObjectMode.SingleCall) System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel( _ New System.Runtime.Remoting.Channels.Tcp.TcpServerChannel(49341)) Console.Write("Press <enter> to exit") Console.Read() End Sub
You can see that we're providing the exact same information here as we did in the config file, only via code. We call
RegisterWellKnownServiceType, passing the mode,
objectUri and type data just as we did in the config file. Then, we call
RegisterChannel, passing a new instance of the
TcpServerChannel configured to use the port we chose earlier.
The result is the same as using the config file. Most server applications use a config file to configure remoting, as it allows us to change things like the channel and port without having to recompile the host application.
Build the solution. At this point our host is ready to run. Open a Command Prompt window, navigate to the
bin directory and run
The final piece of the puzzle is to create a client application that calls the server.
Setting up the Project
Here's how to create a new VS.NET solution with a Windows Application named
SimpleClient. The client needs access to the type information for the classes it wants to call on the server. The easiest way to get this type information is to have it reference
SimpleLibrary.dll. Since we'll be configuring remoting, we also need to reference the remoting dll. Do both via the Add References dialog as shown in Figure 3.
We also need to import the remoting namespace in Form1:
Now, we can write code to interact with the
Calculator class. Add controls to the form as shown in Figure 4.
Name the controls (in order):
RemoteThread. First, let's write the code to get the
thread ID values for each object:
Private Sub LocalThreadButton_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles LocalThreadButton.Click LocalThread.Text = CStr(AppDomain.GetCurrentThreadId) End Sub Private Sub RemoteThreadButton_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles RemoteThreadButton.Click Dim calc As New SimpleLibrary.Calculator RemoteThread.Text = CStr(calc.GetThreadID) End Sub
Displaying the thread ID of the local process is easily accomplished. More interesting though, is that our code to interact with the
Calculator class doesn't look special in any way. Where's the remoting code?
It turns out that there's this idea of location transparency, where it is possible to write "normal" code that interacts with an object whether it is running locally or remotely. This is an important and desirable trait for distributed technologies, and remoting supports the concept. Looking at the code we've written you can't tell if the
Calculator object is local or remoting, its location is transparent.
All that remains is to configure remoting so it knows that the
Calculator object should, in fact, be created remotely. As with the server, we can configure clients either via a config file or through code.
Before we configure remoting, we need to realize something important. If remoting is not configured before our first usage of
SimpleLibrary.Calculator, then the
Calculator object will be created locally. If that happens, configuring remoting won't help and we'll never create remote
To prevent this from happening, we need to make sure we can't interact with the class until after remoting is configured. Typically, this is done by configuring remoting as the application starts up, either in
Sub Main or in the first form's
Load event. In our case, however, we're going to configure remoting behind some buttons, so we need to take a different approach.
Form_Load add the following code:
Private Sub Form1_Load( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load RemoteThreadButton.Enabled = False End Sub
This prevents us from requesting the remote thread. We won't enable this button until after remoting has been configured either through the config file or code.
To configure remoting via a config file, we first need to add a config file to the project. Use the Project -> Add New Item menu to add an Application Configuration File. Make sure to keep the default name of
App.config. In this file add the following code:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <!- the following section defines the classes we're getting from the remote host -> <client> <wellknown mode="SingleCall" type="SimpleLibrary.Calculator, SimpleLibrary" url="tcp://localhost:49341/Calculator.rem" /> </client> </application> </system.runtime.remoting> </configuration>
In this case, we're not using the
<service> element, instead we're using the
<client> element, telling remoting that we're configuring ourselves as a client. Within the
<client> block we define the classes that should be run on a remote server, both wellknown and activated. In our case we have a wellknown class:
<wellknown type="SimpleLibrary.Calculator, SimpleLibrary" url="tcp://localhost:49341/Calculator.rem" />
On the client we only need to provide two bits of information. We need to tell remoting the class and assembly that should be run remotely. This is done with the type attribute, which specifies the full type name and assembly name for the class, just as we did on the server. We also need to provide the full URL for the class on the server.
We defined this URL when we created the server, though it might not have been clear that we did so. When we defined the class for remoting on the server we specified an
objectUri value (
Calculator.rem). Also, on the server we specified the channel (TCP) and port (49341) on which the server will listen for client requests. Combined with the server name itself, we have a URL:
The channel is tcp://, the server name is localhost (or whatever your server name might be), the port is 49341 and the object's URI is
Calculator.rem. This is the unique address of our
SimpleLibrary.Calculator class on the remote server.
As with the server configuration, we might have many
<activated> elements in the config file, one for each server-side object we wish to use.
With the configuration set up, we just need to tell remoting to read the file. We'll do this behind the
Private Sub ConfigureButton_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles ConfigureButton.Click RemotingConfiguration.Configure( _ AppDomain.CurrentDomain.SetupInformation.ConfigurationFile) ConfigureButton.Enabled = False CodeConfigureButton.Enabled = False RemoteThreadButton.Enabled = True End Sub
Once remoting is configured in an application we can't configure it again, so we're disabling the two configuration buttons. Also, we're enabling the button to retrieve the remote thread ID. Now that remoting has been configured it is safe to interact with
The line of code that configures remoting is the same as it was in the server:
RemotingConfiguration.Configure( _ AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
Again, we're telling remoting to read our application configuration file to find the
<system.runtime.remoting> element and process it.
Configuring Remoting via Code
Another option for configuring remoting is to do it via code. We must provide the same information in our code as we did in the config file. Put this behind the
Private Sub CodeConfigureButton_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles CodeConfigureButton.Click RemotingConfiguration.RegisterWellKnownClientType( _ GetType(SimpleLibrary.Calculator), "tcp://localhost:49341/Calculator.rem") ConfigureButton.Enabled = False CodeConfigureButton.Enabled = False RemoteThreadButton.Enabled = True End Sub
RegisterWellKnownClientType method requires that we specify the type of the class to be run remotely, in this case
SimpleLibrary.Calculator. It also requires that we provide the URL for the class on the remote server, just like we did in the config file.
Regardless of whether we configure via code or the config file, the end result is that the .NET runtime now knows that any attempt to create a
SimpleLibrary.Calculator object should be routed through remoting so the object will be created on the server.
Compile and run the application. Try configuring remoting both ways. In either case, you should discover that the local thread ID and the remote thread ID are different, proving that the Calculator code is running on the server, not locally in the Windows application, as shown in Figure 5.
Note that your specific thread ID values will vary from those shown here. The important part is that they are different from each other, establishing that our local code and remote code are running in different places.