Remoting vs. ASP.NET

The Grabber.NET Application

Napster has heightened public awareness of peer-to-peer applications. A peer-to-peer application acts as both a client and a server. In the case of SOAP-based Web services, the application is capable of sending a SOAP request to a peer as well as accepting SOAP requests from a peer.

In this chapter, I build a .NET version of Napster called Grabber.NET. It is a WinForm application with a UI similar to Windows Explorer. The Grabber.NET client looks like this:

The user can connect to other Grabber.NET applications and browse the directory structure to locate files of interest. The user can also select specific files and copy them to a local directory.

Grabber.NET will communicate with other peers via SOAP over HTTP. Because the WinForm application will act as both a client and a server, Grabber.NET cannot be implemented using ASP.NET. In contrast, Remoting allows you to create a Web service that is hosted in any process, including a WinForm application.

Much of the implementation of Grabber.NET is UI related, so in this chapter I list only the code relevant to Remoting. The full source code for Grabber.NET is on the companion CD.

Remoting Architecture

Before I delve into the specifics of implementing and consuming Web services using Remoting, let's discuss the architecture for enabling a client to communicate with a remote object.

When a client creates an instance of a remote object, it receives a proxy instead of the object itself. The proxy exposes the same interfaces as the actual object. When a client invokes a method or accesses a field or property on the proxy, the proxy is responsible for forwarding the request to the remote object. The diagram on the next page shows the major components involved in facilitating communication between the client and the server.

The Remoting infrastructure is composed of four major components:

  • Remoting runtime The Remoting runtime is responsible for dynamically creating proxies on behalf of the client. It is also responsible for invoking the appropriate channel on the server to listen for incoming requests.

  • Proxy The proxy object is responsible for receiving the method calls from the user. Once a method call has been received, the proxy is responsible for eliciting the help of the appropriate formatter and transport to send the parameters to the remote object.

  • Formatter The formatter is responsible for serializing the parameters into a format that can be shipped across the wire. Remoting ships with two formatters, binary and SOAP. In this chapter, I discuss the SOAP formatter.

  • Channel The channel is responsible for sending formatted messages between the client and the server. The client-side channel is responsible for sending the message over the designated transport protocol. The server-side channel is responsible for monitoring incoming messages and passing those messages to the appropriate formatter. Remoting ships with two channels, TCP and HTTP.

One important feature of the Remoting framework that I discussed earlier is support for pluggable formatters and channels. The preceding diagram highlights the ability to combine any formatter with any channel. As you will see in later sections, you can change which formatters and channels a Remoting application uses by modifying a configuration file. For example, by modifying the configuration file you can allow your SOAP-based Web service to switch from accepting requests over HTTP to accepting requests over raw TCP/IP.

Creating an IIS-Hosted Web Service

The illegal sharing of licensed content by peer-to-peer programs is a serious issue, so I will implement a Licensing Web service that Grabber.NET will use to verify that the client has a valid license to copy the requested content. All Grabber.NET peer applications will be responsible for validating the client's request for licensed content against the centrally hosted Licensing Web service, as shown here:

Creating the implementation of a Web service hosted by the Remoting runtime is trivial. The only requirement is that the object must derive from the MarshalByRefObject class. Deriving from MarshalByRefObject instructs the Remoting runtime to ensure that the object is confined to the application domain in which it was created.

When the client requests a new object derived from the MarshalByRefObject class, the remote object created on behalf of the client will not be passed by value to the client. Instead, the client will receive a proxy that will serve as a reference to the remote object.

The Licensing Web service will expose a Validate method that verifies whether the user is licensed to use the content. Here is the implementation:

using System; namespace SomeRecordCompany {     class Licensing : MarshalByRefObject     {         public bool Validate(string resource, LicenseInfo licenseInfo)         {             bool isValid = true;             // Implementation...             return isValid;         }     }

The implementation of the Licensing Web service is contained within the Licensing class. The class contains the Validate method, which accepts two parameters: a string that identifies the resource and an object of type LicenseInfo that contains the client's license information. This class derives from MarshalByRefObject, so when the Web service is hosted by the Remoting runtime, the Validate method will be invoked on the record company's server.

Unlike the Licensing class, instances of the LicenseInfo object should be passed by value. This ensures that the Licensing Web service will maintain a high degree of compatibility with other SOAP implementations because SOAP 1.1 does not support passing objects by reference.

Even if the Licensing Web service were to be consumed only by Remoting clients, you would still want to pass the object by value. If the object were passed by reference, needless round-trips would occur between the client and the server as the properties exposed by the class are accessed.

For an object to be passed by value, it must be marked as serializable. You can mark an object as serializable by decorating the class with the Serializable attribute or having the class support the ISerializable interface.

Instances of a class that is decorated with the Serializable attribute are automatically serialized by the Remoting runtime. Unlike ASP.NET, Remoting is capable of serializing all properties and fields regardless of their visibility. Recall that ASP.NET can serialize only data exposed by public read/writable properties and fields.

Sometimes an object will want a degree of control over how it is serialized. For example, an object might contain state that is specific to the machine on which it is located, such as a handle to a system resource. In this case, the class can implement the ISerializable interface.

Instances of the LicenseInfo class can be serialized by the runtime, so I will decorate it with the Serializable attribute. Here is the rest of the Licensing Web service implementation, which defines the LicenseInfo class:

    [Serializable]     public class LicenseInfo     {         string license = "";         DateTime expirationDate = new DateTime();         public LicenseInfo(string license, DateTime expirationDate)         {             this.license = license;             this.expirationDate = expirationDate;         }         public string License         {             get{ return this.license; }         }         public DateTime ExpirationDate         {             get{ return this.expirationDate; }         }     } }

Now that I have implemented the Licensing Web service, I need to configure it so that it is hosted by the Remoting runtime. The runtime provides two different activation models, well-known object and client-activated object.

Well-Known Object Activation Model

A well-known object accepts method requests without requiring the client to first formally instantiate the object. From the client's perspective, the object already exists and calls can be made to it without the need to create or initialize the object. This is the default behavior of SOAP-based Web services.

Remoting supports two configurations for well-known objects, single call and singleton. These configurations control when the well-known object that handles the client's requests is actually instantiated on the server.

The single call configuration is the most synonymous with ASP.NET. Each time a request is received, the Remoting service creates a new instance of the target object to process the request.

With the singleton configuration, the target object is instantiated when the first request is received. The object is kept alive by the Remoting runtime and is responsible for processing all subsequent requests by all clients. All requests received by the service from any client are handled by the same object.

You should consider the singleton configuration when the same expensive resources are leveraged across multiple method requests. An expensive resource can be initialized within the singleton object's constructor. It can then be used to process multiple method requests over the duration of the object's lifetime. Because the resource might be used to process multiple requests simultaneously, either the resource must be thread safe or you should use the appropriate synchronization primitives.

In general, remote objects that support the single call configuration are the easiest to develop. Because each request is processed by a separate instance of the remote object, the developer need not be concerned with coherency issues such as race conditions and deadlocks.

With either configuration, a client is never allocated its own object that spans across more than one method call. In the case of the single call configuration, the state is never maintained across method calls because each request is addressed by a new instance of the object. In the case of the singleton configuration, the state of the object is shared across all clients. Therefore, if a well-known object must manage state specific to a particular client, it must use an out-of-band mechanism.

Client-Activated Object Activation Model

In the client-activated scenario, a remote object is created on behalf of the client and is available to that client until it is garbage collected. Because SOAP does not define a protocol that supports activation, Remoting defines an extension mechanism for facilitating this behavior.

The Licensing Web service does not have to initialize expensive resources and does not require lifetime management services, so I will configure it as a well-known object that supports the single call mode.

You can configure the remote component by creating an XML file. The following XML file configures the Licensing Web service:

<?xml version="1.0" encoding="utf-8" ?> <configuration>   <system.runtime.remoting>     <application>       <service>         <wellknown mode="SingleCall"          type="SomeRecordCompany.Licensing, Licensing"          objectUri="Licensing.soap"/>       </service>     </application>   </system.runtime.remoting> </configuration>

The service element contains the configuration for objects exposed to other applications. It can contain either a wellknown or activated element. (I cover configuring client-activated objects later in the chapter.)

You use the wellknown element to configure well-known objects. The element must contain three attributes: mode, type, and objectUri. The mode attribute indicates whether the well-known object should be hosted as a single call (SingleCall) or a singleton (Singleton). The type attribute contains the full type name and the assembly of the object that should be exposed by the Remoting runtime. The objectUri attribute specifies the URI where the object can be accessed. In the case of the Licensing Web service, the URI specified is Licensing.soap. Licensing.soap does not physically exist; requests sent to the URI are intercepted and processed by the Remoting runtime.

The Licensing Web service can be hosted within any process. Because many clients might access the Licensing Web service simultaneously, a good option is to host the Web service within Microsoft Internet Information Services (IIS). This allows the Web service to be hosted in a Web farm that takes advantage of applications that improve the manageability of the Web farm (such as Microsoft Application Center).

The Licensing Web service can be deployed within any IIS Web application. You can create a new Web application by opening the IIS MMC, right-clicking on the Web site, selecting New, and then choosing Virtual Directory. Once the Web application has been created, the contents of the configuration file I just created can be copied into the Web application's web.config file.

The Remoting runtime will expect the assemblies to be located within the Web application's bin directory. Therefore, you must create a bin subdirectory and copy the Licensing.dll assembly into the new directory.

You can access the Licensing Web service by addressing the Licensing.soap file within the Web application directory. For example, if the Licensing Web service is located in the SomeRecordCompany Web application on my local server, I can address it using the following URL:

http://localhost/SomeRecordCompany/Licensing.soap

If the Web service is hosted in IIS, the WSDL document will be automatically generated if wsdl is appended as a query string on the URL. The WSDL document for the Licensing Web service is available at the following URL:

http://localhost/SomeRecordCompany/Licensing.soap?wsdl

Creating a WinForm-Hosted Web Service

The primary purpose of Grabber.NET is to facilitate the exchange of files, so it needs a way to obtain a file from a remote computer. In this section, I create the SoapFileShare Web service, which supports two endpoints, one for retrieving files and the other for navigating directories.

The File endpoint allows a client to obtain the contents of the requested file from a remote computer. Here is the implementation:

using System; using IO = System.IO; namespace SoapFileShare {     public class File : MarshalByRefObject     {         string rootDirectory = @"c:\temp\";         public byte[] GetFile(string fileName, string license)         {             // Validate the license...             // Obtain the contents of the requested file.             IO.Stream    s = IO.File.Open(rootDirectory + fileName,                                            IO.FileMode.Open);             byte[]       fileContents = new byte[s.Length];             s.Read(fileContents, 0, (int)s.Length);             return fileContents;         }     }

The File endpoint exposes the GetFile method. The name of the targeted file and the necessary licensing information are passed to the GetFile method, which uses the information to determine whether the client is licensed to receive the file. If the client is licensed to receive the file, the GetFile method obtains a byte array for the requested file and returns it to the client. Later in the chapter, I discuss how to access the Licensing Web service, which validates the request.

Grabber.NET also needs to browse the remote computer to see what files are available, so I need to create the Directory endpoint. The Directory endpoint exposes methods that allow the client to navigate the directory hierarchy on the remote computer and obtain a list of files within a particular directory:

    public class Directory : MarshalByRefObject     {         string rootDirectory = @"c:\temp";         // Get the list of files at the root directory.         public string[] GetFiles(string path)         {             // Obtain the list of files in the directory.             string    [] fileNames =                        IO.Directory.GetFiles(rootDirectory + path);             // Truncate the path information so that it is relative              // to the root directory.             for(int i = 0; i < fileNames.Length; i++)             {                 char [] newFileName =                  new char[fileNames[i].Length - rootDirectory.Length];                 fileNames[i].CopyTo(rootDirectory.Length, newFileName, 0,                  fileNames[i].Length - rootDirectory.Length);                 fileNames[i] = new string(newFileName);             }             return fileNames;         }         // Get the list of files at the root directory.         public string[] GetDirectories(string path)         {             string [] directories =                     IO.Directory.GetDirectories(rootDirectory + path);             // Truncate the path information so that it is relative              // to the root directory.             for(int i = 0; i < directories.Length; i++)             {                 char [] newDirectory =                  new char[directories[i].Length - rootDirectory.Length];                 directories[i].CopyTo(rootDirectory.Length, newDirectory, 0,                  directories[i].Length - rootDirectory.Length);                 directories[i] = new string(newDirectory);             }             return directories;         }     } }

The Directory endpoint exposes two methods, GetFiles and GetDirectories. GetFiles returns a list of files within a specified directory, and GetDirectories returns a list of subdirectories within a specified directory. You can use these two methods to navigate a directory hierarchy.

The primary purpose of Grabber.NET is to allow the exchange of files between peers. A peer-to-peer application has to act as both a client and a server. Therefore, the WebForms application itself must act as a Remoting server.

One major advantage of the Remoting framework over technologies such as ASP.NET is its ability to host a Web service in any process over any transport protocol. In this case, I want the Grabber.NET WinForm application to listen for SOAP requests over HTTP.

Any .NET application can listen for incoming requests by calling the Configure static method on the RemotingConfiguration object to initialize the appropriate listener. The method accepts the path to a configuration file as its only parameter. The configuration file is similar to the one I created for the Licensing Web service. However, unlike a Remoting Web service hosted in IIS, a Remoting Web service hosted within a process is not limited to HTTP. Therefore, the channel needs to be configured.

The following configuration file configures Remoting to listen on port 88 for HTTP requests for the File or Directory endpoint:

<?xml version="1.0" encoding="utf-8" ?> <configuration>   <system.runtime.remoting>     <application name="SoapFileShare">       <service>         <wellknown mode="SingleCall" type="SoapFileShare.Directory,          SoapFileShare" objectUri="Directory.soap"/>         <wellknown mode="SingleCall" type="SoapFileShare.File, SoapFileShare"         objectUri="File.soap"/>       </service>       <channels>         <channel port="88" type="System.Runtime.Remoting.Channels.Http.         HttpChannel, System.Runtime.Remoting" />       </channels>     </application>   </system.runtime.remoting> </configuration>

As with the Licensing Web service hosted in IIS, you must configure the Remoting runtime to host the Web service. Recall that a Remoting Web service can be hosted via any number of transport protocols. Because the Licensing Web service is hosted in IIS, the HTTP transport was assumed. Also, because the Licensing Web service is hosted in a particular Web application, I needed to specify only the filename of the endpoint. But the SoapFileShare Web service is not hosted within IIS, so I need to specify the channel as well as the full endpoint to the path.

As in Web services hosted in IIS, the objectUri property of the wellknown element specifies the filename that will serve as the address of the endpoint. But unlike in Web services hosted in IIS, you must specify the path to the file using the name attribute of the application element.

In the case of Grabber.NET, the directory is SoapFileShare, so the Directory endpoint is addressable at http://localhost/SoapFileShare/Directory.soap. You can also specify a subdirectory. For example, if the name attribute is set to Grabber.NET/SoapFileShare, the Directory endpoint is addressable at http://localhost/Grabber.NET/SoapFileShare/Directory.soap.

The transport protocols supported by Remoting are defined within the channels element. Each supported transport protocol is referenced within an individual channel element. The channel element contains two attributes, port and type. The port attribute specifies the port the transport protocol will use to communicate with the remote application, and the type attribute specifies the .NET type that implements the channel and the type's assembly.

By convention, the configuration file for the application has the same name as the assembly, with .config appended to it. In the case of Grabber.NET, the WinForm application is named SoapFileExplorer.exe, so I will name the configuration file SoapFileExplorer.exe.config.

Once the configuration file has been created, it must be explicitly loaded by the application. You do this by passing the path of the configuration file to the Configure static method exposed by the RemotingConfiguration object. In the case of Grabber.NET, the Configure method is called within the constructor for the main WinForm, as shown here:

public ExplorerForm() {     //     // Required for Windows Form Designer support     //     InitializeComponent();     // Load the Remoting configuration files.     RemotingConfiguration.Configure("SoapFileExplorer.exe.config"); }

You can also configure a well-known object within the application code itself so that you can dynamically configure a well-known object at run time. The following code configures the File and Directory well-known objects:

RemotingConfiguration.ApplicationName = "SoapFileShare"; RemotingConfiguration.RegisterWellKnownServiceType(typeof(SoapFileShare.File), "File.soap", WellKnownObjectMode.SingleCall); RemotingConfiguration.RegisterWellKnownServiceType(typeof(SoapFileShare. Directory), "Directory.soap", WellKnownObjectMode.SingleCall);

Regardless of how the well-known objects are configured, Remoting will spin up another thread to listen for and process incoming requests.

Accessing Web Services

Now that I have created the Licensing and SoapFileShare Web services for Grabber.NET, it is time to write the client portion of Grabber.NET to access these Web services. In this section, I discuss three ways to create a Remoting proxy that will be used to access a Web service.

Recall that the GetFile method of the File object is responsible for sending the requested file to the client. Before the file is sent, the licensing information received from the client must be verified against the Licensing Web service. For this example, I will use the new operator to create the proxy object.

new Keyword–Generated Proxy

You can configure the Remoting runtime to intercept calls to the new operator and return a dynamically generated proxy instead of the object itself. You do this by registering the well-known object within the Remoting configuration file. The following is the modified version of the SoapFileExplorer.exe.config file:

<?xml version="1.0" encoding="utf-8" ?> <configuration>   <system.runtime.remoting>     <application name="SoapFileShare">       <service>         <wellknown mode="SingleCall" type="SoapFileShare.Directory,          SoapFileShare" objectUri="Directory.soap" />         <wellknown mode="SingleCall" type="SoapFileShare.File, SoapFileShare"         objectUri="File.soap" />       </service>       <client>         <wellknown type="SomeRecordCompany.Licensing, Licensing"          url="http://localhost/SomeRecordCompay/Licensing.soap" />       </client>       <channels>         <channel port="88" type="System.Runtime.Remoting.Channels.Http.         HttpChannel, System.Runtime.Remoting" />       </channels>     </application>   </system.runtime.remoting> </configuration>

I added a client element that contains a list of Web service endpoints used by the client. Individual endpoints are referenced by adding a wellknown child element. Note that the wellknown element under the client element is different from the wellknown element under the server element.

The client wellknown element must contain two attributes, type and url. The url attribute contains the address of the targeted endpoint. The type attribute references the .NET type within a particular assembly that describes the remote object. The assembly can be the Licensing.dll that I previously deployed on IIS.

You can also register the well-known object reference using the RegisterWellKnownClientType static method exposed by the RemotingConfiguration class. The following example registers the Licensing well-known object:

RemotingConfiguration.RegisterWellKnownClientType(typeof(SomeRecordCompany. Licensing), "http://localhost/SomeRecordCompany/Licensing.soap");

Because the generated proxy is strongly typed, the same Licensing assembly that was referenced within the Remoting configuration file also needs to be referenced by the client application itself. In this case, the SoapFileShare Web service application must reference the Licensing.dll assembly.

In many cases, it is not practical to have clients reference the assembly that contains the implementation of the Web service. Later in this chapter, I discuss how to build a .NET assembly containing the necessary metadata from the Web service's WSDL file.

Once the configuration file has been modified and the License.dll assembly is referenced by the SoapFileShare project, the Remoting runtime will automatically create a proxy object on behalf of the client when the new operator is called. The Validate method can then be called on the resulting proxy object and the proxy will forward the call to the Licensing Web service. Here is the implementation:

// Validate the license info if it was sent by the client. if(licenseInfo != null) {     SomeRecordCompany.Licensing licensing = new SomeRecordCompany.Licensing();     licensing.Validate(fileName, licenseInfo); }

Other than the fact that I load the Remoting configuration file when the WinForms application is initialized, the code for accessing the Licensing Web service is no different than if I were directly accessing the assembly.

GetObject-Generated Proxy

Next I need to create the client code to access the SoapFileShare Web service. Because the SoapFileShare Web service can be hosted by any number of Grabber.NET peers, using the new keyword to create the proxy raises a significant issue: once a well-known object has been configured by the Remoting runtime, the resulting proxy will always be associated with the same endpoint.

Another way to create a proxy for a well-known object is by calling the GetObject static method on the Activator class. When the new keyword is used to create a proxy object, the Remoting runtime actually calls the GetObject method to obtain the proxy. Because the Activator object is public, you can call it directly.

The GetObject method is overloaded and supports two method signatures. Both versions of the GetObject method accept two parameters, the type of object that should be created and the URL where the well-known object is located. The second GetObject method signature also accepts an object containing channel-specific data.

Recall that SoapFileExplorer has a look and feel similar to that of Windows Explorer. The right pane contains a TreeView control for navigating the directory structure, and the left pane has a ListView control. When a particular node in the TreeView control is selected, the ListView control is refreshed with all of the files contained within the particular directory. The following code updates the ListView control based on the list of files obtained from the SoapFileShare Web service:

private void directoryTree_AfterSelect(object sender,  System.Windows.Forms.TreeViewEventArgs e) {     // Create an instance of the SoapFileShare.Directory object.     string url = ((DirectoryTreeNode)e.Node).Url + "Directory.soap";     SoapFileShare.Directory directory = (SoapFileShare.Directory)Activator.     GetObject(typeof(SoapFileShare.Directory), url);     // Obtain the files within the selected directory.     string [] filePaths = directory.GetFiles(((DirectoryTreeNode)e.Node).Path);     // Display the files within the list view.     this.fileList.Clear();     foreach(string filePath in filePaths)     {         this.fileList.Items.Add(new FileListViewItem(url, filePath));     } }

First the URL of the targeted Directory endpoint of the SoapFileShare Web service is dynamically built. This URL is then passed to the GetObject method to obtain a proxy object for the Directory endpoint. A list of files is then obtained for the selected directory by calling the GetFiles method on the proxy.

By default, GetObject will create a proxy object that communicates with the well-known object via SOAP over HTTP. Therefore, you need not configure the Remoting runtime to execute the preceding code.

WSDL-Generated Proxy

To dynamically create a proxy, the Remoting runtime needs access to an assembly that contains type information that describes the targeted Web service. As I mentioned earlier, this can be the assembly that contains the implementation of the Web service.

Because Grabber.NET both hosts and consumes the SoapFileShare Web service, it is practical to have the Remoting runtime dynamically generate a proxy for the SoapFileShare Web service. However, it is not practical to have Grabber.NET reference the assembly that contains the implementation of the Licensing Web service. You need some way of creating an assembly that contains the type information used to describe the Web service without containing the implementation.

You can use one of the tools provided by the Remoting framework, SoapSuds, to convert the type information contained in a WSDL document into .NET type information. This type information can then be used by the Remoting runtime to create a proxy dynamically.

The following command creates an assembly containing metadata that describes the Licensing Web service:

soapsuds -url:http://localhost/SomeRecordLabel/Licensing.soap?wsdl  -oa:Licensing.dll –gc -nowp

This command creates an assembly called Licensing.dll as well as the source code for the assembly. Either the assembly can be referenced or the source code can be included by a client application such as Grabber.NET that creates proxies using the new keyword or the GetObject method.

Table 8-1 describes the command-line parameters supported by SoapSuds.

Table 8-1  Command-Line Parameters Supported by SoapSuds 

Switch

Description

-domain:domainName or -d:domainName

The domain against which the passed credentials should be authenticated.

-generatecode or –gc

Tells SoapSuds to generate source code for the proxy.

-httpproxyname:proxy or -hpn:proxy

The name of the proxy server that should be used to connect to the Web server to obtain the WSDL.

-httpproxyport:port or -hpp:port

The port number for the proxy server that should be used to connect to the Web server to obtain the WSDL.

-inputassemblyfile:fileName or -ia:fileName

The name of the assembly file from which to obtain type information. Do not include the extension when specifying the filename.

-inputdirectory:directory or -id:directory

The directory of the input assembly files.

-inputschemafile:fileName or -is:fileName

The name of the WSDL file from which to obtain type information.

-nowrappedproxy or -nowp

Specifies that the transparent proxy should not be wrapped within a derived version of the RemotingClientProxy class.

-outputassemblyfile:fileName or -oa:fileName

The name of the assembly file that will contain the generated proxy. Whenever an assembly is created, the associated source code will also be created.

-outputdirectory:directory or -od:directory

The directory where all output files will be saved.

-outputschemafile:fileName or -os:fileName

The filename of the generated WSDL or SDL document.

-password:password or -p:password

The password that should be used to authenticate against the server from which the WSDL or SDL document is obtained.

-proxynamespace:namespace or -pn:namespace

The namespace in which the resulting proxy class will reside.

-sdl

Specifies that SoapSuds should generate an SDL file that describes the types contained within a particular assembly.

-serviceendpoint:URL or -se:URL

The URL that should be placed within a generated WSDL or SDL file to describe the endpoint.

-strongnamefile:fileName or -sn:fileName

The file that contains the key pair that should be used to sign the generated assembly.

-types:type1,assembly[,endpointUrl] [type1,assembly[,endpointUrl]] [...]

The specific types that will serve as input.

-urltoschema:URL or -url:URL

The URL from which the WSDL or SDL file can be obtained.

-username:username or -u:username

The username that should be used to authenticate against the server from which the WSDL document is obtained.

-wrappedproxy or -wp

Specifies that the transparent proxy should be wrapped within a derived version of the RemotingClientProxy class.

-wsdl

Specifies that SoapSuds should generate a WSDL file that describes the types contained within a particular assembly.

One of the more interesting command-line parameters is the -wp switch. This parameter allows you to create a wrapped proxy. A wrapped proxy is a class that derives from the RemotingClientProxy class. Its primary purpose is to expose properties that allow you to more easily configure the HTTP channel. Table 8-2 describes the parameters exposed by the RemotingClientProxy class.

Table 8-2  Parameters of the RemotingClientProxy Class

Property

Description

AllowAutoRedirect

Determines whether the proxy will honor a redirect request sent by the server.

Cookies

Used to access the cookies that have been sent from the server.

Domain

The domain against which the passed credentials should be authenticated.

EnableCookies

Specifies whether cookies will be accepted by the proxy.

Password

The password that should be used to authenticate against the Web service.

Path

The URL of the Web service's endpoint.

PreAuthenticate

Determines whether the authentication credentials should be sent immediately or as a result of receiving a 401 (access denied) error.

ProxyName

The name of the proxy server that should be used to access the Web service.

ProxyPort

The port number of the proxy server that should be used to access the Web service.

Timeout

Determines the period of time, in milliseconds, that a synchronous Web request has to complete before the request is aborted. The default is infinite (-1).

Url

The URL of the Web service's endpoint.

UserAgent

The value of the user agent HTTP header sent to the Web service.

Username

The username that should be used to authenticate against the Web service.

By default, the HTTP channel uses the Internet settings configured on the client's machine using Control Panel, so in most cases it is not necessary to configure the proxy settings using the wrapped proxy. If the client's operating system is configured to route requests through an HTTP proxy server, these settings will be applied to the proxy as well.

Because SoapSuds will generate the source code for the wrapped proxy, you can extend its implementation. For example, you can add client-side logic to validate the parameters before a call is made to the remote server.

Adding SOAP Headers

The final piece of implementation I need to do is to integrate Grabber.NET with the Licensing Web service. Each time a client requests a file from another peer, the peer responding to the request must ensure that the client is licensed to receive the content.

The distribution of some files is limited by licensing agreements, and some of those files are in the public domain. Because license information is not part of the core functionality of the GetFile method of the File Web service, I will pass it within the SOAP header.

You can add headers to the message by using the SetHeaders static function exposed by the CallContext class. The SetHeaders method accepts an array of objects of type Header. Each instance of the Header class encapsulates data about a particular SOAP header. Table 8-3 describes the properties defined by the Header class.

Table 8-3  Properties of the Header Class

Property

Description

HeaderNamespace

The XML namespace of the header in which the element is defined. The default is http://schemas.microsoft.com/clr/soap.

MustUnderstand

Determines whether the header must be understood by the Web service. The default is true.

Name

The name of the header. Sets the name of the root element for the header within a SOAP message.

Value

The object that will be serialized within the header.

I will add the License header to calls made to the GetFile method of the File Web service. This header will contain the serialized contents of an object that holds the client's license information. First I need to declare a class that will be used to contain the license information. Here is the implementation:

[Serializable]public class LicenseInfo {     string license = "";     DateTime expirationDate = new DateTime();     public LicenseInfo(string license, DateTime expirationDate)     {         this.license = license;         this.expirationDate = expirationDate;     }     public string License     {         get{ return this.license; }     }     public DateTime ExpirationDate     {         get{ return this.expirationDate; }     } }

Because the contents of an instance of the LicenseInfo object will be serialized into the License header, I had to indicate that the object can be serialized. I did this by decorating the LicenseInfo class with the Serializable attribute.

Next I will add a call to the SetHeaders method of the CallContext object to add the header to the GetFile SOAP request. Grabber.NET calls the GetFile method as a result of handling the click event on the Copy menu item. Here is the implementation:

private void copyMenu_Click(object sender, System.EventArgs e) {     // Obtain the destination directory from the user.     DirectoryForm directoryForm = new DirectoryForm();     if(directoryForm.ShowDialog() == DialogResult.OK)     {         // Create an instance of the SoapFileShare.File object.         string url = ((DirectoryTreeNode)directoryTree.SelectedNode).Url +         "File.soap";         SoapFileShare.File file = (SoapFileShare.File)Activator.GetObject         (typeof(SoapFileShare.File), url);         // Create the Licensing SOAP header.         Header licenseHeader = new Header("Licensing", this.licenseInfo,          false);         string destinationDirectory = directoryForm.Path;         // Copy the selected files into the destination directory.         foreach(FileListViewItem fileNode in fileList.SelectedItems)         {             // Set the Licensing SOAP header.             CallContext.SetHeaders(new Header [] {licenseHeader});             // Obtain file contents.             byte [] fileContents = file.GetFile(fileNode.Path);             // Parse the filename from the file path.             int index;             for(index = fileNode.Path.Length - 1;              index > 0 && fileNode.Path[index] != '\\'; index--);             char [] text = new char[fileNode.Path.Length - index - 1];             fileNode.Path.CopyTo(index + 1, text, 0,              fileNode.Path.Length - index - 1);             string fileName = new string(text);             // Write file to the destination directory.             Stream s;             s = File.OpenWrite(destinationDirectory + fileName);             s.Write(fileContents, 0, fileContents.Length);             s.Close();         }     } }

The preceding code displays the DirectoryForm dialog box to obtain from the user the directory to which the files should be copied. Then, for each file selected by the user, the file is copied into the destination directory. To place the License header in each GetFile SOAP request, I called the SetHeaders method each time just before I called the GetFile method.

The GetFile method can retrieve the SOAP header using the CallContext object's GetHeaders static method. Here is the implementation:

public byte[] GetFile(string fileName) {     LicenseInfo licenseInfo = null;     // Make sure the client sent valid license information.     Header [] headers = CallContext.GetHeaders();     for(int i = 0; i < headers.Length   licenseInfo == null; i++)     {         licenseInfo = headers[i].Value as LicenseInfo;     }     if(licenseInfo != null)     {         // Validate the licensing information against          // the Licensing Web service...     }     // The rest of the implementation... }

First the GetHeaders method is called to obtain an array of Header objects. Then the array is iterated through until the Licensing header is found or the end of the array is reached. Finally, if the Licensing header is found, the information is passed to the Licensing Web service.

You should be aware of a couple of issues regarding the support for SOAP headers in Remoting. First, as you must with ASP.NET, you have to be careful about receiving a header containing the mustUnderstand attribute set to true. If a header that must be understood by the Web service was not processed after the method returns, the Remoting runtime will automatically generate an exception. So if the implementation of a method exposed by the Web service requires compensating logic in the event of an exception being thrown, you need to take appropriate action. Your choices would be to either verify that there are no unsupported required headers before you run your code or intercept the exception that results from an unhandled required header and then execute the necessary compensation logic.

The other issue is that supported headers will not be exposed within the WSDL that is dynamically generated by the Remoting runtime. If it is necessary to advertise the headers supported by the Web service, you will need to manually modify the WSDL. Two possible options would be either to create a static WSDL document or to intercept the dynamically generated WSDL and inject the header definitions.

Generating WSDL

One of the advantages of implementing Grabber.NET with SOAP over HTTP is that you are not limited only to sharing files with other Grabber.NET peers. You can create an application, potentially on other platforms, that can interact with Grabber.NET peers.

To implement a compatible Web service and proxy, you need access to the interface definition for the various Web services that are used by Grabber.NET. As you have seen, the WSDL describing a Remoting component hosted in IIS can be obtained by passing a query string containing WSDL to the Web service endpoint. However, this is not available to Remoting Web services that are hosted by processes other than IIS.

For these cases, you can use SoapSuds to generate a WSDL document to describe the interfaces supported by the Web service. The resulting WSDL document can then be sent directly to the developer or posted on a Web site. The following SoapSuds command generates a WSDL document that describes both the File and the Directory Web services:

soapsuds -wsdl -types:SoapFileShare.Directory,SoapFileShare, http://localhost/SoapFileShare/Directory.soap;SoapFileShare.File,SoapFileShare, http://localhost/SoapFileShare/File.soap;SomeRecordCompany.LicenseInfo, SomeRecordCompany -os:SoapFileShare.wsdl

Unlike ASP.NET, the SoapSuds utility allows you to create WSDL documents that describe a Web service with multiple endpoints. The preceding command will generate a file named SoapFileShare.wsdl that contains one Web service definition with two endpoints, one for the File class and one for the Directory class.

One issue with the SoapSuds-generated WSDL documents is that there is no way of specifying the name of the Web service. The name of the Web service defaults to the name of the first endpoint specified by the types flag. Here is the service description within SoapFileShare.wsdl:

<service name='DirectoryService'>     <port name='DirectoryPort' binding='ns0:DirectoryBinding'>         <soap:address location=         'http://localhost/SoapFileShare/Directory.soap'/>     </port>     <port name='FilePort' binding='ns0:FileBinding'>         <soap:address location='http://localhost/SoapFileShare/File.soap'/>     </port> </service>

You can modify the name of the Web service within the generated WSDL document. If the endpoints within the document are not related, you can also wrap each endpoint within its own service element. In this case, because File and Directory are related, I could change the name of the service from DirectoryService to SoapFileShareService.

The types flag indicates which classes I want to have described within the WSDL document. The classes that I want exposed as Web services also include their respective endpoints. If the resulting WSDL document contained only one Web service definition, I could have specified the endpoint using the se flag. (See Table 8-1 earlier in the chapter for a list of command-line parameters for SoapSuds.)

You also need to list any additional types that must be represented within the schema. For example, the LicenseInfo type is used within the License SOAP header, so I included it within the types flag.

Suds WSDL Extension Elements

Remoting defines a set of WSDL extension elements called Suds. The Suds extension elements are used to contain additional metadata necessary to maintain full fidelity with the .NET platform.

The Suds extension elements appear in every Remoting-generated WSDL document. For example, every .NET type represented within a WSDL document will have a corresponding binding element that contains a suds:class element that describes additional information about the .NET type (such as its root type). The following is the binding definition for the Directory class:

<binding name='DirectoryBinding' type='ns0:DirectoryPortType'>     <soap:binding style='rpc'      transport='http://schemas.xmlsoap.org/soap/http'/>     <suds:class type='ns0:Directory' rootType='MarshalByRefObject'>     </suds:class>     <!-- Additional definitions... --> </binding>

The Suds extension elements are proprietary to the Remoting framework, but if your Web service does not leverage any services that extend the SOAP specification, the Suds extension elements can be safely ignored by other Web service implementations. A Web service developed on the Remoting framework can thus maintain a high degree of interoperability.



Building XML Web Services for the Microsoft  .NET Platform
Building XML Web Services for the Microsoft .NET Platform
ISBN: 0735614067
EAN: 2147483647
Year: 2002
Pages: 94
Authors: Scott Short

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