Service-Oriented Architecture,面向服务架构,粗粒度、开放式、松耦合的服务结构,将应用程序的不一样功能单元(称为服务)经过这些服务之间定义良好的接口和契约联系起来。接口采用中立的方式定义,独立于实现服务的硬件平台、操做系统和编程语言(跨平台)。html
SOA既是一种编程方式,也是软件开发的一种架构方法,服务层是SOA的基础,核心是“服务”,本质就是将服务组合起来并对外提供接口。SOA架构的技术基础是SOAP(Simple Object Access Protocol,简易对象访问协议)标准,SOAP用XML语言定义一个服务操做方法所发送和接收消息的内容。java
实现SOA思想的技术web
有关 web service 的学习,参见:Web Service - sqh;编程
关于 Web Service 和 WCF,异同点做简单的说明json
关于WCF与Web Service的区别参见:http://www.cnblogs.com/xiurui12345/archive/2012/03/30/2425445.html。下面主要研究 WCF。
windows
Windows Communication Foundation(WCF)是由微软开发的一系列支持弹性数据通讯的应用程序框架,Windows通信开发平台。支持和集成Web Service,兼容和具有微软早期技术的特性,整合了原有windows通信的 .Net Remoting、WebService、Socket机制,融合有HTTP和FTP相关技术,是创建在Web Service架构上的一个全新通讯平台。api
WCF能够理解为Web Service的升级版。浏览器
微软官方解释:WCF是彻底使用托管代码创建和运行面向服务应用程序的统一框架,使开发者能创建一个跨平台的安全、可信赖、事务性的解决方案,且能与已有系统兼容协做安全
WCF是一套框架,用来建立各类服务,且可以建立兼容Web服务的服务,也就是说能够建立可以与Web服务互联互通的服务。WCF最基本的通讯机制是SOAP,保证系统之间的互操做性。WCF技术容许建立服务,能够跨进程、计算机和网络从其余应用程序访问这些服务。利用这些服务,能够在多个应用程序中共享功能,提供数据源或抽象复杂过程。
优势
其他相关信息可参考:WCF .vs. Web Service
WCF作啥
WCF 是面向服务的,跨平台的安全、可信赖、事务性的解决方案,做为 WebService,.Net Remoting,Enterprise Service,WSE,MSMQ 的并集,以面向服务为思想提供了包含通信、事务、并发、队列、安全等一系列的整套的分布式开发方案。
WCF 是 .NET 提供的一种服务,能够将写的完成特定功能的程序(如从数据库中读取数据操做等)封装成服务,而后发布到服务器上会生成一个网址,客户端编程时能够引用这个服务,使用服务中提供的功能。关于 WCF 的其余信息参考:http://blog.csdn.net/fynjy/article/details/46874597
WCF架构
WCF Service经过 终结点(Endpoint) 发布服务来实现网络系统各个应用程序间的通讯,一个WCF Service由一个Endpoint集合组成。Endpoint是WCF实现通讯的核心要素,是服务器间通讯调用的入口,客户端和服务端经过Endpoint交换信息。
当咱们寄宿 WCF 服务时,必须定义一个或多个终结点,而后 Serivce 端经过监听这些终结点来处理 Client 发来的请求。因为应用程序之间靠 Endpoint 通讯,那么 Client 端也必须定义终结点,只有 Client 与 Service 的终结点彻底匹配时才能通讯。
namespace System.ServiceModel.Desc { // 表示容许服务的客户端查找并与服务通讯的服务的终结点 public class ServiceEndpoint { public string Name { get; set; } // 服务终结点的名称 public EndpointAddress Address { get; set; } // 服务终结点的终结点地址 public ContractDescription Contract { get; set; } // 服务终结点的协定 public Binding Binding { get; set; } // 服务终结点的绑定 } }
Endpoint由三个主要部分组成:
A:地址 - Address,服务的位置,Where to locate the WCF Service?:惟必定位服务,提供额外的寻址信息和认证信息
右键 SqhService.svc ,选择在浏览器中查看,获取该服务的惟一地址标识URI(WCF Service Address):http://localhost:18583/SqhService.svc
其中,URI(Uniform Resource Identifier)统一资源标识,格式:[Schema传输协议]://[主机名|域名|IP地址]:[端口号]/[资源路经]
namespace System.ServiceModel { // 提供客户端用来与服务终结点进行通讯的惟一网络地址。 public class EndpointAddress { public Uri Uri { get; } // 终结点的 URI,服务的惟一标识 public EndpointIdentity Identity { get; } // 用于验证终结点的标识 public AddressHeaderCollection Headers { get; } // 获取生成器能够建立的终结点的地址标头的集合 } }
对于终结点地址,是区别于服务地址的:终结点地址是服务地址的子地址,或相对地址;服务地址是终结点地址的父地址,或基地址,示例
服务地址:江陵路xx小区2号楼,楼房地址,只有一个,惟一标识服务
终结点地址:江陵路xx小区2号楼601室,门牌号地址,一个服务能够包含多个终结点
下面给出网上的一段示例代码,有助于理解服务地址和终结点地址的关系
// 服务地址 Uri baseAddress = new Uri("http://localhost:8000/MyService"); // 服务宿主 using(ServiceHost host = new ServiceHost(typeof(HelloWCFService), baseAddress)) { // 添加终结点(地址、绑定、服务协定) host.AddServiceEndpoint(typeof(IHelloWCFService), new WSHttpBinding(), "HelloWCFService"); // 元数据交换结点,用于元数据交换,并指定启用元数据交换行为 ServiceMetadataBehavior smb = new ServiceMetadataBehavior(); smb.HttpGetEnabled = true; host.Description.Behaviors.Add(smb); // 启动服务 host.Open(); // doSomething(...) host.Close(); }
其实,以上信息都可以在配置文件中实现,当配置发生改变时能够不用从新编译程序集。
B:绑定 - Binding,服务的通讯方式,How to communicate with service?,实现Client和Service通讯的全部底层细节,解决WCF服务的通讯问题;
在这给出支持双工通讯的两种协议:
C:契约 - Contract,服务的内容,What functionalities do the Service provide?,见下;
除此以外,还有一个 Behavior,用于定制Endpoint在运行时的一些必要的动做等。
WCF架构体系是基于拦截机制的,一个完整的 WCF 解决方案包括以下四个部分:
Metadata(元数据)
元数据交换(Metadata Exchange):服务端要提供服务的接口描述(或类型描述)、操做的方法签名,相关的数据描述等给客户端
配置文件
这里给出配置文件的完整结构代码,其中:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.serviceModel> <!--配置服务和终结点--> <services> <service name="服务命名空间.服务协定" behaviorConfiguration="myBehavior"> // 宿主服务地址 <host> <baseAddresses> <add baseAddress="wcfURI"/> </baseAddresses> </host> // 服务终结点 <endpoint address="xxx" binding="wsHttpBinding" contract="服务命名空间.服务协定接口" bindingConfiguration="myBinding"></endpoint> // 元数据交换结点 <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <!--配置绑定--> <bindings> <netTcpBinding> <binding name="myBinding"></binding> </netTcpBinding> </bindings> <!--配置行为--> <behaviors> <serviceBehaviors> <behavior name="myBehavior"></behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration>
下面给出一个简单的 WCF 程序的资源结构图
注意,其中的 .svc 文件就是服务托管文件。
3.1 契约
WCF的基本概念是以 契约(Contract) 来定义双方通讯的协议,合约必需要以接口的方式来体现,而实际的服务代码必需要由这些合约接口派生并实现。契约与平台无关、以XML格式发布,是消息参与者之间的约定,提供服务通讯所必需的元数据。首先,了解下代码中的几种契约属性的形式
// 数据协定 // 指定该类型要定义或实现一个数据协定,并可由序列化程序(如 System.Runtime.Serialization.DataContractSerializer)进行序列化 // 若要使其类型可序列化,类型做者必须为其类型定义数据协定 [DataContract] // 数据成员协定 // 当应用于类型的成员时,指定该成员是数据协定的一部分并可由 System.Runtime.Serialization.DataContractSerializer 进行序列化 [DataMember] // 服务协定 // 指示接口或类在 应用程序中定义服务协定 [ServiceContract] // 操做协定 // 指示方法定义一个操做,该操做是 应用程序中服务协定的一部分 [OperationContract]
下面经过代码示例,具体研究各个契约的用法
using System.ServiceModel; using System.Runtime.Serialization; using SqhWcfFunction; namespace SqhWcfService { [ServiceContract] // 服务契约 public interface ISqhService{ [OperationContract] // 操做契约 QueryOut QueryPersonInfoService(QueryIn inParam); } }
注:能够在特性中使用Namespace和Name属性,实现对命名空间和协定方法名称的加密。具体信息在Service References:SqhService下的SqhService.wsdl文件中查看。其中,WSDL(网络服务描述语言,Web Services Description Language)是基于XML的语言,用于描述Web Services、服务元数据以及如何对它们进行访问。=> WSDL 教程 | W3School
下面的服务协定接口的实现
namespace SqhWcfService { public class SqhService : ISqhService { public QueryOut QueryPersonInfoService(QueryIn inParam) { QueryOut outParam = new QueryOut(); try { outParam = SqhServiceFunction.QueryPersonInfo(inParam); } catch (Exception e) { outParam.ErrMsg = "查询person信息时发生异常," + e.Message; outParam.isSuccessed = false; return outParam; } return outParam; } } }
其中,SqhWcfFunction.SqhServiceFunction 是专门定义的数据处理函数类。
using System.ServiceModel; using System.Runtime.Serialization; namespace SqhWcfFunction { [DataContract] // 数据契约 public class QueryIn{ [DataMember] // 数据成员契约,标识类型可序列化 public string personID; [DataMember] public string personName; } [DataContract] public class QueryOut{ [DataMember] public string personResInfo; [DataMember] public bool isSuccessed; } }
关于数据协定的详细使用,可参见:WCF - 数据协定;
更好地控制SOAP头和SOAP体,支持序列化期间的安全机制。全部的消息契约必须实现一个public的无参构造函数。
给出一个 WCF 的简单 Demo,参见:http://www.cnblogs.com/iamlilinfeng/p/4083827.html
3.2 宿主程序
WCF自己不可以独自运行,必须寄宿在一个宿主进程(Host Process)中。服务寄宿的目的就是开启一个进程,为WCF服务提供一个运行的环境。单个宿主进程能够托管多个服务,相同的服务也能够托管在多个宿主进程中。
关于经过控制台承载寄宿服务,相关资料可参见:http://blog.csdn.net/songyefei/article/details/7363296
关于在 IIS 中寄宿 WCF 服务
在 IIS 中宿主服务的主要优势:发生客户端请求时宿主进程会被自动启动,而且能够依靠 IIS 管理宿主进程的生命周期
相关资料可参见:
首先,一个服务要部署到 IIS 上,必需要生成待部署的文件包,具体操做:
[1]. 右键项目,生成部署包 [2]. ...\obj\Debug\Package\PackageTmp\ 目录下的文件就是待部署的文件 [3]. PackageTmp 下的文件所有拷贝到 IIS 下的相关文件夹中便可
下面对 IIS 寄宿 WCF 服务中遇到的几个问题进行总结:
问题1: “/SQHWCFSERVICE”应用程序中的服务器错误。 配置错误 说明: 在处理向该请求提供服务所需的配置文件时出错。请检查下面的特定错误详细信息并适当地修改配置文件。 分析器错误消息: 没法识别的属性“targetFramework”。请注意属性名称区分大小写。 源错误: <compilation debug="true" targetFramework="4.0"/> 版本信息: Microsoft .NET Framework 版本:2.0.50727.4961; ASP.NET 版本:2.0.50727.4955
缘由:.NET Framework 版本不匹配,IIS 配置 和 程序配置的版本不一致
解决方法:修改.NET Framework 版本为相应版本,将 2.0 改成 4.0 版本
具体参见:http://blog.csdn.net/muchlin/article/details/6800863
问题2: 应用程序“NET/CRM”中的服务器错误 Internet Information Services 7.5 错误摘要:HTTP 错误 404.2 - Not Found 因为 Web 服务器上的“ISAPI 和 CGI 限制”列表设置,没法提供您请求的页面。
缘由:ISAPI和CGI限制没有容许须要的版本
解决方法:具体可参见上面的连接。
注意,若是ISAPI和CGI限制列表中不包含须要的版本,须要手动添加 C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll
问题3: 经过IIS直接浏览页面时报错:HTTP 错误 404.3 - Not Found 因为扩展配置问题而没法提供您请求的页面。 若是该页面是脚本,请添加处理程序。若是应下载文件,请添加 MIME 映射。
解决方法:为 IIS 从新注册 .net framework 4.0,具体参见:http://blog.csdn.net/g200407331/article/details/9078219
注意,要以管理员权限运行Visual Studio 命令提示工具,该工具在电脑左下角的开始中能够找到。
为了保险起见,在 IIS 根目录下,双击 MIME 类型,添加以下信息
文件扩展名:.json MIME 类型:application/json
3.3 服务
Service:服务定义,注重逻辑
using System.ServiceModel; using System.Runtime.Serialization; using SqhWcfFunction; namespace SqhWcfService{ public class SqhService : ISqhService { public QueryOut QueryPersonInfoService(QueryIn inParam) { QueryOut outParam = new QueryOut(); try{ outParam = SqhServiceFunction.QueryPersonInfo(inParam); } catch (Exception e){ outParam.ErrMsg = "查询person信息时发生异常," + e.Message; outParam.isSuccessed = false; return outParam; } return outParam; } } }
Function:服务函数,具体实现
namespace SqhWcfFunction{ public class SqhServiceFunction { public static QueryOut QueryPersonInfo(QueryIn inParam) { QueryOut outParam = new QueryOut(); try{ // 动态SQL、访问数据库等其余各类操做 outParam.isSuccessed = true; outParam.personResInfo = string.Format("My WCF Service:{0}-{1}", inParam.personID, inParam.personName); } catch (Exception e){ outParam.ErrMsg = "查询失败" + e.Message; outParam.isSuccessed = false; return outParam; } return outParam; } } }
3.4 客户端
客户端应用程序经过代理类与WCF服务进行通讯,代理类为WCF服务实现了服务契约接口,对这个接口的操做方法的全部调用都重定向到WCF服务上。
using System.ServiceModel; using System.Runtime.Serialization; using SqhWcfClient.SqhService; namespace SqhWcfClient { public class Program{ static void Main(string[] args){ SqhServiceClient SqhClient = new SqhServiceClient(); // 客户端 QueryIn inParam = new QueryIn(){ personID = "001", personName = "sqh" }; QueryOut outParam = SqhClient.QueryPersonInfoService(inParam); if(outParam.isSuccessed) Console.WriteLine(outParam.personResInfo); SqhClient.Close(); // 关闭客户端 } } }
关于双工消息通讯模式(异步)的实现
WCF 支持多种消息格式:单向、请求/回复、双工,但必须绑定对应的协议。
三种方式的基本流程参见:WCF 通讯模式;
该部分经过一个简单的加法方法来了解 WCF 的双工通讯方式:
[1]. 服务端
首先给出服务协定接口的代码,注意服务接口和服务回调接口的命名方式
using System.ServiceModel; namespace sqhWcfService { [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IServiceAsyncCallback))] public interface IServiceAsync { [OperationContract(IsOneWay = true)] void AddTwoInteger(int a, int b); } [ServiceContract] public interface IServiceAsyncCallback { [OperationContract(IsOneWay = true)] void ReturnResult(int c); } }
在服务端,须要实现服务协定接口的方法,服务协定回调接口的方法在客户端实现。服务协定接口必须指明 SessionMode.Required 和 回调协定属性。注意接口命名格式,推荐: 服务协定回调接口 = 服务协定接口 + Callback
using System.ServiceModel; namespace sqhWcfService { public class ServiceAsync : IServiceAsync { public void AddTwoInteger(int a, int b) { int result = a + b; Console.WriteLine("AddTwoInteger:{0}", result); IServiceAsyncCallback callback = OperationContext.Current.GetCallbackChannel<IServiceAsyncCallback>(); callback.ReturnResult(result); } } }
注意,在 Web.config 文件 <system.serviceModel> 结点下添加以下结点
<services> <service name="sqhWcfService.ServiceAsync"> <endpoint address="ServiceAsync" binding="wsDualHttpBinding" contract="sqhWcfService.IServiceAsync" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services>
注意,双工通讯必须是 wsDualHttpBinding 绑定,该属性会创建两条绑定实现互相调用,也能够是 NetTcpBinding。
至此,服务端配置完毕,记录下服务的地址备用。
[2]. 客户端
首先添加服务引用,命名为:sqhWcfServiceReference,而后实现服务回调接口的方法
using sqhWcfClient.sqhWcfServiceReference; namespace sqhWcfClient { public class ServicAsyncCallback : IServiceAsyncCallback { public void ReturnResult(int c) { int result = c; Console.WriteLine("ReturnResult:{0}", result); } } }
下面是实例化客户端、调用服务的代码
using sqhWcfClient.sqhWcfServiceReference; namespace sqhWcfClient { public class Program { public static void Main(string[] args) { // 服务回调对象 ServicAsyncCallback callbackObj = new ServicAsyncCallback(); // 服务回调上下文,维护一个服务回调对象 InstanceContext callbackInstance = new InstanceContext(callbackObj); // 利用上下文实例对象初始化代理客户端对象 ServiceAsyncClient client = new ServiceAsyncClient(callbackInstance); client.AddTwoInteger(1, 2); } } }
至此,客户端代码配置完毕。运行客户端,便可调用服务方法完成加法实现。为了查看异步调用的效果,能够再客户端代码和服务端代码中添加延时。
详细实现信息参见:消息通讯模式(下)- 双工;在 WCF 中实现双工通讯深刻理解 - Artech;
关于利用 WCF服务库 提供服务
上面介绍的 WCF 服务均是经过 VS新建项目 -> WCF服务应用程序 的方式提供服务,此处介绍经过 VS新建项目 -> WCF服务库 的方式提供服务并部署到 IIS 上。首先了解下 WCF服务应用程序 和 WCF服务库 的区别:
其余区别的详细信息能够参见:WCF Service Application -VS- WCF Service Library;WCF服务应用程序 - WCF服务库;
在项目工做中,推荐使用 WCF服务库,某个服务的类定义为一个单独能够编译的类库,低耦合,能够为其余项目使用、提升代码的复用性。
下面的 WCF服务,包含2个 WCF 服务库(具体的独立的服务,CalculateService 和 CertificateService)和1个 ASP.NET 空 Web 应用程序(包含2个WCF服务类库,对外提供服务)
[1]. 建立WCF服务库
首先,分别建立2个独立的WCF服务库:CalculateService(计算服务) 和 CertificateService(注册服务),下面直接给出代码:
(1)CalculateService
1 namespace RVC.WCF.Calculate 2 { 3 [ServiceContract] 4 public interface ICalculateService 5 { 6 [OperationContract] 7 double Add(double a, double b); 8 [OperationContract] 9 double Minus(double a, double b); 10 [OperationContract] 11 double Multiple(double a, double b); 12 [OperationContract] 13 double Divide(double a, double b); 14 } 15 }
1 namespace RVC.WCF.Calculate 2 { 3 public class CalculateService : ICalculateService 4 { 5 public double Add(double a, double b) { 6 return a + b; 7 } 8 9 public double Minus(double a, double b) { 10 return a - b; 11 } 12 13 public double Multiple(double a, double b) { 14 return a * b; 15 } 16 17 public double Divide(double a, double b) { 18 if ((int)b == 0){ 19 return double.MaxValue; 20 } 21 return a * 1.0 / b; 22 } 23 } 24 }
(2)CertificateService
1 namespace RVC.WCF.Certificate 2 { 3 [ServiceContract] 4 public interface ICertificateService 5 { 6 [OperationContract] 7 string CertificateUser(string userName, string passWord); 8 [OperationContract] 9 UserInfo GetUserInfo(string userID); 10 [OperationContract] 11 bool CancelUser(string userID); 12 } 13 }
namespace RVC.WCF.Certificate { public class CertificateService : ICertificateService { private static Dictionary<string, UserInfo> UserDic = new Dictionary<string, UserInfo>(); public string CertificateUser(string userName, string passWord) { string userID = DateTime.Now.ToShortTimeString() + passWord; UserInfo user = new UserInfo() { UserID = userID, UserName = userName, Password = passWord, CertTime = DateTime.Now }; if (UserDic.ContainsKey(user.UserID)) { throw new Exception("该用户已存在"); } UserDic.Add(user.UserID, user); return userID; } public UserInfo GetUserInfo(string userID) { UserInfo ret = new UserInfo(); if (!UserDic.ContainsKey(userID)){ throw new Exception("该用户不存在"); } ret = UserDic[userID]; return ret; } public bool CancelUser(string userID) { if (!UserDic.ContainsKey(userID)){ throw new Exception("该用户不存在"); } UserDic.Remove(userID); return true; } } }
1 namespace RVC.WCF.Certificate 2 { 3 [DataContract] 4 public class UserInfo 5 { 6 [DataMember] 7 public string UserID; 8 [DataMember] 9 public string UserName; 10 [DataMember] 11 public string Password; 12 [DataMember] 13 public DateTime CertTime; 14 } 15 }
建立的 WCF服务库,仅需新增 .cs接口文件 和 .svc.cs接口实现文件,配置文件 App.config 暂时无需改动。注意代码仅供参考,不要过于讲究细节。
[2]. 包装WCF服务库
建立好的 WCF服务库不能直接运行,须要宿主托管才能执行。经过 VS新建项目 -> ASP.NET 空 Web 应用程序,做为该 WCF服务类库的 Host 将上述 WCF服务类库包含进来。
每个 ASP.NET Web服务 都具备一个 .asmx 文件,客户端经过访问 .asmx 文件实现对相应 web 服务的调用。相似,每一个 WCF 服务也具备一个对应的文件,即 .svc 文件。基于 IIS 的服务寄宿要求相应的 WCF 服务具备相应的 .svc 文件,.svc 文件部署于 IIS 站点中,对 WCF 服务的调用体如今对 .svc 文件的访问上。
(1)右键项目,添加一个文件夹,命名为 Services
(2)右键 Services 文件夹,添加 WCF服务 文件,分别命名为 CalculateService.svc 和 CertificateService.svc,同时将 .cs接口文件 和 .svc.cs接口实现文件删除,只留下 .svc服务托管文件
(3)依次修改 .svc 文件为以下形式,分别指向对应的服务实现文件
// CalculatorService <%@ ServiceHost Language="C#" Debug="true" Service="RVC.WCF.Calculator.CalculatorService" CodeBehind="CalculateService.svc.cs" %> // CertificateService <%@ ServiceHost Language="C#" Debug="true" Service="RVC.WCF.Certificate.CertificateService" CodeBehind="CertificateService.svc.cs" %>
其中 Service = "命名空间.服务名称" 表示提供的服务,CodeBehind = "服务接口实现文件" 表示服务实现类。
除此以外,Web.config 暂不改动。至此,服务相关文件所有建立完毕,下面给出资源结构图
其中,右图是为了调试方便,将2个WCF服务类库项目添加到了该Web应用程序中。
为了验证服务是否能运行,右键 RVC.WCF.Service,选择 在浏览器中查看 并进入 Services 文件夹
点击便可查看服务运行状况。
[3]. 在 IIS 上发布 WCF 服务
一样是右键 RVC.WCF.Service,选择 生成部署包,生成文件路径:...\RVC.WCF.Service\obj\Debug\Package\PackageTmp
将该文件夹下的文件复制到 IIS 的相关路径下,在 IIS -> 网站 -> 添加网站 或 IIS -> 网站 -> Default Web Site -> 添加应用程序 并指向该路径便可。
至此,将该 WCF 服务成功发布到 IIS 上。
推荐文章:WCF4.0新特性体验(7):IIS无SVC文件托管WCF服务(IIS hosting without an SVC file );
在 WCF 学习中遇到的一些问题
[1]. 如何读取配置文件中 endpoint 结点的地址?
// 方法1 SqhServiceClient SqhClient = new SqhServiceClient(); var endpoint = SqhClient.Endpoint; // 方法2 string sectionPath = "system.serviceModel/client"; var clientNode = (ClientSection)ConfigurationManager.GetSection(sectionPath); var endpoints = clientNode.Endpoints;
其中,方法1只能读取某个终结点的信息,而方法2能够读取该服务下全部终结点的信息。
关于方法2的具体解决方法参见:http://www.cnblogs.com/huangxincheng/p/4396284.html