为了实现不同系统间的业务数据交换,我们需要访问对方的接口,也需要开放出自己的接口。.Net的web service挺好用的,不管是创建还是调用都是相当容易。
创建Web Service
-
添加Web服务(ASMX)
默认的大概就这个样子吧,例子是个很好的东西。
/// <summary>
/// WebService1 的摘要说明
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消注释以下行。
// [System.Web.Script.Services.ScriptService]
public class WebService1 : System.Web.Services.WebService
{
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}
}
-
创建Web Method
依葫芦画瓢,添加方法并为方法添加WebMethod特性。当需要实现的服务比较复杂,类型比较多,但是调用比较集中,可以考虑一个Web Method承载多个服务,把服务名包含在参数中,这要可以避免到处都是Web Method,数据格式也比较统一,写说明文档的时候深有感受。
/// <summary>
/// TestService1 把服务名做成一个参数
/// </summary>
/// <param name="service">服务名</param>
/// <param name="data">数据</param>
/// <param name="verifyCode">校验码</param>
/// <returns></returns>
[WebMethod]
public string TestService1(string service,string data,string verifyCode)
{
return null;
}
/// <summary>
/// TestService2 把服务名包含在主数据内
/// </summary>
/// <param name="data">数据</param>
/// <param name="verifyCode">校验码</param>
/// <returns></returns>
[WebMethod]
public string TestService2(string data,string verifyCode)
{
return null;
}
这两种方案我也挺纠结的,一扯就多了,最近用了第二种方案做接口有点后悔了,所以我们以第一种方案为例。
访问接口的请求需要验明正身(这里考虑的是用户名密码这种),但本着不传密码本身的思想,数据在发送前需要把数据和密码计算成一个校验码,类似上面的考虑,也有两种方案,贴一个上来。
/// <summary>
/// TestService3 把用户ID做成一个参数
/// </summary>
/// <param name="service">服务名</param>
/// <param name="userId">用户ID</param>
/// <param name="data">数据</param>
/// <param name="verifyCode">校验码</param>
/// <returns></returns>
[WebMethod]
public string TestService3(string service,string userId, string data, string verifyCode)
{
return null;
}
是否应该写在同一个Web Method中可以看服务类型是不是能够分到一个大类里面,而参数怎样组成可以根据对安全和便捷的要求。对数据安全性要求不高的时候,可以直接用两个参数,一是包含服务名和用户ID在内的主数据,二是可以结合主数据进行身份验证的校验码。
至于主数据的结构,见过JSON和XML,其中XML偏多。不知怎么的,感觉XML与Web Service更般配。但是显然XML结构比JSON更笨重,因为XML每个节点前后都有节点名称,而JSON一个属性一个名称;生成XML格式数据也比生成JSON格式数据麻烦,大部分XML的特别的东西没有体现出来,我们只需要一个简单的统一的数据格式标准而已。
后来遇到另一种XML数据格式的结构,把数据中的变量全部做成XML节点的属性,XML节点变少了,也算是一种方案。
-
添加调用方法
刚才新添加的Web Service默认支持soap1.1,soap1.2访问方法。如果需要支持POST方法,需要修改config文件,在system.web节点中增加配置。
<system.web>
<webServices>
<protocols>
<add name="HttpGet"/>
<add name="HttpPost"/>
</protocols>
</webServices>
</system.web>
访问Web Service
浏览器打开Web Service的路径,可以看到访问方法示例,soap1.1,soap1.2,post,照着样子拼凑出来就可以调用。另外可以添加web引用,添加服务引用,制作dll的方式调用。
以下的例子用的Web Service创建为
/// <summary>
/// TestService 的摘要说明
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消注释以下行。
// [System.Web.Script.Services.ScriptService]
public class TestServices : System.Web.Services.WebService
{
[WebMethod]
public string HelloWorld(string name)
{
return "Hello "+name;
}
}
忍不住说一句,python调用不要太容易,顿时感觉写C#好苦逼
from suds.client import Client
url = 'http://www.webxml.com.cn/WebServices/WeatherWebService.asmx?wsdl"
client = Client(url)
result = client.service.HelloWorld('A')
print(result)
-
添加服务引用
添加服务引用之后,config文件内将在configuration节点下自动生成
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="TestServicesSoap" />
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:1029/TestServices.asmx" binding="basicHttpBinding"
bindingConfiguration="TestServicesSoap" contract="ServiceReference1.TestServicesSoap"
name="TestServicesSoap" />
</client>
</system.serviceModel>
调用
ServiceReference1.TestServicesSoapClient client = new ServiceReference1.TestServicesSoapClient();
string content = client.HelloWorld("甲");
当前项目直接调用不会有什么问题
如果项目A添加了服务引用,项目B引用项目A,会出现类似于下面这种错误:
在 ServiceModel 客户端配置部分中,找不到引用协定“ServiceReference1.TestServicesSoap”的默认终结点元素。这可能是因为未找到应用程序的配置文件,或者是因为客户端元素中找不到与此协定匹配的终结点元素。
解决办法,将刚才那段配置信息复制到项目B的config文件中就可以了。
如果要在项目B中直接调用项目A中的服务引用,需要在项目B中添加引用system.serviceModel。
-
添加Web引用
添加Web引用之后,config文件内将在configuration节点下自动生成
<applicationSettings>
<service.Properties.Settings>
<setting name="service_localhost_TestServices" serializeAs="String">
<value>http://localhost:1029/TestServices.asmx</value>
</setting>
</service.Properties.Settings>
</applicationSettings>
调用
WebService1.TestServices service = new WebService1.TestServices();
//service.Url = "http://localhost:1031/TestServices.asmx";
string content = service.HelloWorld("甲");
当项目B调用项目A时将不需要修改配置文件
-
制作成dll文件
使用vs命令行wsdl制作dll,步骤:
1.浏览器打开http://localhost:1029/TestServices.asmx?wsdl,并保存为.wsdl文件,如:TestServices.wsdl
2.执行命令wsdl /namespace:Services.Test TestServices.wsdl,将生成文件TestServices.cs
3.执行命令csc /out:Services.TestServices.dll /t:library TestServices.cs,将生成文件Services.TestServices.dll
访问Web Service直接引用Services.TestServices.dll,干净利落。
Services.Test.TestServices service = new Services.Test.TestServices();
//service.Url = "http://localhost:1031/TestServices.asmx";
string content = service.HelloWorld("甲");
其它非.Net的Web Service制作dll也可以用类似的方案。
当wsdl文件带有wsdl:import时,需要把import的文件也下载下来,否则wsdl命令会执行出错,一个一个找import难免比较麻烦,这里有个取巧的方法,添加web引用或者服务引用都会生成全部的wsdl文件,找到对应目录拷贝出来或者直接定位到对应目录生成dll即可。
-
post方法
用Post方法调用需要注意几点
1.ContentType设置为application/x-www-form-urlencoded
2.Request流用System.Text.Encoding.Default编码
3.Response流用System.Text.Encoding.UTF8解码
4.键值对的值一定要UrlEncode编码
因此简单的访问方式就出来了
string url = "http://localhost:1031/TestServices.asmx" + "/HelloWorld";
string data = "name=" + HttpUtility.UrlEncode("甲");
string content;
WebRequest req = HttpWebRequest.Create(url);
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
using (StreamWriter sw = new StreamWriter(req.GetRequestStream(), System.Text.Encoding.Default))
{
sw.Write(data);
}
using (StreamReader sr = new StreamReader(req.GetResponse().GetResponseStream(), System.Text.Encoding.UTF8))
{
content = sr.ReadToEnd();
}
-
调用动态生成的Web Service
WebService不适合高频动态生成,偶尔需要调用可以用动态生成
public static object InvokeWebService(string url, string methodname, object[] args)
{
//namespace是需引用的webservices的命名空间
string @namespace = "client";
//获取WSDL
System.Net.WebClient wc = new System.Net.WebClient();
System.IO.Stream stream = wc.OpenRead(url + "?WSDL");
System.Web.Services.Description.ServiceDescription sd = System.Web.Services.Description.ServiceDescription.Read(stream);
string classname = sd.Services[0].Name;
System.Web.Services.Description.ServiceDescriptionImporter sdi = new System.Web.Services.Description.ServiceDescriptionImporter();
sdi.AddServiceDescription(sd, "", "");
System.CodeDom.CodeNamespace cn = new System.CodeDom.CodeNamespace(@namespace);
//生成客户端代理类代码
System.CodeDom.CodeCompileUnit ccu = new System.CodeDom.CodeCompileUnit();
ccu.Namespaces.Add(cn);
sdi.Import(cn, ccu);
//设定编译参数
System.CodeDom.Compiler.CompilerParameters cplist = new System.CodeDom.Compiler.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");
//编译代理类
Microsoft.CSharp.CSharpCodeProvider csc = new Microsoft.CSharp.CSharpCodeProvider();
System.CodeDom.Compiler.CompilerResults cr = csc.CompileAssemblyFromDom(cplist, ccu);
if (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(@namespace + "." + classname, true, true);
object obj = Activator.CreateInstance(t);
System.Reflection.MethodInfo mi = t.GetMethod(methodname);
return mi.Invoke(obj, args);
}
看不懂没关系,直接调用,好好使用,不要把内存撑着了。
string url = "http://localhost:1031/TestServices.asmx";
string method = "HelloWorld";
string name = "甲";
string content = InvokeWebService(url, method, new object[] { name }) as string;
-
补充
1.对于接口相同的多套系统,每个系统都对接一遍不现实,只是URL不同。其中web引用和dll都有属性URL,可以修改,服务引用貌似不能修改URL。如果对方系统更新,Web引用和服务引用都可以直接更新,dll不能直接更新,需要重新制作。
2.若是在C#中调用Java的webservice,可能会遇到一个问题,java只能获取到string类型参数的值,int,double等在服务器端得到null。非string类型的属性同时生成两个属性:"属性××"、"属性××Specified"。而"属性××Specified"是一个bool类型,只有这个属性被设置成true时,"属性××"的值才会被序列化成xml传递。因此需要注意修改cs文件,把"属性××Specified"删掉。如果是服务引用和web引用,可以直接找到Reference.cs进行修改,如果是制作dll,需要在生成的cs文件上修改。
第一次遇到这个问题时某人给我的解答:http://www.cnblogs.com/zhbsh/archive/2013/04/22/3035477.html
顺手贴上微软的解释:https://msdn.microsoft.com/zh-cn/library/system.xml.xmlattribute.specified(v=vs.110).aspx
3.如果有留意会发现web引用的Reference.cs文件与wsdl命令生成的cs文件惊人的相似,因此制作dll还有个取巧的方法,先添加web引用,然后csc命令打包成dll。
4.哪天想起再补充
网友评论