1. 有关生存期的补充 正常情况下,每次调用 WebMethod,服务器都会创建一个新的 WebService 对象,即便客户端使用同一个代理对象多次调用 WebMethod。 而我们一旦调用了有缓存标记的 WebMethod,只要未超出缓存期,WebService 对象都不会被重新创建。在缓存期内调用没有缓存标记的 WebMethod,也会继续使用该 WebService 对象。有太多因素让这个缓存机制变得不那么可靠,因此我们不能奢望用缓存标记来维持特定的对象状态,况且缓存机制的设计初衷也只是为了快速输出那些比较稳定非常大的数据。 基于多用户并发调用这个环境,WebService 本身最好设计成无状态对象,我们可以使用 Session 和 Application 来保持特定的状态信息。 2. 异步调用 网上很多人在写有关 .net 2.0 的文章时,都喜欢用“优雅”这个词。的确,在 2.0 中编译器和代码生成器为我们封装了很多罗嗦的东西,诸如匿名方法、委托推断等等,当然还有这 WebService 的异步调用。我们不用再写那些个 BeginXXX、EndXXX 了,基于事件驱动的异步机制会自动为每个 WebMethod 生成一个 XXXAsync 的异步方法和 XXXCompleted 事件,我们只需调用该方法,并处理该事件即可完成异步操作,当真是优雅了不少。不要小看 2.0 的这些封装,我们编写的代码越少意味着出错的几率越小。 下面的示例中,我们使用了匿名方法来处理事件,看上去更简洁了些。 WebServices.cs
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { [WebMethod] public string HelloWorld() { return "Hello World!"; } }
Client.cs
WebService ws = new WebService(); ws.HelloWorldCompleted = delegate(object sender, HelloWorldCompletedEventArgs e) { Console.WriteLine(e.Result); }; ws.HelloWorldAsync("xxx");
3. 缓存 WebMethodAttribute.CacheDuration 为 WebService 提供了缓存申明机制。通过添加该标记,我们可以缓存输出结果。不过缓存机制会影响 WebService 的生存期(见上)。 WebServices.cs
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { [WebMethod(CacheDuration=10)] public DateTime TestCache() { return DateTime.Now; } }
Client.cs
WebService ws = new WebService(); for (int i = 0; i < 20; i ) { Console.WriteLine("{0}:{1}", i 1, ws.TestCache()); Thread.Sleep(1000); }
4. 保持状态 .NET WebService 是建立在 ASP.NET 基础上,在 WebService 中我们同样可以访问 Session、User、Application 等上下文对象,不过在某些使用细节上可能有所不同。 由于 WebService 客户端代理对象可能应用于 ConsoleApplication、WinForm 或 WebForm 等环境,而 Session 又必须通过 Cookie 来保存唯一的 SessionID,因此我们必须使用 CookieContainer 创建 Cookie 容器来保存 WebService 返回的 Session 信息,否则每次调用的 SessionID 都不同,自然无法使用 Session 来保存状态了。 创建容器对象后,必须将其引用赋值给代理对象的 CookieContainer 属性。在第一次调用 SessionEnabled WebMethod 后,该容器将持有 Session Cookie 信息。如果需要在多个代理对象中调用 SessionEnabled WebMethod,那么它们必须持有同一个 Cookie 容器对象。 WebServices.cs
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { [WebMethod(EnableSession = true)] public string TestSession() { string s = "TestSession"; object o = Session[s]; int i = o != null ? (int)o : 0; i; Session[s] = i; return Session.SessionID.ToString() ":" i; } }
Client.cs
WebService ws = new WebService(); // 创建Cookie容器,保持SessionID。否则每次调用的 SessionID 都不同。 CookieContainer cookies = new CookieContainer(); ws.CookieContainer = cookies; for (int i = 0; i < 10; i ) { Console.WriteLine("{0}:{1}", i 1, ws.TestSession()); }
至于 Application 的使用和 WebForm 中基本没有什么区别。 WebServices.cs
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { [WebMethod] public DateTime TestApplicationState() { object o = Application["TestApplicationState"]; if (o == null) { o = DateTime.Now; Application["TestApplicationState"] = o; } return (DateTime)o; } }
Client.cs
for (int i = 0; i < 10; i ) { WebService ws = new WebService(); Console.WriteLine("{0}:{1}", i 1, ws.TestApplicationState()); Thread.Sleep(1000); }
5. SoapHeader SoapHeader 多数情况下用来传递用户身份验证信息,当然它的作用远不止如此,有待于在实际应用中发掘。 SoapHeader 缺省情况下由客户端代理对象发送给 WebService,当然我们可以通过 WebMethodAttribute.Direction 来改变传送方向。 SoapHeader 使用步骤: (1) 创建继承自 System.Web.WebServices.SoapHeader 的自定义 SoapHeader 类型。 (2) 在 WebService 中创建拥有 public 访问权限的自定义 SoapHeader 字段。 (3) 在需要使用 SoapHeader 的 WebMethod 上添加 SoapHeaderAttribute 访问特性。SoapHeaderAttribute 构造必须指定 memberName 参数,就是我们在第二步中申明的字段名称。 (4) 生成器会自动为客户端生成同名的自定义 SoapHeader 类型,只不过比起我们在 WebService 端创建的要复杂一些。同时还会为代理类型添加一个 soapheaderValue 属性。 在下面的演示代码,客户端将传递一个自定义 MyHeader 到 WebService。请注意,我们尽管在 WebService 中申明了 MyHeader 字段,但并没有创建对象实例,这是因为客户端传递过来的 XML 中包含了 SoapHeader 信息,基础结构会自动解析并创建对象实例,然后赋值给 my 字段。至于客户端,自然需要创建一个 MyHeader 对象实例,并赋值给 WebService.MyHeaderValue 属性。SoapHeaderAttribute.Direction 缺省就是 In,下面例子中的 "Direction = SoapHeaderDirection.In" 可以省略。 WebServices.cs
public class MyHeader : SoapHeader { public string Username; public string Password; } [WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public MyHeader my; [WebMethod] [SoapHeader("my", Direction = SoapHeaderDirection.In)] public void TestSoapHeadIn() { System.Diagnostics.Debug.Write(my.Username); System.Diagnostics.Debug.Write(my.Password); } }
Client.cs
WebService ws = new WebService(); MyHeader head = new MyHeader(); head.Username = "u2"; head.Password = "p2"; ws.MyHeadValue = head; ws.TestSoapHeadIn();
我们改写一下,将传递方向改为从 WebService 到客户端。自然我们需要调整 "Direction = SoapHeaderDirection.Out",在 WebMethod 中我们还必须创建 MyHeader 实例,因为这次我们不会接受到客户端传递的 SoapHeader 了。客户端代理对象调用 WebMethod 后就可以使用 MyHeaderValue 属性访问其内容了。 WebServices.cs
public class MyHeader : SoapHeader { public string Username; public string Password; } [WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public MyHeader my; [WebMethod] [SoapHeader("my", Direction = SoapHeaderDirection.Out)] public void TestSoapHeadOut() { my = new MyHeader(); my.Username = "u1"; my.Password = "p1"; } }
Client.cs
WebService ws = new WebService(); ws.TestSoapHeadOut(); Console.WriteLine(ws.MyHeaderValue.Username); Console.WriteLine(ws.MyHeaderValue.Password);
6. 异常 ASP.NET WebService 通过 Fault XML 元素来传递异常信息,客户端代理对象会生成一个 SoapException 的异常,并使用 Fault XML 信息填充其相关属性,诸如 Message 等。另外我们可以对 WebService 进行异常包装,除了传递 Exception Message 外,还可以传递一些错误状态代码,以便客户端用户做进一步处理。 WebServices.cs
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { [WebMethod] public void TestException() { try { throw new Exception("aaa..."); } catch (Exception e) { throw new SoapException(e.Message, new System.Xml.XmlQualifiedName("ErrorCode01"), e); } } }
Client.cs
WebService ws = new WebService(); try { ws.TestException(); } catch (System.Web.Services.Protocols.SoapException e) { Console.WriteLine(e.Message); Console.WriteLine(e.Code.Name); }
ASP.NET WebService 支持绝大多数的基元类型及其数组,另外还支持自定义的结构(Struct)、类型(Class)、枚举(Enum)、DataSet、XmlElement、XmlNode、集合(IEnumerable/ICollection)等。 ASP.NET WebService 使用 XmlSerializer 进行序列化操作,对于自定义类型要注意以下几点: 1. 只能序列化可读写公共属性和字段。只读属性(get;)、只读字段(readonly)、常量(const)以及所有的非 public 数据成员都不会被序列化。 2. 自定义类型必须具有不接受任何参数的默认构造函数。 3. 不能序列化方法。客户端生成的代理对象不包含任何自定义类型方法(不是WebMethod)。 基于以上几点,因此我们最好只定义纯粹用来传输复合数据的数据类型(Data Object)。 以下是一些演示代码。 基元类型
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public WebService () { } [WebMethod] public DateTime GetNowTime() { return DateTime.Now; } [WebMethod] public string[] GetStringArray() { return new string[] { "a", "b", "c"}; } [WebMethod] public float[] GetFloatArray() { return new float[]{1F, 2F, 3F}; } [WebMethod] public byte[] GetBytes() { return System.IO.File.ReadAllBytes(@"c:windowsnotepad.exe"); } }
枚举 Enum
public enum Sex { Female, Male } [WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public WebService () { } [WebMethod] public Sex GetSex() { return Sex.Female; } [WebMethod] public Sex[] GetAllSex() { return Enum.GetValues(typeof(Sex)) as Sex[]; } }
结构 Struct 结构体默认就会创建无参数构造方法,且不允许自定义。
public struct MyStruct { public int X; public int Y; public MyStruct(int x, int y) { this.X = x; this.Y = y; } } [WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public WebService () { } [WebMethod] public MyStruct GetMyStruct() { MyStruct st = new MyStruct(15, 16); return st; } [WebMethod] public MyStruct[] GetMyStructs() { return new MyStruct[] { new MyStruct(1, 2), new MyStruct(3, 4) }; ; } }
类型 Class
public struct MyStruct { public int X; public int Y; public MyStruct(int x, int y) { this.X = x; this.Y = y; } } public class MyClass { public MyClass() { myStruct = new MyStruct(); } public MyClass(int x, int y, string name) : this() { myStruct.X = x; myStruct.Y = y; this.name = name; } private string name; public string Name { get { return name; } set { name = value; } } private MyStruct myStruct; public MyStruct MyStruct { get { return myStruct; } set { myStruct = value; } } public void Test() // 客户端代理不会生成该方法。 { Console.WriteLine(name); } } [WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public WebService () { } [WebMethod] public MyClass GetMyClass() { return new MyClass(1, 2, "name1"); } [WebMethod] public MyClass[] GetMyClassArray() { return new MyClass[] { new MyClass(1, 2, "name1"), new MyClass(2, 3, "name2") }; ; } }
有关数据类型的更详细信息,请查看 MSDN 文档。 ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_fxwebservices/html/70567d9f-6e53-42a8-bbd5-aee42b25dd28.htm
多数时候我们通过 "添加 Web 引用..." 创建客户端代理类的方式调用WebService,但在某些情况下我们可能需要在程序运行期间动态调用一个未知的服务。在 .NET Framework 的 System.Web.Services.Description 命名空间中有我们需要的东西。 具体步骤: 1. 从目标 URL 下载 WSDL 数据。 2. 使用 ServiceDescription 创建和格式化 WSDL 文档文件。 3. 使用 ServiceDescriptionImporter 创建客户端代理类。 4. 使用 CodeDom 动态创建客户端代理类程序集。 5. 利用反射调用相关 WebService 方法。 OK,看看具体的例子。 我们要调用的目标 WebService,其 URL 是 http://localhost:60436/Learn.WEB/WebService.asmx HelloWorld.asmx
[WebService(Namespace = "http://www.rainsts.net/", Description="我的Web服务")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class WebService : System.Web.Services.WebService { public WebService () { } [WebMethod] public string HelloWorld() { return "Hello Wolrd!"; } }
1. 动态调用 WebService 客户端动态调用代码
using System.IO; using System.Net; using System.Reflection; using System.CodeDom; using System.CodeDom.Compiler; using System.Web.Services; using System.Web.Services.Description; using System.Web.Services.Protocols; using System.Xml.Serialization; // 1. 使用 WebClient 下载 WSDL 信息。 WebClient web = new WebClient(); Stream stream = web.OpenRead("http://localhost:60436/Learn.WEB/WebService.asmx?WSDL"); // 2. 创建和格式化 WSDL 文档。 ServiceDescription description = ServiceDescription.Read(stream); // 3. 创建客户端代理代理类。 ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.ProtocolName = "Soap"; // 指定访问协议。 importer.Style = ServiceDescriptionImportStyle.Client; // 生成客户端代理。 importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties | CodeGenerationOptions.GenerateNewAsync; importer.AddServiceDescription(description, null, null); // 添加 WSDL 文档。 // 4. 使用 CodeDom 编译客户端代理类。 CodeNamespace nmspace = new CodeNamespace(); // 为代理类添加命名空间,缺省为全局空间。 CodeCompileUnit unit = new CodeCompileUnit(); unit.Namespaces.Add(nmspace); ServiceDescriptionImportWarnings warning = importer.Import(nmspace, unit); CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"); CompilerParameters parameter = new CompilerParameters(); parameter.GenerateExecutable = false; parameter.GenerateInMemory = true; parameter.ReferencedAssemblies.Add("System.dll"); parameter.ReferencedAssemblies.Add("System.XML.dll"); parameter.ReferencedAssemblies.Add("System.Web.Services.dll"); parameter.ReferencedAssemblies.Add("System.Data.dll"); CompilerResults result = provider.CompileAssemblyFromDom(parameter, unit); // 5. 使用 Reflection 调用 WebService。 if (!result.Errors.HasErrors) { Assembly asm = result.CompiledAssembly; Type t = asm.GetType("WebService"); // 如果在前面为代理类添加了命名空间,此处需要将命名空间添加到类型前面。 object o = Activator.CreateInstance(t); MethodInfo method = t.GetMethod("HelloWorld"); Console.WriteLine(method.Invoke(o, null)); }
2. 生成客户端代理程序集文件 上面的代码通过在内存中创建动态程序集的方式完成了动态调用过程。如果我们希望将客户端代理类生成程序集文件保存到硬盘,则可以进行如下修改。生成程序集文件后,我们可以通过 Assembly.LoadFrom() 载入并进行反射调用。对于需要多次调用的系统,要比每次生成动态程序集效率高出很多。
using System.IO; using System.Net; using System.CodeDom; using System.CodeDom.Compiler; using System.Web.Services; using System.Web.Services.Description; using System.Web.Services.Protocols; using System.Xml.Serialization; // 1. 使用 WebClient 下载 WSDL 信息。 WebClient web = new WebClient(); Stream stream = web.OpenRead("http://localhost:60436/Learn.WEB/WebService.asmx?WSDL"); // 2. 创建和格式化 WSDL 文档。 ServiceDescription description = ServiceDescription.Read(stream); // 3. 创建客户端代理代理类。 ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.ProtocolName = "Soap"; // 指定访问协议。 importer.Style = ServiceDescriptionImportStyle.Client; // 生成客户端代理。 importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties | CodeGenerationOptions.GenerateNewAsync; importer.AddServiceDescription(description, null, null); // 添加 WSDL 文档。 // 4. 使用 CodeDom 编译客户端代理类。 CodeNamespace nmspace = new CodeNamespace(); // 为代理类添加命名空间,缺省为全局空间。 CodeCompileUnit unit = new CodeCompileUnit(); unit.Namespaces.Add(nmspace); ServiceDescriptionImportWarnings warning = importer.Import(nmspace, unit); CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"); CompilerParameters parameter = new CompilerParameters(); parameter.GenerateExecutable = false; parameter.OutputAssembly = "test.dll"; // 可以指定你所需的任何文件名。 parameter.ReferencedAssemblies.Add("System.dll"); parameter.ReferencedAssemblies.Add("System.XML.dll"); parameter.ReferencedAssemblies.Add("System.Web.Services.dll"); parameter.ReferencedAssemblies.Add("System.Data.dll"); CompilerResults result = provider.CompileAssemblyFromDom(parameter, unit); if (result.Errors.HasErrors) { // 显示编译错误信息 }
调用程序集文件演示
Assembly asm = Assembly.LoadFrom("test.dll"); Type t = asm.GetType("WebService"); object o = Activator.CreateInstance(t); MethodInfo method = t.GetMethod("HelloWorld"); Console.WriteLine(method.Invoke(o, null));
3. 获取客户端代理类源代码 还有一种情形,就是我们需要获得客户端代理类的 C# 源代码。
using System.IO; using System.Net; using System.CodeDom; using System.CodeDom.Compiler; using System.Web.Services; using System.Web.Services.Description; using System.Web.Services.Protocols; using System.Xml.Serialization; // 1. 使用 WebClient 下载 WSDL 信息。 WebClient web = new WebClient(); Stream stream = web.OpenRead("http://localhost:60436/Learn.WEB/WebService.asmx?WSDL"); // 2. 创建和格式化 WSDL 文档。 ServiceDescription description = ServiceDescription.Read(stream); // 3. 创建客户端代理代理类。 ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.ProtocolName = "Soap"; // 指定访问协议。 importer.Style = ServiceDescriptionImportStyle.Client; // 生成客户端代理。 importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties | CodeGenerationOptions.GenerateNewAsync; importer.AddServiceDescription(description, null, null); // 添加 WSDL 文档。 // 4. 使用 CodeDom 编译客户端代理类。 CodeNamespace nmspace = new CodeNamespace(); // 为代理类添加命名空间,缺省为全局空间。 CodeCompileUnit unit = new CodeCompileUnit(); unit.Namespaces.Add(nmspace); ServiceDescriptionImportWarnings warning = importer.Import(nmspace, unit); CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"); // 5. 保存源代码到文件。当然,你也可以直接保存到内存字符串中。 TextWriter writer = File.CreateText("test.cs"); // 指定你所需的源代码文件名。 provider.GenerateCodeFromCompileUnit(unit, writer, null); writer.Flush(); writer.Close();
如果你调用时触发 "WebException: 请求因 HTTP 状态 415 失败: Unsupported Media Type。" 这样的异常,那么恭喜你和我一样郁闷 ,赶紧把服务器端的 WSE 关掉吧。在必须使用 WSE 的情况下,需要对客户端进行调整,至于代码需要你自己去写了。呵呵~~~~