Web Service远程调用技术

Web Service远程调用技术(RPC)的基本概念及实现方式。

前言

Web Service即web服务,是一种跨编程语言和跨操作系统平台的远程调用技术。Web服务包含了一套标准,例如HTTP、XML、SOAP、WSDL、UDDI等,定义了应用程序如何在Web上实现互操作,可以在任何支持这些标准的平台(如Windows、Linux)中使用。

Web Service与Web API相比,更加适合端到端的应用场景(C/S架构),适合作为内部服务使用。

基本概念

SOAP

SOAP即简单对象访问协议(Simple Object Access Protocol),是Web Service的通信协议,基于XML文件并绑定在HTTP协议上传递。SOAP消息包括Envelope、Header和Body元素。

一条SOAP消息就是一个普通的XML文档,文档包括下列元素:

  • Envelope元素,必选,可把此XML文档标识为一条SOAP消息
  • Header元素,可选,包含头部信息
  • Body元素,必选,包含所有的调用和响应信息

WSDL

Web Service描述语言(SebService Definition Language,简称WSDL)就是用机器能阅读的方式提供的一个正式描述文档而基于XML的语言,用于描述Web Service及其函数、参数和返回值。

在WSDL说明书中,描述了

  • 对外发布的服务名称(类)
  • 接口方法名称(方法)
  • 接口参数(方法参数)
  • ​服务返回的数据类型(方法返回值)

UDDI

UDDI(Universal Description,Discovery and Integration),也就是通用的描述、发现以及整合,是一套基于Web的、分布式的、为WebService提供的、信息注册中心的实现标准规范。用户可以通过UDDI来注册和搜索Web服务。

SOA架构

面向服务架构(Service Oriented Architecture,简称SOA),是一个组件模型,它将应用程序的不同功能单元(服务)通过预先定义的接口和契约联系起来。接口是采用中立的方式进行定义的,独立于实现服务的硬件平台、操作系统和编程语言,构建在系统中的服务以一种统一和通用的方式进行交互。

实现方式

注: 以下项目基于.NET Framework,.NET中无法使用

创建服务

使用ASP.NET Web应用程序(.NET Framework)创建一个Web服务(asmx文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Web.Services;

namespace WebApplicationDemo
{
[WebService(Namespace = "http://tempuri.org/")] // 定义命名空间
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] // 绑定规范
public class WebServiceTest : WebService
{
[WebMethod(Description = "测试方法")]
public int Sum(int a, int b)
{
return a + b;
}
}
}

调用服务

静态引用

根据提供的Web Service地址,通过Connected Services添加WCF Web服务引用,生成cs文件,然后直接调用。

以下是在控制台程序以及ASP.NET Web API项目中的调用方式。

1
2
3
4
5
6
7
8
static void Main(string[] args)
{
WebServiceTestSoapClient client = new WebServiceTestSoapClient();

var res = client.SumAsync(1, 2);

Console.WriteLine(res.Result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Route("api/Test")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
public string Get()
{
//创建 HTTP 绑定对象
var binding = new BasicHttpBinding();
//根据 WebService 的 URL 构建终端点对象,参数是提供的WebService地址
var endpoint = new EndpointAddress(@"http://localhost:8083/WebServiceTest.asmx");
//创建调用接口的工厂,注意这里泛型只能传入接口 泛型接口里面的参数是WebService里面定义的类名+Soap
var factory = new ChannelFactory<WebServiceTestSoap>(binding, endpoint);
//从工厂获取具体的调用实例
var callClient = factory.CreateChannel();

var task = callClient.SumAsync(3, 4);
var res = task.Result;

return $"WebService中Sum方法返回结果为{res}";
}
}

反射调用

将Web Service地址存放到配置文件中,通过读取地址生成代理类,动态在项目中生成代理类文件,然后通过反射调用。

1
2
3
4
5
6
static void Main(string[] args)
{
int res = (int)WebServiceProxy.InvokeWebService("https://localhost:44319/WebServiceTest.asmx", "WebServiceTest", "Sum", new object[] { 1, 2 });

Console.WriteLine(res);
}
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
/// <summary>
/// 反射代理类
/// </summary>
public class WebServiceProxy
{
public static object InvokeWebService(string url,string ns, string methodname, object[] args)
{
try
{
//获取WSDL
WebClient wc = new WebClient();
Stream stream = wc.OpenRead(url + "?WSDL");
ServiceDescription sd = ServiceDescription.Read(stream);
string classname = sd.Services[0].Name;
ServiceDescriptionImporter sdi = new ServiceDescriptionImporter();
sdi.AddServiceDescription(sd, "", "");
CodeNamespace cn = new CodeNamespace(ns);

//生成客户端代理类代码
CodeCompileUnit ccu = new CodeCompileUnit();
ccu.Namespaces.Add(cn);
sdi.Import(cn, ccu);
CSharpCodeProvider csc = new CSharpCodeProvider();
ICodeCompiler icc = csc.CreateCompiler();

//设定编译参数
CompilerParameters cplist = new CompilerParameters();
cplist.GenerateExecutable = false;
cplist.GenerateInMemory = true;
cplist.ReferencedAssemblies.Add("System.dll");
cplist.ReferencedAssemblies.Add("System.XML.dll");
cplist.ReferencedAssemblies.Add("System.Web.Services.dll");
cplist.ReferencedAssemblies.Add("System.Data.dll");

//编译代理类
CompilerResults cr = icc.CompileAssemblyFromDom(cplist, ccu);
if (true == cr.Errors.HasErrors)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (System.CodeDom.Compiler.CompilerError ce in cr.Errors)
{
sb.Append(ce.ToString());
sb.Append(System.Environment.NewLine);
}
throw new Exception(sb.ToString());
}

//生成代理实例,并调用方法
System.Reflection.Assembly assembly = cr.CompiledAssembly;
Type t = assembly.GetType(ns + "." + classname, true, true);
object obj = Activator.CreateInstance(t);
System.Reflection.MethodInfo mi = t.GetMethod(methodname);

return mi.Invoke(obj, args);
}
catch
{
return null;
}
}
}

使用HTTP方式调用

Web Service还可以使用HTTP方式,通过发送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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static  void Main(string[] args)
{
using (HttpClient client = new HttpClient())
{
var webServiceUri = "http://localhost:8083/WebServiceTest.asmx";
string soapRequest = @"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
<soap:Body>
<Sum xmlns=""http://tempuri.org/"">
<a>1</a>
<b>2</b>
</Sum>
</soap:Body>
</soap:Envelope>";


// var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:8083/WebServiceTest.asmx");
// request.Headers.Add("SOAPAction", "\"http://tempuri.org/Sum\"");
// request.Content = new StringContent(soapRequest, Encoding.UTF8, "text/xml");
// HttpResponseMessage response = client.SendAsync(request).Result;

var content = new StringContent(soapRequest, Encoding.UTF8, "text/xml");
var response = client.PostAsync(webServiceUri, content).Result;

string responseContent;

if (response.IsSuccessStatusCode)
{
responseContent = response.Content.ReadAsStringAsync().Result;
// 解析XML响应
XDocument xmlResponse = XDocument.Parse(responseContent);
// 处理XML
Console.WriteLine($"{xmlResponse}");
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}

Console.ReadKey();
}
}

请求返回的soap消息如下:

1
2
3
4
5
6
7
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<SumResponse xmlns="http://tempuri.org/">
<SumResult>3</SumResult>
</SumResponse>
</soap:Body>
</soap:Envelope>

soap消息解析

假设返回的soap消息包含xml数据如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<getMqResponse xmlns="http://tempuri.org/">
<getMqResult>
<maindata table="person" disid="1" xmlns="">
<item opeType="insert">
<idno>1001</idno>
</item>
</maindata>
</getMqResult>
</getMqResponse>
</soap:Body>
</soap:Envelope>

如果想要解析带命名空间的xml并获取其中某些节点的属性值和内容,则需要使用命名空间管理器。

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
// 加载SOAP响应的XML数据
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(soapResponse);

// 创建命名空间管理器,并添加SOAP命名空间
XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
namespaceManager.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
namespaceManager.AddNamespace("ns", "http://tempuri.org/");

// 使用XPath表达式提取所需的数据
XmlNode mainDataNode = xmlDoc.SelectSingleNode("/soap:Envelope/soap:Body/ns:getMqResponse/ns:getMqResult/maindata", namespaceManager);

// 获取maindata中disid的属性值
XmlElement mainDataElement = (XmlElement)mainDataNode;
var disid = mainDataElement.GetAttribute("disid");

var childNodes = mainDataNode.SelectNodes("item");
foreach(var itemNode in childNodes)
{
XmlElement itemElement = (XmlElement)itemNode;
// 获取item中opeType的属性值
var opeType = itemElement.GetAttribute("opeType");
// 获取idno的文本值
var idno = itemElement.SelectSingleNode("idno").InnerText;
}

xml字符串转义问题

在WebService方法返回XML数据的时候,将XML处理成字符串返回,在客户端得到的XML字符串会出现被转义的情况。

将已经为HTTP传输进行过HTML编码的字符串转换为已解码的字符串,可在不改动服务端代码的情况下解决转义问题。

1
result = System.Net.WebUtility.HtmlDecode(xmlResponse.ToString());

string类型和XmlDocument类型在WebService序列化过程中的处理方法不同。如果返回可序列化的标准XML对象,可从根本上解决转义问题。

对应的服务端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[WebMethod(Description = "测试方法")]
public System.Xml.XmlDocument GetMainData()
{
// your xml response
var res = @"<maindata>
<item></item>
</maindata>";

System.Xml.XmlDocument xmldoc = new System.Xml.XmlDocument();
xmldoc.LoadXml(res);

return xmldoc;
}

参考文档