虽然 WebService
已经很 Low
了,但是胜在简单。所以很多小公司或者公司内部仍然会使用这个做一些接口。
这里总结一下 WebService
的一些使用技巧,以及经验总结。
创建服务端 WebService
宿主是 IIS
,所以我们需要先创建一个 ASP.Net Web
的空项目,当然如果选择 MVC
或 WebForm
也没有影响。
创建以后我们就可以添加对应的服务文件,如下图:
会生成一个 *.asmx
文件与 一个 *.asmx.cs
文件,结构与 WebForm
的窗体页面或一般处理程序等一致。
如果我们没有将后台代码 cs
文件另外单独存放的需求,那么就不需要调整,直接修改展开对 cs
文件进行修改即可。
这里我们添加一些方法用于测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Services;namespace JohnSun.SOA.WebService.Server { [WebService(Namespace = "http://tempuri.org/" ) ] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1) ] [System.ComponentModel.ToolboxItem(false) ] public class MyWebService : System.Web.Services.WebService { private static List <UserInfo > _users = new List<UserInfo>() { new UserInfo(){ Id = 1 , Name = "Kangkang" , Country = "China" }, new UserInfo(){ Id = 2 , Name = "John" , Country = "America" }, new UserInfo(){ Id = 3 , Name = "Jane" , Country = "France" }, new UserInfo(){ Id = 4 , Name = "Han Meimei" , Country = "China" }, }; [WebMethod ] public string HelloWorld ( ) { return "Hello World" ; } [WebMethod ] public decimal Sum (decimal x, decimal y ) { return x + y; } [WebMethod ] public UserInfo GetUserInfo (int id ) { return _users.Find(u => u.Id == id); } [WebMethod ] public List<UserInfo> GetUsers (string country ) { return _users.FindAll(u => u.Country == country); } [WebMethod ] public UserInfo[] GetAllUsers ( ) { return _users.ToArray(); } } public class UserInfo { public int Id { get ; set ; } public string Name { get ; set ; } public string Country { get ; set ; } } }
需要注意的是:
如果我们需要调用一个方法,则需要将方法标记为 WebMethod
特性,才能调用。
这里不遵循重载,所以方法名不能重复,即便入参不同也不行。
入参和返回值都可以是数组或集合,但是调用服务时,可以配置,这里再调用时会说明。
完成后,我们就可以右键该文件,在浏览器打开访问该服务。
另外,我们也可以在浏览器里直接调用服务。
连接服务端 服务端托管完成以后,我们就可以使用客户端完成对服务端的调用了,客户端的项目没有什么限制,但是建议是使用 .NET Framework 4.0
或以上版本,否则添加服务的界面可能会与截图演示的有些许区别。
这里为了方便演示,直接添加一个 WinForm
的项目,然后可以在引用中,选择添加服务引用:
在弹出的添加页面,录入我们的服务地址,点击发现,添加我们需要的服务,另外我们需要调整这个服务的命名控件,需要注意的是不要与其他命名控件或类型重名,否则会比较麻烦:
服务创建后,在窗体中简单写一些调用服务的代码进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private void button1_Click (object sender, EventArgs e ) { MyWebServiceSoapClient client = null ; try { client = new MyWebServiceSoapClient(); client.Open(); string h = client.HelloWorld(); Log(LogLevel.Info, $"调用 HelloWorld 方法成功:{h} " ); decimal x = 1 m; decimal y = 2 m; decimal d = client.Sum(x, y); Log(LogLevel.Info, $"调用 Sum 方法成功:Sum({x} , {y} ) = {d} " ); int id = 1 ; UserInfo info = client.GetUserInfo(id); JavaScriptSerializer serializer = new JavaScriptSerializer(); Log(LogLevel.Info, $"调用 GetUserInfo 方法成功:GetUserInfo({id} ) = {serializer.Serialize(info)} " ); string country = "China" ; UserInfo[] users = client.GetUsers(country); Log(LogLevel.Info, $"调用 GetUsers 方法成功:GetUsers({country} ) 获取到用户数量: {users.Length} " ); UserInfo[] allUsers = client.GetAllUsers(); Log(LogLevel.Info, $"调用 GetAllUsers 方法成功,获取到用户数量: {allUsers.Length} " ); client.Close(); } catch (Exception exc) { if (client != null ) client.Abort(); Log(LogLevel.Error, "测试服务失败:" + exc.Message); } }
测试一下调用:
这里主要需要注意两点:
MyWebServiceSoapClient
并未继承 IDisposable
接口,所以不能使用 using
来关闭连接,使用上文的写法即可,否则资源释放存在问题。
无论返回值是集合还是数组,我们只能使用一种类型,服务创建以后我们也可以指定,如上虽然我们 GetUsers
在服务中定义返回集合,但是调用时返回的仍然是数组。
如果我们想让服务默认返回的数据类型调整为集合,可以在引用的服务上右键,选择“配置服务引用”,将集合类型调整为 System.Collections.Generic.List
,同样的字典类型也可以调整。
添加身份认证 以上服务方法都没有进行身份认证,如果一些私有的方法,就会有安全问题,我们也可以在服务中配置简单身份认证。
比较常用的是使用 SoapHeader
为服务方法,添加 SOAP 标头
,这样我们在调用服务时就需要传入一个标头信息,我们可以使用这个信息来传递用于用户验证的信息。
首先需要调整 WebService
服务端的代码,定义一个继承自 SoapHeader
的类型 AuthenticationHeader
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AuthenticationHeader : SoapHeader { public string UserName { get ; set ; } public string Password { get ; set ; } public string Token { get ; set ; } public void Validate ( ) { if (UserName == "admin" && Password == "admin" ) { MD5 md5 = MD5.Create(); Token = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(Password))); } else { throw new SoapException("Failed to verify user login information." , SoapException.ServerFaultCode); } } }
修改服务后台类,增加一个用于接收 SoapHeader
的字段,为需要附加标头的方法标记 SoapHeader
特性,并指定 SOAP 标头
的数据赋值给服务后台类的哪个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public AuthenticationHeader authenticationHeader;[WebMethod ] [SoapHeader("authenticationHeader" ) ] public string Validate ( ) { if (authenticationHeader != null ) { authenticationHeader.Validate(); return authenticationHeader.Token; } else return null ; } [WebMethod ] [SoapHeader("authenticationHeader" , Direction = SoapHeaderDirection.InOut) ] public UserInfo[] GetAllUsers ( ) { if (authenticationHeader != null ) authenticationHeader.Validate(); else return null ; return _users.ToArray(); }
修改完成以后需要重新编译这个服务,并且在客户端更新服务引用,更新完成以后 GetAllUsers
方法应该会报错,因为需要我们传递一个 AuthenticationHeader
,可以将客户端调用服务方法的代码略作调整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 AuthenticationHeader header = new AuthenticationHeader() { UserName = "admin" , Password = "admin" }; UserInfo[] allUsers = null ; try { allUsers = client.GetAllUsers(ref header); Log(LogLevel.Info, $"调用 GetAllUsers 方法成功,获取到用户数量: {allUsers?.Length} ,Token:{header.Token} " ); } catch (Exception exc) { Log(LogLevel.Error, $"调用 GetAllUsers 方法失败:{exc.Message} " ); } try { header = new AuthenticationHeader() { UserName = "test" , Password = "test" }; string token = client.Validate(header); if (token != null ) { Log(LogLevel.Info, $"调用 Validate 方法成功:{token} " ); } else { Log(LogLevel.Error, $"调用 Validate 方法失败,请验证用户名和密码。" ); } } catch (Exception exc) { Log(LogLevel.Error, $"调用 Validate 方法失败:{exc.Message} " ); }
修改以后可以执行测试,运行客户端查看一下效果:
当然除以上方法外,我们还可以提供一个登录的服务方法,如果验证成功返回一个 token
,然后私有的方法增加一个 token
的入参,每次调用方法前进行验证,因为比较简单这里不再过多演示。
动态调整服务 目前来说服务是固定指向了一个我们生成服务时的地址,当然如果我们想要修改服务地址也是很简单的,只需要打开 app.config
文件,修改默认生成的配置信息:
这样我们要严格的依赖这个配置文件,而且如果我们想要配置多个服务地址,在一个地址无法连接时自动切换到其他服务,好像是不可实现的,所以第一步就是移除掉对配置文件的依赖,我们删除 app.config
文件,重新生成项目并运行:
1 17:19:31 Error: 测试服务失败:在 ServiceModel 客户端配置部分中,找不到引用协定“MyServiceTest.MyWebServiceSoap”的默认终结点元素。这可能是因为未找到应用程序的配置文件,或者是因为客户端元素中找不到与此协定匹配的终结点元素。
这个其实很简单,因为没有了配置文件读取不到服务的链接地址,我们可以在初始化客户端的时候,指定服务端连接:
1 client = new MyWebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress("http://localhost:15178/MyWebService.asmx" ));
因为已经不再依赖配置文件,这样我们初始化客户端时就可以更自由,我们可以创建一个工厂来初始化服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 using JohnSun.SOA.WebService.Client.MyServiceTest;using System;using System.Collections.Generic;using System.Linq;using System.ServiceModel;using System.Text;namespace JohnSun.SOA.WebService.Client { public class SoapClientFactory { public static bool TryGetSoapClient (out MyWebServiceSoapClient soapClient, params string [] urls ) { soapClient = null ; if (urls == null || urls.Length == 0 ) return false ; foreach (string url in urls) { try { soapClient = new MyWebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress(url)); soapClient.HelloWorld(); break ; } catch { if (soapClient != null ) soapClient.Abort(); soapClient = null ; } } return soapClient != null ; } } }
然后初始化服务可以调整为以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 if (!SoapClientFactory.TryGetSoapClient(out client , "http://localhost:80/MyWebService.asmx" , "http://localhost:1008/MyWebService.asmx" , "http://localhost:15178/MyWebService.asmx" )) { Log(LogLevel.Error, "初始化服务失败!" ); return ; } else { Log(LogLevel.Info, $"初始化服务成功:{client.Endpoint.ListenUri} " ); }
测试效果:
我们已经解决了必须通过 app.config
来配置服务地址的问题,但是,如果我们只有 wsdl
文件,无法连接服务添加服务应该怎么处理呢?
首先我们下载服务的 wsdl
文件用于演示,在服务后面添加 wsdl
参数即可下载:http://localhost:15178/MyWebService.asmx?wsdl
然后和通过链接添加服务一样,不过我们输入的是下载下来的 wsdl
文件的文件路径:
其实如果我们做出来的服务要提供给第三方调用,也是通过这种方式,将 wsdl
文件下载下来,发送给第三方即可。
参考:
源码下载: