For web service developers working strictly on the Windows platform, Microsoft's .NET development platform offers built-in support for easily creating and deploying SOAP web services. Let's walk through how you create the Hello World service using C#, the new Java-like programming language designed specifically for use with .NET.
The first thing you need to do is download and install the Microsoft .NET SDK Beta 2 from http://msdn.microsoft.com. This free distribution contains everything you need to create and run any .NET application, including .NET Web Services.
There are, however, several prerequisites that you need:
The .NET Framework SDK installation is a fairly automatic process, with an easy-to-use installation wizard. Once installed, we can create the Hello World service.
Before we get into exactly how web services are created in .NET, let's take a quick walk through the .NET architecture to help put things into perspective.
First and foremost, .NET is a runtime environment similar to the Java Virtual Machine. Code packages, called assemblies, can be written in several .NET specific versions of popular programming languages like Visual Basic, C++, C#, Perl, Python, and so on. Assemblies run within a managed, hierarchically organized runtime called the "Common Language Runtime" that deals with all of the low-level memory and system management details (see Figure 3-4).
Currently, .NET runs basically as an extension to the existing COM environment upon which the current versions of Windows are built. As such, .NET can be utilized anywhere COM can be used, including within Microsoft's Internet Information Server (IIS) environment.
.NET web services are specific types of .NET assemblies that are specially flagged for export as web services. These assemblies are either contained within or referenced from a new type of server-side script called an .asmx file. The .NET extensions to IIS recognize files ending in .asmx as web services and automatically export the functions of the referenced assemblies.
The process is simple:
.NET introduces a programming language called C#. We'll develop our example web service in C#, but remember that .NET makes it just as easy to develop in Visual Basic, C++, and other languages.
Example 3-17 defines the .NET Hello World service. You can use an ordinary text editor to create this file.
Example 3-17: HelloWorld.asmx, a C# Hello World Service
<%@ WebService Language="C#" Class="Example1" %>
using System.Web.Services;
[WebService(Namespace="urn:Example1")]
public class Example1 {
[ WebMethod ]
public string sayHello(string name) {
return "Hello " + name;
}
}
Notice how similar the code looks to the Java version we created earlier. At heart, a function that appends two strings isn't rocket science. The bracketed sections (<%
%>
and [
]
) tell the .NET runtime that this code is intended to be exported as a SOAP web service.
The <% WebService Language="C#" Class="Example1" %>
line tells .NET that we are exporting one web service, written in C#, implemented by the Example1
class.
The using
line imports a module, in this case the standard web services classes.
The [WebService(Namespace="urn:Example1")]
line is optional, but allows us to declare various attributes of the web service being deployed. In this instance, we are setting an explicit namespace for the web service rather than allowing .NET to assign a default (which, by the way, will always be http://tempuri.org/
). Other attributes you can set for the web service include the name and textual description of the service.
The [
WebMethod
]
line sets an attribute that flags the methods in the class to be exposed as part of the web service. As with the WebService
attribute previously, we could use this line to define various custom properties about the web service operation. Options include whether to buffer the response of the operation; if buffered, how long to keep it buffered; whether to maintain a session state for the operation; whether transactions are supported on the method; what the exported name of the operation is; and a textual description of the operation. In the case of the Hello World example, we have no need to set any of these options, so we simply leave it alone.
What will .NET do with all of this information? It's actually quite simple. Whenever a .asmx file is requested by a client through IIS, the .NET runtime will first compile the code for the service if it hasn't done so already. The compiled code is temporarily cached in memory and recompiled every time a change is made to the .asmx file or the IIS server is restarted.
Next, the .NET runtime will determine what type of request is being made. There are several choices:
Save the HelloWorld.asmx file to a location in your IIS web root. Take note of the .asmx file's URL. For example, if your Microsoft IIS server is installed at c:/inetpub (the default installation location), the web root is c:/inetpub/wwwroot. If you saved the .asmx file directly to this location, the URL of the .asmx file will be http://localhost/helloworld.asmx, where localhost is the DNS name or IP address of your IIS server. Once you've completed this step, your .NET web service is deployed.
Ensure that your .NET environment and web service are fully operational by launching your favorite web browser and navigating to http://localhost/HelloWorld.asmx. If all goes well, you should be presented with an automatically generated HTML page that documents the Hello World service you just created (see Figure 3-5).
These pages are generated dynamically whenever an HTTP-GET request is received for the deployed .asmx file. You do not have to do anything to create these pages.
Clicking on the "sayHello" link will yield a detailed description of how to invoke the sayHello
operation using SOAP, HTTP-GET, and HTTP-POST, as well as a simple HTML form for testing the operation (see Figure 3-6).
To test the service, either type your name in the test form at the top of the automatically generated documentation page (see Figure 3-7), or navigate your browser to http://localhost/helloworld.asmx/sayHello?name=yourname.
Either method should generate the response shown in Figure 3-8.
If you get the "Hello James" message, you're ready to move on.
Creating a SOAP client for the Hello World service using .NET is, surprisingly, harder than creating the service itself. There are tools to make it easier (we will explore them briefly in Chapter 5), but for now we'll go through the steps manually so you know what is going on.
Again using your favorite text editor, create HelloWorld.cs (the .cs extension indicates C# source code) from Example 3-18.
Example 3-18: HelloWorld.cs, a C# HelloWorld Client
// HelloWorld.cs
using System.Diagnostics;
using System.Xml.Serialization;
using System;
using System.Web.Services.Protocols;
using System.Web.Services;
[System.Web.Services.WebServiceBindingAttribute(
Name="Example1Soap",
Namespace="urn:Example1")]
public class Example1 :
System.Web.Services.Protocols.SoapHttpClientProtocol {
public Example1( ) {
this.Url = "http://localhost/helloworld.asmx ";
}
[System.Web.Services.Protocols.SoapDocumentMethodAttribute(
"urn:Example1/sayHello",
RequestNamespace="urn:Example1",
ResponseNamespace="urn:Example1",
Use=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
public string sayHello(string name) {
object[] results = this.Invoke("sayHello",
new object[] {name});
return ((string)(results[0]));
}
public static void Main(string[] args) {
Console.WriteLine("Calling the SOAP Server to say hello");
Example1 example1 = new Example1( );
Console.WriteLine("The SOAP Server says: " +
example1.sayHello(args[0]));
}
}
The [System.Web.Services.WebserviceBindingAttribute]
line tells the .NET managed runtime that this particular .NET assembly is going to be used to invoke a web service. When the assembly is compiled, .NET will automatically supply the infrastructure to make the SOAP request work.
Subclassing System.Web.Services.Protocols.SOAPHttpClientProtocol
tells the .NET runtime which protocol you want to use (SOAP over HTTP in this case). Within the constructor for this class, set the URL for the web service (the assignment to this.Url).
The rest of the class declares a proxy for the sayHello
operation, specifies various attributes of the web services invocation, calls the invoke method, and returns the result.
Lastly, we create the main entry point for the C# application. The entry point does nothing more than create an instance of our client class and invoke the proxy sayHello
operation, outputting the results to the console.
Compile the client to a HelloWorld.exe application:
C:/book>csc HelloWorld.cs
To invoke the web service, simply type:
C:/book>HelloWorld yourname
You will be greeted with the same result we saw previously with the Java and Perl versions of the Hello World service:
Calling the SOAP Server to say hello
The SOAP Server says: Hello James
At the time of this writing, .NET's SOAP implementation still has a few issues that need to be worked out, primarily in the area of interoperability.
Slight variations between the way .NET implements SOAP and SOAP::Lite's implementation of SOAP, for example, cause some difficulty in allowing the two to work together out of the box. To illustrate the problem, follow the steps shown here. One would think that everything would work fine, but it doesn't. I'll point out why after we walk through it.
First, launch the Java TcpTunnelGui tool that ships with Apache SOAP, specifying port 8080 as the local listening port, and redirecting to whatever server you have your HelloWorld.asmx file deployed to:
C:/book>start java org.apache.soap.util.net.TcpTunnelGui 8080
localhost 80
Then, modify the Perl Hello World client to point to the HelloWorld.asmx file, but replace the server part of the URL with localhost:8080.
When you run the Perl script:
C:/book>perl hello_client1.pl James
The result is not what you would expect. The script ends without ever displaying the "Hello James" result. If you take a look at the TcpTunnelGui tool, you'll see that the SOAP message is sent, but the .NET runtime rejects the request and issues a SOAP fault in response. This is shown in Example 3-19.
Example 3-19: SOAP fault from .NET
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>
System.Web.Services.Protocols.SoapException: Server did
not recognize the value of HTTP Header SOAPAction:
urn:Example#sayHello.
at System.Web.Services.Protocols.SoapServerProtocol.Initialize( )
at System.Web.Services.Protocols.ServerProtocolFactory.Create(
Type type, HttpContext context, HttpRequest request,
HttpResponse response)
</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>
.NET requires that the HTTP SOAPAction
header be used to exactly identify the operation on which service is being invoked. .NET requires the format of the SOAPAction
header to be the service namespace, followed by a forward slash, followed by the name of the operation, or urn:Example/sayHello
. Notice, though, that SOAP::Lite's default is to use a pound sign (#
) to separate the service namespace from the name of the operation. This wasn't an issue when we were invoking Java services with SOAP::Lite because Apache SOAP simply ignores the SOAPAction
header altogether.
To fix this problem, we must explicitly tell SOAP::Lite how to format the SOAPAction header. To do so, make the change to the client script highlighted in Example 3-20.
Example 3-20: Fragment showing change to Perl client script
print SOAP::Lite
-> uri('urn:Example1')
-> on_action(sub{sprintf '%s/%s', @_ })
-> proxy('http://localhost:8080/helloworld/example1.asmx')
-> sayHello($name)
-> result . "/n/n";
The on_action
method in SOAP::Lite allows the developer to override the default behavior and specify a new format for the SOAPAction
header.
However, even with this change there's still a problem. The script will appear to run, but rather than returning the expected Hello James
string, all that will be returned is Hello
. The name is missing from the response! This happens because .NET requires all parameters for a method call to be named and typed explicitly, whereas Perl does not do this by default.
Again, take a look at the TcpTunnelGui tool and look at the SOAP message sent to the HelloWorld.asmx
service from SOAP::Lite. This is shown in Example 3-21.
Example 3-21: The Perl-generated SOAP request sent to the .NET service
<SOAP-ENV:Envelope
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<namesp1:sayHello xmlns:namesp1="urn:Hello">
<c-gensym3 xsi:type="xsd:string">
James
</c-gensym3>
</namesp1:sayHello>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Notice the oddly named c-gensym3
element that contains the input parameter. Because Perl is a scripting language that does not support strong typing or strict function signatures, method parameters do not have names, nor do they have types. When SOAP::Lite creates the SOAP message it automatically generates an element name and sets all parameters to the string
data type. .NET doesn't like this behavior. If the C# method is written to take a String
parameter called name
it expects to find an element in the SOAP envelope called name
with a type of xsi:type="xsd:string"
. In XML, that would be as shown in Example 3-22.
Example 3-22: A SOAP request encoded by .NET
<SOAP-ENV:Envelope
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<namesp1:sayHello xmlns:namesp1="urn:Hello">
<name xsi:type="xsd:string">
James
</name>
</namesp1:sayHello>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The .NET beta also did not properly recognize that the name
element is declared as part of the same namespace as its parent sayHello
element. This is a standard rule of XML namespaces. To get SOAP::Lite working with .NET, we must tell SOAP::Lite the name, type, and namespace of each of the parameters we are passing into the operation, as shown in Example 3-23.
Example 3-23: Perl client modified to work with .NET
use SOAP::Lite;
my $name = shift;
print "/n/nCalling the SOAP Server to say hello/n/n";
print "The SOAP Server says: ";
print SOAP::Lite
-> uri('urn:Example1')
->on_action(sub{sprintf '%s/%s', @_ })
->proxy('http://localhost:8080/helloworld/example1.asmx')
->sayHello(SOAP::Data->name(name => $name)->type->('string')
->uri('urn:Example1'))
->result . "/n/n";
Now, run the script and you will see that everything works as expected.
Developers who are writing and using web services that may be accessed by a wide variety of SOAP implementations need to be aware that inconsistencies like this will exist between the various toolkits and you need to be prepared to deal with them. As web services become more complex and more mission critical, it is important to have a clear understanding of how to manage these issues. Over time, the more popular SOAP implementations will be honed to a point where they will work together seamlessly, but with many of these implementations still being released as beta and sometimes alpha code status, you must be aware that issues will exist. Luckily, as we will see in Chapter 5, there are workarounds available for some of these problems.
源自:http://oreilly.com/catalog/progwebsoap/chapter/ch03.html