Miscellaneous .NET Remoting Features


The final section of this chapter explores the following .NET Remoting features:

  • How application configuration files can be used to define remoting channels

  • Hosting .NET Remoting Servers in an IIS Server by using the ASP.NET runtime

  • Different ways to get the type information of the server for building the client with the utility Soapsuds

  • Calling .NET Remoting methods asynchronously

  • Implementing events to callback methods in the client

  • Using call contexts to pass some data automatically to the server behind the scenes

Configuration Files

Instead of writing the channel and object configuration in the source code, you can use configuration files. This way the channel can be reconfigured, additional channels can be added, and so on, without changing the source code. Like all the other configuration files on the .NET platform, XML is used. The same application and configuration files that you read about in Chapter 16, “Assemblies,” and in Chapter 19, “.NET Security,” are used here, too. For .NET Remoting, there are some more XML elements and attributes to configure the channel and the remote objects. The difference with the remoting configuration file is that this configuration needn’t be in the application configuration file itself; the file can have any name. To make the build process easier, in this chapter the Remoting configuration is written inside the application configuration files named with a .config file extension after the file name of the executable.

The code download (from www.wrox.com) contains the following example configuration files in the root directory of the client and the server examples: clientactivated.config and wellknown.config. With the client example, you will also find the file wellknownhttp.config that specifies an HTTP channel to a well-known remote object. To use these configurations, the files must be renamed to the name that is used with the parameter of the RemotingConfiguration.Configure() method and placed in the directory containing the executable file.

Here is just one example of how such a configuration file might look:

  <configuration>    <system.runtime.remoting>       <application name="Hello">          <service>             <wellknown mode="SingleCall"                type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                      objectUri="Hi" />          </service>          <channels>             <channel ref="tcp" port="6791" />             <channel ref="http" port="6792" />             <channel ref="ipc" portName="myIPCPort" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

<configuration> is the XML root element for all .NET configuration files. All the remoting configurations can be found in the subelement <system.runtime.remoting>. <application> is a subelement of <system.runtime.remoting>.

The following list describes the main elements and attributes of the parts within <system.runtime .remoting>:

  • With the <application> element, you can specify the name of the application using the attribute name. On the server side, this is the name of the server, and on the client side it’s the name of the client application. As an example for a server configuration, <application name=”Hello”> defines the remote application name Hello, which is used as part of the URL by the client to access the remote object.

  • On the server, the element <service> is used to specify a collection of remote objects. It can have <wellknown> and <activated> subelements to specify the type of the remote object as well known or client-activated.

  • The client part of the <service> element is <client>. Like the <service> element, it can have <wellknown> and <activated> subelements to specify the type of the remote object. Unlike the <service> counterpart, <client> has a url attribute to specify the URL to the remote object.

  • <wellknown> is an element that’s used on the server and the client to specify well-known remote objects. The server part could look like this:

     <wellknown mode="SingleCall"    type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"          objectURI="Hi" />

  • While the mode attribute SingleCall or Singleton can be specified, the type is the type of the remote class, including the namespace Wrox.ProCSharp.Remoting.Hello, followed by the assembly name RemoteHello. objectURI is the name of the remote object that’s registered in the channel.

  • On the client, the type attribute is the same as it is for the server version. mode and objectURI are not needed, but the url attribute is used to define the path to the remote object instead: protocol, hostname, port number, application name, and the object URI:

     <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                 url="tcp://localhost:6791/Hello/Hi" />

  • The <activated> element is used for client-activated objects. With the type attribute the type and the assembly must be defined for both the client and the server application:

     <activated type=”Wrox.ProCSharp.Remoting.Hello, RemoteHello” />

  • To specify the channel, the <channel> element is used. It’s a subelement of <channels> so that a collection of channels can be configured for a single application. Its use is similar for clients and servers. With the XML attribute ref you reference a preconfigured channel name. For the server channel, you have to set the port number with the XML attribute port. The XML attribute displayName is used to specify a name for the channel that is used from the .NET Framework Configuration tool, as discussed later in this chapter.

     <channels>    <channel ref="tcp" port="6791" displayName="TCP Channel" />    <channel ref="http" port="6792" displayName="HTTP Channel" />    <channel ref="ipc" portName="myIPCPort" displayName="IPC Channel" /> </channels>

Tip 

Predefined channels have the names tcp, http, and ipc, which define the classes TcpChannel, HttpChannel, and IpcChannel.

Server Configuration for Well-Known Objects

This example file, Wellknown_Server.config, has the value Hello for the name property. In the following configuration file, the TCP channel is set to listen on port 6791 and the HTTP channel is set to listen on port 6792. The IPC channel is configured with the port name myIPCPort. The remote object class is Wrox.ProCSharp.Remoting.Hello in the assembly RemoteHello, the object is called Hi in the channel, and the object mode is SingleCall:

  <configuration>    <system.runtime.remoting>       <application name="Hello">          <service>             <wellknown mode="SingleCall"                        type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                        objectUri="Hi" />           </service>           <channels>              <channel ref="tcp" port="6791"                 displayName="TCP Channel (HelloServer)" />              <channel ref="http" port="6792"                 displayName="HTTP Channel (HelloServer)" />              <channel ref="ipc" portName="myIPCPort"                 displayName="IPC Channel (HelloServer)" />           </channels>       </application>    </system.runtime.remoting> </configuration> 

Client Configuration for Well-Known Objects

For well-known objects, you have to specify the assembly and the channel in the client configuration file Wellknown_Client.config. The types for the remote object are in the RemoteHello assembly, Hi is the name of the object in the channel, and the URI for the remote type Wrox.ProCSharp.Remoting.Hello is ipc://myIPCPort/Hello/Hi. In the client, an IPC channel is used as well, but no port is specified, so a free port is selected. The channel that is selected with the client must correspond to the URL.

  <configuration>    <system.runtime.remoting>       <application name="Client">          <client displayName="Hello client for well-known objects">             <wellknown type = "Wrox.ProCSharp.Remoting.Hello, RemoteHello"                                url="ipc://myIPCPort/Hello/Hi" />          </client>          <channels>             <channel ref="ipc" displayName="IPC Channel (HelloClient)" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

After a small change in the configuration file, you’re using the HTTP channel (as you can see in WellknownHttp_Client.config):

  <client>    <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                     url="http://localhost:6792/Hello/Hi" /> </client> <channels>    <channel ref="http" displayName="HTTP Channel (HelloClient)" /> </channels> 

Server Configuration for Client-Activated Objects

By changing only the configuration file (located in ClientActivated_Server.config), you can change the server configuration from server-activated to client-activated objects. Here the <activated> subelement of the <service> element is specified. With the <activated> element for the server configuration, just the type attribute must be specified. The name attribute of the application element defines the URI.

 <configuration>    <system.runtime.remoting>       <application name="HelloServer">          <service>             <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" />          </service>          <channels>             <channel ref="http" port="6788"                      displayName="HTTP Channel (HelloServer)" />             <channel ref="tcp" port="6789"                displayName="TCP Channel (HelloServer)" />                <channel ref="ipc" portName="myIPCPort"                      displayName="IPC Channel (HelloServer)" />          </channels>       </application>    </system.runtime.remoting> </configuration>

Client Configuration for Client-Activated Objects

The ClientActivated_Client.config file defines the client-activated remote object using the url attribute of the <client> element and the type attribute of the <activated> element:

 <configuration>    <system.runtime.remoting>       <application>          <client url="http://localhost:6788/HelloServer"                  displayName="Hello client for client-activated objects">             <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" />          </client>          <channels>             <channel ref="http" displayName="HTTP Channel (HelloClient)" />             <channel ref="tcp" displayName="TCP Channel (HelloClient)" />             <channel ref="ipc" displayName="IPC Channel (HelloClient)" />          </channels>       </application>    </system.runtime.remoting> </configuration>

Server Code Using Configuration Files

In the server code, you have to configure remoting using the static method Configure() from the RemotingConfiguration class. Here all channels that are defined in the configuration file are created and configured with the .NET Remoting runtime. Maybe you also want to know about the channel configurations from the server application - that’s why the static methods ShowActivatedServiceTypes() and ShowWellKnownServiceTypes() were created; they are called after loading and starting the remoting configuration:

 public static void Main() {    RemotingConfiguration.Configure("HelloServer.exe.config", false);    Console.WriteLine("Application: " + RemotingConfiguration.ApplicationName);    ShowActivatedServiceTypes();    ShowWellKnownServiceTypes();    System.Console.WriteLine("press return to exit");    System.Console.ReadLine(); }

These two functions show configuration information of well-known and client-activated types:

  public static void ShowWellKnownServiceTypes() {    WellKnownServiceTypeEntry[] entries =    RemotingConfiguration.GetRegisteredWellKnownServiceTypes();     foreach (WellKnownServiceTypeEntry entry in entries)    {       Console.WriteLine("Assembly: " + entry.AssemblyName);       Console.WriteLine("Mode: " + entry.Mode);       Console.WriteLine("URI: " + entry.ObjectUri);       Console.WriteLine("Type: " + entry.TypeName);    } } public static void ShowActivatedServiceTypes() {    ActivatedServiceTypeEntry[] entries =    RemotingConfiguration.GetRegisteredActivatedServiceTypes();    foreach (ActivatedServiceTypeEntry entry in entries)    {       Console.WriteLine("Assembly: " + entry.AssemblyName);       Console.WriteLine("Type: " + entry.TypeName);    } } 

Client Code Using Configuration Files

In the client code, it is only necessary to configure the remoting services using the configuration file client.exe.config. After that, you can use the new operator to create new instances of the remote class Hello, no matter whether you work with server-activated or client-activated remote objects. However, there’s a small difference - with client-activated objects, it’s now possible to use nondefault constructors with the new operator. This isn’t possible for server-activated objects. Single-call objects can have no state because they are destroyed with every call; singleton objects are created just once. Calling nondefault constructors is only possible with client-activated objects because this is the only type where by invoking the new operator the remote object is instantiated.

In the Main() method of the file HelloClient.cs, you can now change the remoting code to use the configuration file with RemotingConfiguration.Configure() and create the remote object with the new operator:

 RemotingConfiguration.Configure("HelloClient.exe.config", false); Hello obj = new Hello(); if (obj == null) {    Console.WriteLine("could not locate server");    return; } for (int i=0; i < 5; i++) {    Console.WriteLine(obj.Greeting("Christian")); }

Delayed Loading of Client Channels

With the configuration file machine.config, three channels are configured that can be used automatically if the client doesn’t configure a channel:

  <system.runtime.remoting>    <application>       <channels>          <channel ref="http client" displayName="http client (delay loaded)"                   delayLoadAsClientChannel="true" />          <channel ref="tcp client" displayName="tcp client (delay loaded)"                   delayLoadAsClientChannel="true" />          <channel ref="ipc client" displayName="ipc client (delay loaded")"                   delayLoadAsClientChannel="true" />       </channels>    </application> </system.runtime.remoting> 

The XML attribute delayLoadAsClientChannel with a value true specifies that the channel should be used from a client that doesn’t configure a channel. The runtime tries to connect to the server using the delay-loaded channels. So, it is not necessary to configure a channel in the client configuration file, and a client configuration file for the well-known object you have used earlier can look as simple as this:

  <configuration>    <system.runtime.remoting>       <application name="Client">          <client url="tcp:/localhost:6791/Hello">             <wellknown type = "Wrox.ProCSharp.Remoting.Hello, RemoteHello"                        url="tcp://localhost:6791/Hello/Hi" />          </client>       </application>    </system.runtime.remoting> </configuration> 

Debugging Configuration

If you have a misconfigured server configuration file (for example, by specifying the wrong name of the remote assembly), the error is not detected when the server starts. Instead, the error is detected when the client instantiates a remote object and invokes a method. The client will get the exception that the remote object assembly cannot be found. By specifying the configuration <debug loadTypes=”true” />, the remote object is loaded and instantiated on the server when the server starts up. This way you will get an error on the server if the configuration file is misconfigured:

    <configuration>    <system.runtime.remoting>       <application name="Hello">          <service>             <wellknown mode="SingleCall"                        type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                        objectUri="Hi" />          </service>          <channels>              <channel ref="tcp" port="6791"                 displayName="TCP Channel (HelloServer)" />          </channels>       </application>       <debug loadTypes="true" />    </system.runtime.remoting> </configuration>

Lifetime Services in Configuration Files

Leasing configuration for remote servers can also be done with the application configuration files. The <lifetime> element has the attributes leaseTime, sponsorshipTimeOut, renewOnCallTime, and pollTime, as shown in this example:

 <configuration>    <system.runtime.remoting>       <application>          <lifetime leaseTime = "15M" sponsorshipTimeOut = "4M"                    renewOnCallTime = "3M" pollTime = "30s"/>       </application>    </system.runtime.remoting> </configuration>

Using configuration files, it is possible to change the remoting configuration by editing files instead of working with source code. You can easily change the channel to use HTTP instead of TCP, change a port, the name of the channel, and so on. With the addition of a single line, the server can listen to two channels instead of one.

Formatter Providers

Earlier, this chapter discussed where properties of the formatter provider need to be changed to support marshaling all objects across the network. Instead of doing this programmatically, as was done earlier, you can configure the properties of a formatter provider in a configuration file.

The following server configuration file is changed within the <channel> element, insofar as <serverProviders> and <clientProviders> are defined as child elements. With the <serverProviders>, the built-in providers wsdl, soap, and binary are referenced, and with the soap and binary providers the property typeFilterLevel is set to Full:

 <configuration>    <system.runtime.remoting>       <application name="HelloServer">          <service>             <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" />          </service>          <channels>             <channel ref="tcp" port="6789"                      displayName="TCP Channel (HelloServer)">               <serverProviders>                 <provider ref="wsdl" />                 <provider ref="soap" typeFilterLevel="Full" />                 <provider ref="binary" typeFilterLevel="Full" />               </serverProviders>               <clientProviders>                 <provider ref="binary" />               </clientProviders>             </channel>          </channels>       </application>    </system.runtime.remoting> </configuration>

.NET Framework Configuration Tool

The system administrator can use the .NET Framework Configuration tool (see Figure 37-11) to reconfigure existing configuration files. This tool is part of the administrative tools, which can be accessed by using the Control Panel.

image from book
Figure 37-11

By adding the application HelloClient.exe where you used the client configuration file to the configured applications in this tool, you can configure the URL of the remote object by selecting the hyperlink View Remoting Services Properties.

As shown in Figure 37-12, for the client application you can see the value of the displayName attribute in the combo box. This combo box allows you to select the remote application, so you can change the URL of the remote object.

image from book
Figure 37-12

By adding the server application to this tool, the configuration of the remote object and the channels can be changed, as shown in Figures 37-13 and 37-14.

image from book
Figure 37-13

image from book
Figure 37-14

Hosting Servers in ASP.NET

Up to this point, all the sample servers have been running in self-hosted .NET servers. A self-hosted server must be launched manually. A .NET Remoting server can also be started in a lot of other application types. In a Windows Service, the server can be automatically started at boot time, and in addition the process can run with the credentials of the system account. For more details on Windows Services, see Chapter 22, “Windows Services.”

There’s special support for .NET Remoting servers for ASP.NET. ASP.NET can be used for the automatic startup of remote servers. ASP.NET-hosted Remoting uses a different file for configuration than EXE-hosted applications, but it has the same syntax.

To use the infrastructure from the Internet Information Server and ASP.NET, you have to create a class that derives from System.MarshalByRefObject and has a default constructor. The code used earlier for the server to create and register the channel is no longer necessary; that’s done by the ASP.NET runtime. You have to create only a virtual directory on the Web server that maps a directory into which you put the configuration file Web.config. The assembly of the remote class must reside in the bin subdirectory.

To configure a virtual directory on the Web server, you can use the Internet Information Services MMC. Selecting the Default Web site, and opening the Action menu creates a new Virtual Directory.

The configuration file Web.config on the Web server must be put in the home directory of the virtual Web site. With the default IIS configuration, the channel that will be used listens to port 80:

  <configuration>    <system.runtime.remoting>       <application>          <service>             <wellknown mode="SingleCall"                type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                objectUri="HelloService.soap" />          </service>       </application>    </system.runtime.remoting> </configuration> 

Important 

If the remoting object is hosted within IIS, the name of the remote object must end either with .soap, or .rem, depending on the type of formatter that is used (SOAP or the binary).

The client can now connect to the remote object using the following configuration file. The URL that must be specified for the remote object here is the Web server localhost, followed by the Web application name RemoteHello (specified when creating the virtual Web site), and the URI of the remote object HelloService.soap that is defined in the file Web.config. It’s not necessary to specify the port number 80, because that’s the default port for the HTTP protocol. Not specifying a <channels> section means that the delay-loaded HTTP channel from the configuration file machine.config is used:

  <configuration>    <system.runtime.remoting>       <application>          <client url="http:/localhost/RemoteHello">             <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                        url="http://localhost/RemoteHello/HelloService.soap" />          </client>       </application>    </system.runtime.remoting> </configuration> 

Important 

Hosting remote objects in ASP.NET only supports well-known objects.

Classes, Interfaces, and Soapsuds

In the .NET Remoting samples you have seen until now, you have always copied the assembly of the remote object not only to the server but also to the client application. This way the MSIL code of the remote object is on both the client and the server system, although in the client application only the metadata is needed. However, copying the remoting object assembly means that it’s not possible for the client and the server to be programmed independently. A much better way to use just the metadata is to use interfaces or the Soapsuds.exe utility instead.

Interfaces

You get a cleaner separation of the client and server code by using interfaces. An interface simply defines the methods without implementation. This way the contract (the interface) is separated from the implementation, and just the contract is needed on the client system. Here are the necessary steps for using an interface:

  1. Define an interface that will be placed in a separate assembly.

  2. Implement the interface in the remote object class. To do this, the assembly of the interface must be referenced.

  3. On the server side no more changes are required. The server can be programmed and configured in the usual way.

  4. On the client side, reference the assembly of the interface instead of the assembly of the remote class.

  5. The client can now use the interface of the remote object rather than the remote object class. The object can be created using the Activator class as it was done earlier. You can’t use the new operator in this way, because the interface itself cannot be instantiated.

The interface defines the contract between the client and server. The two applications can now be developed independently of each other. If you also stick to the old COM rules about interfaces (that interfaces should never be changed), you will not have any versioning problems.

Soapsuds

You can also use the Soapsuds utility to get the metadata from an assembly if an HTTP channel and the SOAP formatter are used. Soapsuds can convert assemblies to XML schemas and XML schemas to wrapper classes, and also works in the other direction.

The following command converts the type Hello from the assembly RemoteHello to the assembly HelloWrapper, where a transparent proxy is generated that calls the remote object:

 soapsuds –types:Wrox.ProCSharp.Remoting.Hello,RemoteHello –oa:HelloWrapper.dll 

With Soapsuds you can also get the type information directly from a running server, if the HTTP channel and the SOAP formatter are used:

 soapsuds –url:http://localhost:6792/hello/hi?wsdl –oa:HelloWrapper.dll 

In the client, you can now reference the generated assembly instead of the original one. Some of the Soapsuds options are listed in the following table.

Open table as spreadsheet

Option

Description

-url

Retrieve schema from the specified URL

-proxyurl

If a proxy server is required to access the server, specify the proxy with this option

-types

Specify a type and assembly to read the schema information from it

-is

Input schema file

-ia

Input assembly file

-os

Output schema file

-oa

Output assembly file

Asynchronous Remoting

If server methods take some time to complete and the client needs to do some different work at the same time, it isn’t necessary to start a separate thread to make the remote call. By making an asynchronous call, you cause the method to start but return immediately to the client. Asynchronous calls can be made on a remote object, as they are made on a local object with the help of a delegate. With methods that don’t return a value, you can also use the OneWay attribute.

Using Delegates with .NET Remoting

To make an asynchronous method, you create a delegate, GreetingDelegate, with the same argument and return value as the Greeting() method of the remote object. With the delegate keyword a new class GreetingDelegate that derives from MulticastDelegate is created. You can verify this by using ildasm and checking the assembly. The argument of the constructor of this delegate class is a reference to the Greeting() method. You start the Greeting() call using the BeginInvoke() method of the delegate class. The second argument of BeginInvoke() is an AsyncCallback instance that defines the method HelloClient.Callback(), which is called when the remote method is finished. In the Callback() method, the remote call is finished using EndInvoke():

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting {    public class HelloClient    {       private delegate String GreetingDelegate(String name);       private static string greeting;       public static void Main()       {          RemotingConfiguration.Configure("HelloClient.exe.config");          Hello obj = new Hello();          if (obj == null)          {             Console.WriteLine("could not locate server");             return;          }          // synchronous version          // string greeting = obj.Greeting("Christian");          // asynchronous version           GreetingDelegate d = new GreetingDelegate(obj.Greeting);          IAsyncResult ar = d.BeginInvoke("Christian", null, null);           // do some work and then wait          ar.AsyncWaitHandle.WaitOne();          if (ar.IsCompleted)          {             greeting = d.EndInvoke(ar);          }           Console.WriteLine(greeting);       }    } } 

You can find more information about delegates and events in Chapter 7, “Delegates and Events.”

OneWay Attribute

A method that has a void return and only input parameters can be marked with the OneWay attribute. The OneWay attribute (defined within the namespace System.Runtime.Remoting.Messaging) makes a method automatically asynchronous, regardless of how the client calls it. Adding the method TakeAWhile() to your remote object class RemoteHello creates a fire-and-forget method. If the client calls it by the proxy, the proxy immediately returns to the client. On the server, the method finishes some time later:

  [OneWay] public void TakeAWhile(int ms) {    Console.WriteLine("TakeAWhile started");    System.Threading.Thread.Sleep(ms);    Console.WriteLine("TakeAWhile finished"); } 

Security with .NET Remoting

A completely new feature with .NET 2.0 is security support with .NET Remoting. With .NET 1.1, security was only available when the remote object was hosted within IIS and ASP.NET. .NET 2.0 supports confidentiality of the data that is transferred across the wire as well as authentication of the user.

Security can be configured with every channel of the .NET 2.0 Framework. TCP, HTTP, and IPC channels support security configuration. With the server configuration you have to define the minimum security requirement of the communication, while the client configuration defines the capabilities regarding security. If the client defines a security configuration that is lower than the minimum requirements defined by the server, the communication fails.

Let’s start with the server configuration. The following extract of the configuration file shows how the security can be defined for the server.

With the XML attribute protectionLevel, you can specify if the server requires encrypted data to be sent across the network. The possible values that can be set with the protectionLevel attribute are None, Sign, and EncryptAndSign. With the value Sign a signature is created that is used to verify if the data that is sent didn’t change on its way across the network. However, it is still possible to read the data with a sniffer that reads all the data on the network. With the value EncryptAndSign, the data that is sent is also encrypted to make it impossible that systems not officially participating with the message transfer can read the data.

The attribute impersonate can be set to true or false. If impersonate is set to true, the server can impersonate the user of the client to access resources in its name.

 <channels>    <channel ref="tcp" port="9001"       secure="true"       protectionLevel="EncryptAndSign"       impersonate="false" /> </channels>

With the client configuration, the capabilities of the network communication are defined. If the capabilities don’t fulfil the server requirements, the communication fails. You can see protectionLevel and TokenImpersonationLevel configured in the following sample configuration.

protectionLevel can be set to the same options as you have seen with the server configuration file.

The TokenImpersonationLevel can be set to Anonymous, Identification, Impersonation, and Delegation. With the Anonymous value, the server cannot identify the user of the client. If the value is set to Identification, the server can find out the user identity of the server. If the server is configured with impersonate set to true, the communication fails if the client is only configured with Anonymous or Identification. Setting TokenImpersonationLevel to Impersonation allows the server to impersonate the client. Setting TokenImpersonationLevel to Delegation allows the server to impersonate the client not only to access resources local on the server but also to use the user identification to access resources on other servers. Delegation is only possible if Kerberos is used for logging on the user, as is possible with Active Directory.

 <channels>    <channel ref="tcp"       secure="true"       protectionLevel="EncryptAndSign"       TokenImpersonationLevel="Impersonate" /> </channels>

If you create the channel programmatically instead of using configuration files, you can define the security setting programmatically as well. A collection that contains all security settings can be passed to the constructor of the channel as shown. The collection class must implement the interface IDictionary, for example the generic Dictionary class or the Hashtable class.

  Dictionary<string, string> dict;           = new Dictionary<string, string>(); dict.Add("secure", "true"); TcpClientChannel clientChannel =            new TcpClientChannel(dict, null); 

You can read more about security in Chapter 19, “.NET Security.”

Remoting and Events

Not only can the client invoke methods on the remote object across the network but the server can do the same - invoking methods in the client. For this, a mechanism that you already know from the basic language features is used: delegates and events.

In principle, the architecture is simple. The server has a remotable object that the client can call, and the client has a remotable object that the server can call:

  • The remote object in the server must declare an external function (a delegate) with the signature of the method that the client will implement in a handler.

  • The arguments that are passed with the handler function to the client must be marshalable, so all the data sent to the client must be serializable.

  • The remote object must also declare an instance of the delegate function modified with the event keyword; the client will use this to register a handler.

  • The client must create a sink object with a handler method that has the same signature as the delegate defined, and it has to register the sink object with the event in the remote object.

Take a look at an example. To see all the parts of event handling with .NET Remoting, create five classes: Server, Client, RemoteObject, EventSink, and StatusEventArgs. The dependencies of these classes are shown in Figure 37-15.

image from book
Figure 37-15

The Server class is a remoting server such as the one you already are familiar with. The Server class will create a channel based on information from a configuration file and register the remote object that’s implemented in the RemoteObject class in the remoting runtime. The remote object declares the arguments of a delegate and fires events in the registered handler functions. The argument that’s passed to the handler function is of type StatusEventArgs. The class StatusEventArgs must be serializable so that it can be marshaled to the client.

The Client class represents the client application. This class creates an instance of the EventSink class and registers the StatusHandler() method of this class as a handler for the delegate in the remote object. EventSink must be remotable like the RemoteObject class, because this class will also be called across the network.

Remote Object

The remote object class is implemented in the file RemoteObject.cs. The remote object class must be derived from MarshalByRefObject, as you already know from the previous examples. To enable the client to register an event handler that can be called from within the remote object, you have to declare an external function with the delegate keyword. Declare the delegate StatusEvent() with two arguments: the sender (so the client knows about the object that fired the event) and a variable of type StatusEventArgs. Into the argument class you can put all the additional information that you want to send to the client.

The method that will be implemented in the client has some strict requirements. It can only have input parameters - return types, ref, and out parameters are not allowed - and the argument types must be either [Serializable] or remotable (derived from MarshalByRefObject). These requirements are fulfilled by the parameters that are defined with this StatusEvent delegate:

  public delegate void StatusEvent(object sender, StatusEventArgs e); public class RemoteObject : MarshalByRefObject { 

Within the RemoteObject class, declare an event name Status of type StatusEvent, which is the delegate. The client must add an event handler to the Status event to receive status information from the remote object:

 public class RemoteObject : MarshalByRefObject {    public RemoteObject()    {       Console.WriteLine("RemoteObject constructor called");    }    public event StatusEvent Status; 

The LongWorking() method checks whether an event handler is registered before the event is fired by calling Status(this, e). To verify that the event is fired asynchronously, fire an event at the start of the method before doing the Thread.Sleep() and after the sleep:

  public void LongWorking(int ms) {    Console.WriteLine("RemoteObject: LongWorking() Started");    StatusEventArgs e = new StatusEventArgs(                              "Message for Client: LongWorking() Started");    // fire event    if (Status != null)    {       Console.WriteLine("RemoteObject: Firing Starting Event");        Status(this, e);    }    System.Threading.Thread.Sleep(ms);    e.Message = "Message for Client: LongWorking() Ending";    // fire ending event     if (Status != null)    {       Console.WriteLine("RemoteObject: Firing Ending Event");       Status(this, e);    }    Console.WriteLine("RemoteObject: LongWorking() Ending"); } 

Event Arguments

As you’ve seen in the RemoteObject class, the class StatusEventArgs is used as an argument for the delegate. With the [Serializable] attribute an instance of this class can be transferred from the server to the client. Here is a simple property of type string to send a message to the client:

  [Serializable] public class StatusEventArgs {    public StatusEventArgs(string m)    {       message = m;    }    public string Message    {       get       {          return message;       }       set       {          message = value;       }    }    private string message; } 

Server

The server is implemented within a console application. With RemotingConfiguration.Configure(), the configuration file is read and thus the channel and remote objects are set up. With the Console .ReadLine() the server waits until the user stops the application:

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting {    class Server    {       static void Main()       {          RemotingConfiguration.Configure("Server.exe.config", true);          Console.WriteLine("press return to exit");          Console.ReadLine();       }    } }

Server Configuration File

The server configuration file, Server.exe.config, is also created as already discussed. There is just one important point: the remote object must keep state for the client because the client at first registers the event handler and calls the remote method afterward. You cannot use single-call objects with events, so the RemoteObject class is configured as a client-activated type. Also, to support delegates, you have to enable full serialization by specifying the typeFilterLevel attribute with the <provider> element:

 <configuration>    <system.runtime.remoting>       <application name="CallbackSample">          <service>             <activated type="Wrox.ProCSharp.Remoting.RemoteObject,                              RemoteObject" />          </service>          <channels>             <channel ref="tcp" port="6791">                <serverProviders>                   <provider ref="binary" typeFilterLevel="Full" />                </serverProviders>             </channel>          </channels>       </application>    </system.runtime.remoting> </configuration>

Event Sink

An event sink library is required for use by the client and is invoked by the server. The event sink implements the handler StatusHandler() that’s defined with the delegate. As previously noted, the method can only have input parameters and only a void return. The EventSink class must also inherit from the class MarshalByRefObject to make it remotable, because it will be called remotely from the server:

  using System; using System.Runtime.Remoting.Messaging; namespace Wrox.ProCSharp.Remoting {    public class EventSink : MarshalByRefObject    {       public EventSink()       {       }       public void StatusHandler(object sender, StatusEventArgs e)       {          Console.WriteLine("EventSink: Event occurred: " + e.Message);       }    } } 

Client

The client reads the client configuration file with the RemotingConfiguration class, which is not different from the clients that have been discussed so far. The client creates an instance of the remotable sink class EventSink locally. The method that should be called from the remote object on the server is passed to the remote object:

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting {    class Client    {       static void Main()       {          RemotingConfiguration.Configure("Client.exe.config", true);

The differences start here. You have to create an instance of the remotable sink class EventSink locally. Because this class will not be configured with the <client> element, it’s instantiated locally. Next, the remote object class RemoteObject is instantiated. This class is configured in the <client> element, so it’s instantiated on the remote server:

  EventSink sink = new EventSink(); RemoteObject obj = new RemoteObject(); 

Now you can register the handler method of the EventSink object in the remote object. StatusEvent is the name of the delegate that was defined in the server. The StatusHandler() method has the same arguments as defined in the StatusEvent.

By calling the LongWorking() method, the server will call back into the method StatusHandler() at the beginning and at the end of the method:

  // register client sink in server - subscribe to event obj.Status += new StatusEvent(sink.StatusHandler); obj.LongWorking(5000); 

Now you are no longer interested in receiving events from the server, so you are unsubscribing from the event. The next time you call LongWorking(), no events will be received:

          // unsubscribe from event          obj.Status -= new StatusEvent(sink.StatusHandler);          obj.LongWorking(5000);          Console.WriteLine("press return to exit");          Console.ReadLine();       }    } }

Client Configuration File

The configuration file for the client, client.exe.config, is nearly the same configuration file for client-activated objects that we’ve already seen. The difference is in defining a port number for the channel. Because the server must reach the client with a known port, you have to define the port number for the channel as an attribute of the <channel> element. It isn’t necessary to define a <service> section for your EventSink class, because this class will be instantiated from the client with the new operator locally. The server does not access this object by its name; it will receive a marshaled reference to the instance instead:

 <configuration>    <system.runtime.remoting>       <application name="Client">          <client url="tcp://localhost:6791/CallbackSample">             <activated type="Wrox.ProCSharp.Remoting.RemoteObject,                              RemoteObject" />          </client>          <channels>             <channel ref="tcp" port="0">                <serverProviders>                   <provider ref="binary" typeFilterLevel="Full" />                </serverProviders>             </channel>          </channels>       </application>    </system.runtime.remoting> </configuration>

Running Programs

Figure 37-16 shows the resulting output of the server. The constructor of the remote object is called once because you have a client-activated object. Next, you can see the call to LongWorking() has started and an event is fired to the client. The next start of the LongWorking() method doesn’t fire events, because the client has already unregistered its interest in the event.

image from book
Figure 37-16

Figure 37-17 shows the client output of the events that made it across the network.

image from book
Figure 37-17

Call Contexts

Client-activated objects can hold state for a specific client. With client-activated objects the server allocates resources for every client. With server-activated SingleCall objects, a new instance is created for every instance call, and no resources are held on the server; these objects can’t hold state for a client. For state management, you can keep state on the client side; the state information is sent with every method call to the server. To implement such a state management, it is not necessary to change all method signatures to include an additional parameter that passes the state to the server; this is automatically done by the call context.

A call context flows with a logical thread and is passed with every method call. A logical thread is started from the calling thread and flows through all method calls that are started from the calling thread, passing through different contexts, different application domains, and different processes.

You can assign data to the call context using CallContext.SetData(). The class of the object that’s used as data for the SetData() method must implement the interface ILogicalThreadAffinative. You can get this data again in the same logical thread (but possibly a different physical thread) using CallContext.GetData().

For the data of the call context, create a new C# Class Library with the class CallContextData. This class will be used to pass some data from the client to the server with every method call. The class that’s passed with the call context must implement the System.Runtime.Remoting.Messaging .ILogicalThreadAffinative interface. This interface doesn’t have a method; it’s just a markup for the runtime to define that instances of this class should flow with a logical thread. The CallContextData class must also be marked with the Serializable attribute so it can be transferred through the channel:

  using System; using System.Runtime.Remoting.Messaging; namespace Wrox.ProCSharp.Remoting {    [Serializable]    public class CallContextData : ILogicalThreadAffinative     {       public CallContextData()       {       }       public string Data       {          get          {             return data;          }          set          {             data = value;          }       }       protected string data;    } } 

In the remote object class Hello, change the Greeting() method to access the call context. For the use of the CallContextData class you have to reference the previously created assembly CallContextData in the file CallContextData.dll. To work with the CallContext class, the namespace System .Runtime.Remoting.Messaging must be opened. The variable cookie holds the data that is sent from the client to the server. The name “cookie” is chosen because the context works similar to a browser-based cookie, where the client automatically sends data to the Web server:

 public string Greeting(string name) {    Console.WriteLine("Greeting started");    CallContextData cookie =       (CallContextData)CallContext.GetData("mycookie");    if (cookie != null)    {       Console.WriteLine("Cookie: " + cookie.Data);    }    Console.WriteLine("Greeting finished");    return "Hello, " + name; }

In the client code, the call context information is set by calling CallContext.SetData(). With this method an instance of the class CallContextData is assigned to be passed to the server. Now every time the method Greeting() is called in the for loop, the context data is automatically passed to the server:

 CallContextData cookie = new CallContextData(); cookie.Data = "information for the server"; CallContext.SetData("mycookie", cookie); for (int i=0; i < 5; i++) {    Console.WriteLine(obj.Greeting("Christian")); }

You can use such a call context to send information about the user, the name of the client system, or simply a unique identifier used on the server side to get some state information from a database.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

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