|
|
.NET Remoting configuration files allow you to specify parameters for most aspects of the remoting framework. These files can define tasks as simple as registering a channel and specifying a Type as a server-activated object, or can be as complex as defining a whole chain of IMessageSinks with custom properties.
Instead of writing code like this on the server:
HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl); RemotingConfiguration.RegisterWellKnownServiceType( typeof(CustomerManager), "CustomerManager.soap", WellKnownObjectMode.Singleton);
you can use a configuration file that contains the following XML document to specify the same behavior:
<configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="1234" /> </channels> <service> <wellknown mode="Singleton" type="Server.CustomerManager, Server" objectUri="CustomerManager.soap" /> </service> </application> </system.runtime.remoting> </configuration>
To employ this configuration file in your application, you have to call RemotingConfiguration.Configure() and pass the filename of your *.config file to it.
String filename = "server.exe.config"; RemotingConfiguration.Configure(filename);
Note | As a convention for .NET applications, the configuration filename should be <applicationname>.config, whereas an application filename includes the extension .exe or .dll. |
When using this code in the IDE, remember to put the *.config files in the directory where the application will be built!
Instead of using Activator.GetObject() and passing a URL to it, you can use the new operator after loading the configuration file with RemotingConfiguration.Configure().
In terms of the sample application in Chapter 2, this means that instead of the following call:
ICustomerManager mgr = (ICustomerManager) Activator.GetObject( typeof(ICustomerManager), "http://localhost:1234/CustomerManager.soap");
you might simply use this statement after the configuration file has been loaded:
CustomerManager mgr = new CustomerManager()
And here the problem starts: you need the definition of the class CustomerManager on the client. The interface is not sufficient anymore, because you cannot use IInterface x = new IInterface(), as this would represent the instantiation of an interface, which is not possible.
In Chapter 3, I show you several tools for supplying the necessary metadata in a shared assembly: interfaces, abstract base classes, and SoapSuds-generated metadata-only assemblies. When using configuration files, you won't be able to employ abstract base classes or interfaces—you simply have to resort to SoapSuds-generated metadata.
When your application includes only SAOs/CAOs (and no [Serializable] objects), you're fine with using soapsuds -ia:<assembly> -nowp -oa:<meta_data.dll> to generate the necessary metadata. However, when you are using [Serializable] objects, which not only hold some data but also have methods defined, you need to provide the implementation (the General.dll in the examples) to the client as well.
To see the problem and its solution, take a look at Listing 4-1. This code shows you a [Serializable] class in a shared assembly that will be called General.dll.
Listing 4-1: A Shared [Serializable] Class
using System; namespace General { [Serializable] public class Customer { public String FirstName; public String LastName; public DateTime DateOfBirth; public int getAge() { TimeSpan tmp = DateTime.Today.Subtract(DateOfBirth); return tmp.Days / 365; // rough estimation } } }
On the server side you use the following configuration file, which allows you to write CustomerManager obj = new CustomerManager to acquire a reference to the remote object.
<configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="1234" /> </channels> <service> <wellknown mode="Singleton" type="Server.CustomerManager, Server" objectUri="CustomerManager.soap" /> </service> </application> </system.runtime.remoting> </configuration>
The server itself, which is shown in Listing 4-2, implements aMarshalByRefObject that provides a getCustomer() method, which will return a Customer object by value.
Listing 4-2: The Server-Side Implementation of CustomerManager
using System; using System.Runtime.Remoting; using General; namespace Server { class CustomerManager: MarshalByRefObject { public Customer getCustomer(int id) { Customer tmp = new Customer(); tmp.FirstName = "John"; tmp.LastName = "Doe"; tmp.DateOfBirth = new DateTime(1970,7,4); return tmp; } } class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); String filename = "server.exe.config"; RemotingConfiguration.Configure(filename); // the server will keep running until keypress. Console.WriteLine("Server is running, Press <return> to exit."); Console.ReadLine(); } } }
After compiling, starting the server, and running the SoapSuds command shown in Figure 4-1, you'll unfortunately end up with a little bit more output than expected.
Figure 4-1: SoapSuds command line for extracting the metadata
The Generated_General.dll file (which you can see in Figure 4-2) contains not only the metadata for Server.CustomerManager, but also the definition for General.Customer. This is because SoapSuds generates metadata-only assemblies for all classes and assemblies that are referenced by your server. You'll run into the same problems when referencing parts from mscorlib.dll or other classes from the System.* namespaces.
Figure 4-2: The Generated_General.dll that has been created by SoapSuds
Comparing Generated_Meta.dll to the original General.Customer in General.dll (the one that has been created by compiling the shared project) in Figure 4-3, you can see that although the generated Customer class contains all defined fields, it does not include the getAge() method.
Figure 4-3: The original General.dll contains the method Customer.GetAge().
You can now safely assume that using the Generated_General.dll will not be sufficient for the application; after all, you want access to the getAge() method.
If you try to reference both General.dll and Generated_General.dll in your client application, you will end up with a namespace clash. Both assemblies contain the same namespace and the same class (General.Customer). Depending on the order of referencing the two DLLs, you'll end up with either a compile-time error message telling you that the getAge() method is missing or an InvalidCastException when calling CustomerManager.getCustomer().
To further convince you that shipping the implementation to the client is not the best idea, even if it does solve the preceding problems posed by sharing metadata, I present an alternative solution.
Although SoapSuds can be used to generate DLLs from the WSDL information provided by your server, it can also generate C# code files, including not only the definition but also the required SoapMethodAttributes, so that the remoting framework will know the server's namespace identifier.
To generate code instead of DLLs, you have to specify the parameter -gc instead of -oa:<someassembly>.dll. This command, shown in Figure 4-4, generates one C# source code file for each server-side assembly. You will therefore end up with one file called general.cs and another called server.cs (both are placed in the current directory). The generated server.cs file is shown in Listing 4-3.
Figure 4-4: SoapSuds command line for generating C# code
Listing 4-3: The SoapSuds-Generated server.cs File
using System; using System.Runtime.Remoting.Messaging; using System.Runtime.Remoting.Metadata; using System.Runtime.Remoting.Metadata.W3cXsd2001; namespace Server { [Serializable, SoapType(XmlNamespace="http://schemas.microsoft.com/clr/n sassem/Server/Server%2C%20Version%3D1.0.678.38058%2C%20Culture%3Dneutral%2C% 20PublicKeyToken%3Dnull", XmlTypeNamespace="http://schemas.microsoft.com/clr /nsassem/Server/Server%2C%20Version%3D1.0.678.38058%2C%20Culture%3Dneutral%2 C%20PublicKeyToken%3Dnull")] public class CustomerManager : System.MarshalByRefObject { [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/Ser ver.CustomerManager/Server#getCustomer")] public General.Customer getCustomer(Int32 id) { return((General.Customer) (Object) null); } } }
Generally speaking, these lines represent the interface and the attributes necessary to clearly resolve this remoting call to the server.
Instead of including the Generated_General.dll file (which also contains the namespace General), you can include this C# file in the client-side project or compile it (using the csc.exe stand-alone command line compiler) to a DLL.
Note | When using the command-line compiler csc.exe, you have to specify the /r parameter to add a reference to General.dll. A possible compilation command might be csc /t:library /out:new_generated_meta.dll /r:general server.cs. |
You can now safely reference the shared General.dll, without any namespace clashes, in your project to have access to the Customer class' implementation.
Taking the first sample application in Chapter 2 (the CustomerManager SAO that returns a Customer object by value), I'll show you here how to enhance it to use configuration files.
Assume that on the server side of this application you want an HTTP channel to listen on port 1234 and provide remote access to a well-known Singleton object. You can do this with the following configuration file:
<configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="1234" /> </channels> <service> <wellknown mode="Singleton" type="Server.CustomerManager, Server" objectUri="CustomerManager.soap" /> </service> </application> </system.runtime.remoting> </configuration>
The server-side implementation will simply load the configuration file and wait for <Return> to be pressed before exiting the application. The implementation of CustomerManager is the same as shown previously, and only the server's startup code is reproduced here:
using System; using System.Runtime.Remoting; using General; namespace Server { class ServerStartup { static void Main(string[] args) { String filename = "server.exe.config"; RemotingConfiguration.Configure(filename); Console.WriteLine("Server is running. Press <Return> to exit."); Console.ReadLine(); } } }
The client will consist of two source files, one of them being the previously mentioned SoapSuds-generated server.cs and the second will contain the real client's implementation. It will also have a reference to the shared General.dll.
To allow the client to access the server-side Singleton, you can use the following configuration file to avoid hard coding of URLs:
<configuration> <system.runtime.remoting> <application> <client> <wellknown type="Server.CustomerManager, Client" url="http://localhost:1234/CustomerManager.soap" /> </client> </application> </system.runtime.remoting> </configuration>
Even before I get to the details of configuration files, I want to mention what the attribute "type" in the XML tag <wellknown> contains. In the previous example, you can see that on the server side the tag includes "Server.CustomerManager, Server" and on the client-side it includes "Server.CustomerManager, Client".
The format is generally "<namespace>.<class>, <assembly>", so when a call to the remote object is received at the server, it will create the class Server.CustomerManager from its Server assembly.
Caution | Make sure you do not include the .dll or .exe extension here as the .NET Remoting Framework will not give you any error messages in this case. Instead, your application just won't work as expected. If you want to be sure that you're dealing with a remote object, you can call your object's IsTransparentProxy() method right after creation of the remote reference. |
On the client side the format is a little bit different. The preceding entry, translated into plain English, more or less reads, "If someone creates an instance of the class Server.CustomerManager, which is located in the Client assembly, then generate a remote reference pointing to http://localhost:1234/CustomerManager.soap." You therefore have to specify the client's assembly name in the type attribute of this tag. This differs when using aSoapSuds-generated DLL, in which case you would have to include the name of this generated assembly there.
Caution | When a typo occurs in your configuration file, such as when as when you misspell he assmbly name or the class name, there won't be an exception during "x = new Something()". Instead, you will get a reference to the local object. When you subseuently call methods on it, they will return null. |
The C# code of the client application (excluding the SoapSuds-generated server.cs) is shown in Listing 4-4.
Listing 4-4: The Working Client Application (Excluding server.cs)
using System; using System.Runtime.Remoting; using General; // from General.DLL using Server; // from server.cs namespace Client { class Client { static void Main(string[] args) { String filename = "client.exe.config"; RemotingConfiguration.Configure(filename); CustomerManager mgr = new CustomerManager(); Console.WriteLine("Client.Main(): Reference to CustomerManager" + " acquired"); Customer cust = mgr.getCustomer(4711); int age = cust.getAge(); Console.WriteLine("Client.Main(): Customer {0} {1} is {2} years old.", cust.FirstName, cust.LastName, age); Console.ReadLine(); } } }
When server and client are started, you will see the familiar output shown in Figures 4-5 and 4-6.
Figure 4-5: Client's output when using the configuration file
Figure 4-6: Servers's output when using the configuration file
All .NET configuration files start with <configuration>, and this applies to remoting configuration files as well.
A remoting configuration file basically contains the following structure:
<configuration> <system.runtime.remoting> <application> <lifetime /> <channels /> <service /> <client /> </application> </system.runtime.remoting> </configuration>
Use the <lifetime> tag to configure your object's default lifetime (as discussed in Chapter 3). Valid attributes for the <lifetime> tag are listed here:
ATTRIBUTE | DESCRIPTION |
---|---|
leaseTime | The initial time to live (TTL) for your objects (default is 5 minutes) |
sponsorshipTimeout | The time to wait for a sponsor's reply (default is 2 minutes) |
renewOnCallTime | The time to add to an object's TTL when a method is called (default is 2 minutes) |
leaseManagerPollTime | The interval in which your object's TTL will be checked (default is 10 seconds) |
|
All attributes are optional and may be specified in different time units. Valid units are D for days, H for hours, M for minutes, S for seconds, and MS for milliseconds. When no unit is specified, the system will default to S. Combinations such as 1H5M are not supported.
Here is an example for a very short-lived object:
<lifetime leaseTime="90MS" renewOnCallTime="90MS" leaseManagerPollTime="100MS" />
The <channels> tag contains one or more channel entries. It only serves as a collection for these and doesn't have any XML attributes assigned to it.
To register a server-side TCP channel that listens on port 1234, you can specify the following configuration section:
<channels> <channel ref="tcp" port="1234"> </channels>
The <channel> tag allows you to specify a port number for the server application, to reference custom channels, and to set additional attributes on channels. When you want to use the default HTTP channel or TCP channel, this tag does not have to be specified on the client because these channels will be registered automatically by the framework. On the server, you have to specify at least a port number on which the server-side channel will listen.
You have basically two ways of referencing channels: using a named reference for a predeclared channel or specifying the exact type (namespace, classname, and assembly) for the channel's implementation. Valid attributes for the <channel> tag are as follows:
ATTRIBUTE | DESCRIPTION |
---|---|
ref | Reference for a predefined channel ("tcp" or "http") or reference to a channel that has been defined in a configuration file. |
displayName | Attribute only used for the .NET Framework Configuration Tool. |
type | Attribute that is mandatory when ref has not been specified. Contains the exact type (namespace, classname, assembly) of the channel's implementation. When the assembly is in the GAC, you have to specify version, culture, and public key information as well. For an example of this, see the default definition of HTTP channel in your machine.conf file (which is located in %WINDIR%\Microsoft.NET\Framework\v1.0.3705\CONFIG). |
port | Server side port number. When using this attribute on a client, 0 should be specified if you want your client-side objects to be able to receive callbacks from the server. |
|
In addition to the preceding configuration properties, the HTTP channel, which is created by specifying <channel ref="http">, supports the following entries:
ATTRIBUTE | DESCRIPTION |
---|---|
name | Name of the channel (default is "http"). When registering more than one channel, these names have to be unique or an empty string ("") has to be specified. The value of this attribute can be used when calling ChannelServices.GetChannel(). |
priority | Indicator of the likelihood for this channel to be chosen by the framework to transfer data (default is 1). The higher the integer, the greater the possibility. Negative numbers are allowed. |
clientConnectionLimit | Number of connections that can be simultaneously opened to a given server (default is 2). |
proxyName | Name of the proxy server. |
proxyPort | Port number for your proxy server. |
suppressChannelData | Directive specifying whether the channel will contribute to the ChannelData that is used when creating an ObjRef. Takes a value of true or false (default is false). |
useIpAddress | Directive specifying whether the channel shall use IP addresses in the given URLs instead of using the hostname of the server computer. Takes a value of true or false (default is true). |
listen | Directive specifying whether activation shall be allowed to hook into the listener service. Takes a value of true or false (default is true). |
bindTo | IP address on which the server will listen. Used only on computers with more than one IP address. |
machineName | A string that specifies the machine name used with the current channel. This property overrides the useIpAddress property. |
|
The TCP channel, which is created by specifying <channel ref="tcp"> supports the same properties as the HTTP channel and the following additional property:
ATTRIBUTE | DESCRIPTION |
---|---|
rejectRemoteRequests | Indicator specifying whether the server will accept requests from remote systems. When set to true, the server will not accept such requests, only allowing interapplication communication from the local machine. |
|
On the server side, the following entry can be used to specify an HTTP channel listening on port 1234:
<channels> <channel ref="http" port="1234"> </channels>
On the client, you can specify an increased connection limit using the following section:
<channels> <channel ref="http" port="0" clientConnectionLimit="100"> </channels>
Underneath each channel property, you can configure nondefault client-side and server-side sink providers and formatter providers.
Caution | When any of these elements are specified, it's important to note that no default providers will be created by the system. This means that appending ?WSDL to the URL will only work if you explicitly specify <provider ref="wsdl" />. |
The .NET Remoting Framework is based on messages that travel through various layers. Those layers can be extended or replaced and additional layers can be added. (I discuss layers in more detail in Chapter 7.)
These layers are implemented using so-called message sinks. A message will pass a chain of sinks, each of which will have the possibility to work with the message's content or to even change the message.
Using the ClientProviders and ServerProviders properties in the configuration file, you can specify this chain of sinks through which you want a message to travel and the formatter with which a message will be serialized.
The structure for this property for the server side is as follows:
<channels> <channel ref="http" port="1234"> <serverProviders> <formatter /> <provider /> </serverProviders> </channel> </channels>
You may only have one formatter entry but several provider properties. Also note that sequence does matter.
The following attributes are common between formatters and providers:
ATTRIBUTE | DESCRIPTION |
---|---|
ref | Reference for a predefined SinkProvider ("soap", "binary", or "wsdl") or reference to a SinkProvider that has been defined in a configuration file. |
type | Attribute that is mandatory when ref has not been specified. Contains the exact type (namespace, classname, assembly) of the SinkProvider's implementation. When the assembly is in the GAC, you have to specify version, culture, and public key information as well. |
|
Here are additional attributes that are optional for formatters:
ATTRIBUTE | DESCRIPTION |
---|---|
includeVersions | Indicator of whether version information should be included in the requests. Takes a value of true or false (defaults to true for built-in formatters). This attribute changes behavior on the client side. |
strictBinding | Indicator of whether the server will look for the exact type (including version) or any type with the given name. Takes a value of true or false (defaults to false for built-in formatters). |
|
In addition to these attributes, both formatters and providers can accept custom attributes, as shown in the following example. You have to check the documentation of your custom sink provider for the names and possible values of such properties:
<channels> <channel ref="http" port="1234"> <serverProviders> <provider type="MySinks.SampleProvider, Server" myAttribute="myValue" /> <sampleProp>This is a Sample</sampleProp> <sampleProp>This is another Sample</sampleProp> </provider> <formatter ref="soap" /> </serverProviders> </channel> </channels>
Depending on the setting of the includeVersion attribute on the client-side for-matter and the strictBinding attribute on the server-side formatter, different methods for creating instances of the given types are employed:
INCLUDEVERSIONS | STRICTBINDING | RESULTING BEHAVIOR |
---|---|---|
true | true | The exact type is loaded, or a TypeLoadException is thrown. |
false | true | The type is loaded using only the type name and the assembly name. A TypeLoadException is thrown if this type doesn't exist. |
true | false | The exact type is loaded if present; if not, the type is loaded using only the type name and the assembly name. If the type doesn't exist, aTypeLoadException is thrown. |
false | false | The type is loaded using only the type name and the assembly name. A TypeLoadException is thrown if this type doesn't exist. |
|
As you already know, the default HTTP channel will use a SoapFormatter to encode the messages. Using configuration files and the previously mentioned properties, you can easily switch to a BinaryFormatter for an HTTP channel.
On the server side, you use the following section in your configuration file:
<channels> <channel ref="http" port="1234"> <serverProviders> <formatter ref="binary" /> </serverProviders> </channel> </channels>
And on the client side, you can take the following configuration file snippet:
<channels> <channel ref="http> <clientProviders> <formatter ref=”binary” /> </serverProviders> </channel> </channels>
Note | The server-side entry is not strictly necessary, because the server-side HTTP channel automatically uses both formatters and detects which encoding has been chosen at the client side. |
The <service> property in the configuration file allows you to register SAOs and CAOs that will be made accessible by your server application. This section may contain a number of <wellknown> and <activated> properties.
The main structure of these entries is as follows:
<configuration> <system.runtime.remoting> <application> <service> <wellknown /> <activated /> </service> </application> </system.runtime.remoting> </configuration>
Using the <wellknown> property in the server-side configuration file, you can specify SingleCall and Singleton objects that will be provided by your server. This property supports the same attributes that can also be specified when calling RemotingConfiguration.RegisterWellKnownServiceType(), as listed here:
ATTRIBUTE | DESCRIPTION |
---|---|
type | The type information of the published class in the form "<namespace>.<classname>, <assembly>". When the assembly is in the GAC, you have to specify version, culture, and public key information as well. |
mode | Indicator specifying object type. Can take "Singleton" or "SingleCall". |
objectUri | The endpoint URI for calls to this object. When the object is hosted in IIS (shown later in this chapter), the URI has to end with .soap or .rem to be processed correctly, as those extensions are mapped to the .NET Remoting Framework in the IIS metabase. |
displayName | Optional attribute that specifies the name that will be used inside the .NET Framework Configuration Tool. |
|
Using the following configuration file, the server will allow access to aCustomerManager object via the URI http://<host>:1234/CustomerManager.soap.
<configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="1234" /> </channels> <service> <wellknown mode="Singleton" type="Server.CustomerManager, Server" objectUri="CustomerManager.soap" /> </service> </application> </system.runtime.remoting> </configuration>
The <activated> property allows you to specify CAOs in the server-side configuration file. As the full URI to this object is determined by the application name, the only attribute that has to be specified is the type to be published.
ATTRIBUTE | DESCRIPTION |
---|---|
type | The type information of the published class in the form "<namespace>.<classname>, <assembly>". When the assembly is in the GAC, you have to specify version, culture, and public key information as well. |
|
The following example allows a client to create an instance of MyClass at http://<hostname>:1234/.
<configuration> <system.runtime.remoting> <application> <channels> <channel ref="http" port="1234" /> </channels> <service> <activated type="MyObject, MyAssembly"/> </service> </application> </system.runtime.remoting> </configuration>
The client-side counterpart to the <service> property is the <client> configuration entry. Its primary structure is designed to look quite similar to the <service> entry:
<configuration> <system.runtime.remoting> <application> <client> <wellknown /> <activated /> </client> </application> </system.runtime.remoting> </configuration>
When using CAOs, the <client> property has to specify the URI to the server for all underlying <activated> entries.
Note | When using CAOs from more than one server, you have to create several <client> properties in your configuration file. |
ATTRIBUTE | DESCRIPTION |
---|---|
url | The URL to the server, mandatory when using CAOs. |
displayName | Attribute that is used in the .NET Framework Configuration Tool. |
|
The <wellknown> property is used to register SAOs on the client and allows you to use the new operator to instantiate references to remote objects. The client-side <wellknown> entry has the same attributes as the call to Activator.GetObject(), as listed here:
ATTRIBUTE | DESCRIPTION |
---|---|
url | The full URL to the server's registered object. |
type | Type information in the form "<namespace>.<classname>, <assembly>". When the target assembly is registered in the GAC, you have to specify version, culture, and public key information as well. |
displayName | Optional attribute that is used in the .NET Framework Configuration Tool. |
|
When registering a type to be remote, the behavior of the new operator will be changed. The framework will intercept each call to this operator and check if it's for a registered remote object. If this is the case, a reference to the server will be created instead of an instance of the local type.
When the following configuration file is in place, you can simply write CustomerManager x = new CustomerManager() to obtain a remote reference.
<configuration> <system.runtime.remoting> <application> <client> <wellknown type="Server.CustomerManager, Client" url="http://localhost:1234/CustomerManager.soap" /> </client> </application> </system.runtime.remoting> </configuration>
This is the client-side counterpart to the <activated> property on the server. As the URL to the server has already been specified in the <client> entry, the only attribute to specify is the type of the remote object.
ATTRIBUTE | DESCRIPTION |
---|---|
type | The type information in the form "<namespace>.<classname>, <assembly>". When the target assembly is registered in the GAC, you have to specify version, culture, and public key information as well. |
|
Data from this entry will also be used to intercept the call to the new operator. With a configuration file like the following, you can just write MyRemote x = new MyRemote() to instantiate a server-side CAO.
<configuration> <system.runtime.remoting> <application> <client url="http://localhost:1234/MyServer> <activated type="Server.MyRemote, Client" /> </client> </application> </system.runtime.remoting> </configuration>
|
|