Code Copied

WCF SOAP客户端和REST客户端调用

介绍

通常有2种形式的客户端调用WCF服务:“HTML客户端”和“C#客户端”,这两种“客户端”的说法并不准确,确切的说应该是REST客户端和SOAP客户端。
由于本文演示两种客户端的调用方式分别使用了HTML代码和C#代码,为了让下面的陈述更贴近代码层次,暂且用这种不恰当的称谓。

HTML客户端

HTML客户端通过JavaScript向WCF服务的操作发起请求获取资源(XML,JSON等数据),HTML客户端又有2种调用类型:同域和跨域,其中跨域调用有两种方式实现。

  • 同域XMLHTTPRequest调用
  • 跨域XMLHTTPRequest调用
  • 跨域JSONP调用

HTML客户端(REST客户端)的调用过程可从用下图来描述,当然您也可以先阅读鄙人之前的一篇文章:REST介绍

image

C#客户端

C#客户端通过调用本地代理,然后代理再去调用服务端的WCF服务实现调用。
生成客户端代理有多种方式,常用的有两种:①在Visual Studio的工程中添加Service Reference ②通过WCF的svcutil工具生成。
由于本文中的代码都在Visual Studio环境下演示,所以我们将采用方式①。

SOAP客户端的调用过程可以从下图get√。同样地,您也可以先阅读SOAP介绍这一篇文章,以get√一些必要的知识点。

image

下面显示了以上几种调用方式和WCF终结点和绑定的关系。
如果使用HTML客户端,则终结点的绑定类型应该为WebHttpBinding;如果使用C#客户端,则终结点的绑定类型可以是BasicHttpBinding或WsHttpBinding,本文中使用了BasicHttpBinding演示这种情况。

image

同域XMLHttpRequest调用

1. 创建WCF工程

创建一个WCF Service Application类型的工程,工程名称为WcfXHRService。
       定义一个服务契约IInfoService                下面的服务契约包含以下几个信息:
               ①.服务契约的名称为IInfoService
               ②.操作契约的名称为SimpleOperation
              ③.操作契约的请求参数(输入参数)为3个string类型的参数,响应参数(输出参数)为一个数据契约类型。

using System.Runtime.Serialization;
using System.ServiceModel;

namespace WcfXHRService
{
    [ServiceContract]
    public interface IInfoService
    {

        [OperationContract]
        OutputInfo SimpleOperation(string dealerCode, string userId, string ifid);
    }

    /// <summary>
    /// 返回信息的数据契约
    /// </summary>
    [DataContract]
    public class OutputInfo
    {
        /// <summary>
        /// 返回状态
        /// </summary>
        [DataMember]
        public string Status { get; set; }
        /// <summary>
        /// 返回信息
        /// </summary>
        [DataMember]
        public string Message { get; set; }
    }
}

2. 实现服务契约

定义一个类InfoService.cs,继承服务契约接口,然后实现操作契约。

namespace WcfXHRService
{
    public class InfoService : IInfoService
    {
        /// <summary>
        /// 请求参数的类型都是简单类型
        /// </summary>
        public OutputInfo SimpleOperation(string dealerCode, string userId, string ifid)
        {
            var info = new OutputInfo
            {
                Status = "0",
                Message = "OK"
            };

            return info;
        }
    }
}

实现操作契约的方法没有添加任何逻辑,这里仅仅起演示作用。

3. 使用一个空的Web.config文件

为了逐步演示web.config中各个配置项对运行WCF程序的影响,我们使用一个空的web.config文件进行配置。
将工程WcfHttpService自带的web.config文件内容清空,仅保留<system.web>节点。或者您可以将这个web.config文件删除以后,再新建一个web.config文件。  

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

</configuration>

我们现在看到的CommonWcfService目录结构如下图:
①.服务契约定义:IInfoService.cs
②服务契约实现:InfoService.svc
③配置文件:web.config

image

4. 配置服务的元数据公布

先不要在服务代码或者web.config中添加任何配置,我们在浏览器中查看WcfXHRService的InfoService.svc文件。
虽然页面上没有出现Exception信息,但明显地我们的service无法访问:Metadata publishing for the service is currently disabled.(元数据发布服务当前被禁用)

Fortunately,页面上也给出了如何配置元数据公布的步骤。

image 

按照页面提示的步骤,在web.config中添加以下配置:

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.serviceModel>

    <services>
      <!-- Note: the service name must match the configuration name for the service implementation. -->
      <service name="WcfXHRService.InfoService" behaviorConfiguration="MyServiceTypeBehaviors" >
        <!-- Add the following endpoint.  -->
        <!-- Note: your service must have an http base address to add this endpoint. -->
        <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />
      </service>
    </services>

    <behaviors>
      <serviceBehaviors>
        <behavior name="MyServiceTypeBehaviors" >
          <!-- Add the following element to your service behavior configuration. -->
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>

  </system.serviceModel>

</configuration>

5.配置一个webHttpBinding的服务终结点

再次在浏览器中浏览InfoService.svc文件,这次页面显示了Application Error,并提示我们要配置服务的终结点

image

终结点(endpoint)用于公开服务的地址(address)、绑定(binding)和契约(contract),这也是我们所熟知的ABC。

每个服务必须至少公开一个业务终结点,而上面的配置是帮助我们添加一个基于HTTP的元数据交换终结点,是非业务的终结点。

选择一个绑定方式

当以IIS作为WCF服务的宿主,并为服务配置终结点时,我们常用的绑定方式有3种
①.webHttpBinding
②.basicHttpBinding
③.wsHttpBinding

①是REST风格的绑定,客户端请求一个URL就能得到一些XML或JSON格式的数据,客户端的代码常常是html或js,在本文中这种客户端我称之为“html客户端”。
            ②和③基于SOAP的绑定,使用这两种绑定的终结点将在WSDL和XSD文件中公开更多的服务细节,我们必须使用一个SOAP客户端来调用服务,这个SOAP客户端就是我们所熟知的代理。
在Visual Studio中,代理可以通过在项目中添加Service Reference生成,在本文中我将用C#演示这种情况,在本文中这种客户端我称之为“C#客户端”。

关于这3种binding的解释,请参考stackoverfolw上面较为精辟的一段解释:
http://stackoverflow.com/questions/2650785/basichttpbinding-vs-wshttpbinding-vs-webhttpbinding

You're comparing apples to oranges here:

▪ webHttpBinding is the REST-style binding, where you basically just hit a URL and get back a truckload of XML or JSON from the web service

▪ basicHttpBinding and wsHttpBinding are two SOAP-based bindings which is quite different from REST. SOAP has the advantage of having WSDL and XSD to describe the service, its methods, and the data being passed around in great detail (REST doesn't have anything like that - yet). On the other hand, you can't just browse to a wsHttpBinding endpoint with your browser and look at XML - you have to use a SOAP client, e.g. the WcfTestClient or your own app.

So your first decision must be: REST vs. SOAP (or you can expose both types of endpoints from your service - that's possible, too).

Then, between basicHttpBinding and wsHttpBinding, there differences are as follows:

basicHttpBinding is the very basic binding - SOAP 1.1, not much in terms of security, not much else in terms of features - but compatible to just about any SOAP client out there --> great for interoperability, weak on features and security

wsHttpBinding is the full-blown binding, which supports a ton of WS-* features and standards - it has lots more security features, you can use sessionful connections, you can use reliable messaging, you can use transactional control - just a lot more stuff, but wsHttpBinding is also a lot *heavier" and adds a lot of overhead to your messages as they travel across the network

For an in-depth comparison (including a table and code examples) between the two check out this codeproject article: Differences between BasicHttpBinding and WsHttpBinding

配置一个webHttpBinding的终结点,然后添加一个endpoint,并添加其ABC元素。

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.serviceModel>

    <services>
      <!-- Note: the service name must match the configuration name for the service implementation. -->
      <service name="WcfXHRService.InfoService" behaviorConfiguration="MyServiceTypeBehaviors" >
        <!-- Add the following endpoint.  -->
        <!-- Note: your service must have an http base address to add this endpoint. -->
        <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />

        <!--配置一个webHttpBinding的终结点-->
        <endpoint address="" binding="webHttpBinding" contract="WcfXHRService.IInfoService"  />
      </service>
    </services>

    <behaviors>
      <serviceBehaviors>
        <behavior name="MyServiceTypeBehaviors" >
          <!-- Add the following element to your service behavior configuration. -->
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>

  </system.serviceModel>

</configuration>

再次在浏览器中浏览InfoService.svc文件

image

6. 配置服务使其支持REST GET请求

既然webHttpBinding通过一个URL就能够请求服务,并返回XML或JSON的数据,那么我们可以在浏览器中通过输入URL进行尝试,按照GET请求的方式将参数的名称和值输入到URL中。

例如:http://localhost:51969/InfoService.svc/SimpleOperation?dealerCode=SR002&userId=02&ifid=0101
结果:服务器给我们返回了SOAP Fault的消息,在chrome的F12工具中也可以看到请求状态500的错误。

image

解决办法

①.在操作契约上添加WebGet特性

[ServiceContract]
public interface IInfoService
{
    [WebGet]
    [OperationContract]
    OutputInfo SimpleOperation(string dealerCode, string userId, string ifid);
}

②.在web.config中添加一个终结点行为,并添加<enableScript />节点。
 然后在服务的终结点配置中,将行为配置指向该终结点行为。

image

在浏览器中再次浏览URL:http://localhost:51969/InfoService.svc/SimpleOperation?dealerCode=SR002&userId=02&ifid=0101

image

当使用WebHttpBinding绑定时,WCF默认的数据传输格式为json。
当服务的终结点行为配置使用了时,请求成功后返回默认的json根节点为d。

7. html客户端调用

我们已经将一个基本的restful的WCF服务配置好了,现在可以去添加一个html画面并通过jQuery Ajax来调用WCF服务了。
在项目下添加一个html页面,页面名称为same-domain-xhr.html。

image

然后将jQuery库也添加进来。

image

  

在same-domain-xhr.html中添加如下代码:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" src="jquery-1.10.2.min.js"></script>
    <script type="text/javascript">
        $(function () {

            $('#run').click(function () {
                
                var inputData = {
                    dealerCode: $('#dealerCode').val(),
                    userId: $('#userId').val(),
                    ifid: $('#ifid').val()
                };

                $.ajax({
                    cache: false,
                    url: 'InfoService.svc/SimpleOperation',
                    data: inputData,
                    type: 'GET',
                    dataType: 'json',
                    error: function () {
                        alert('error');
                    },
                    success: function (data) {
                        var d = data.d;
                        $('#status').text(d.Status);
                        $('#message').text(d.Message);
                    }
                });
            });
        });
    </script>

</head>
<body>

    <h2>Input</h2>
    <table>
        <tr>
            <td>Dealer Code</td>
            <td>
                <input type="text" id="dealerCode" value="SR002" />
            </td>
        </tr>
        <tr>
            <td>User ID</td>
            <td>
                <input type="text" id="userId" value="02" />
            </td>
        </tr>
        <tr>
            <td>IFID</td>
            <td>
                <input type="text" id="ifid" value="0101" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="button" id="run" value="Run" />
            </td>
        </tr>
    </table>

    <h2>Output</h2>
    <table>
        <tr>
            <td>Status:</td>
            <td>
                <label id="status"></label>
            </td>
        </tr>
        <tr>
            <td>Message:</td>
            <td>
                <label id="message"></label>
            </td>
        </tr>
    </table>

</body>
</html>

image

使用jQuery的$.ajax函数执行XMLHttpRequest,然后在success回调函数中将WCF服务返回的信息输出到画面。
请注意,由于json的根节点是d,所以要通过data.d才能获取到数据对象。

画面的运行结果:

image

配合截图,这个调用可以解释为3个步骤
①通过js向WCF服务发起XHR请求(XMLHttpRequest)
②执行WCF服务操作契约,返回json数据
③将请求结果输出到画面

跨域XMLHTTPRequest调用

在采用这种方式进行跨域调用时,我们首先要了解一些基本的概念:同源策略、跨域和跨域资源共享。

1. 同源策略和跨域的概念

同源策略(Same-orgin policy)限制了一个源(orgin)中加载脚本或脚本与来自其他源(orgin)中资源的交互方式。
如果连个页面拥有相同的协议(protocol),端口(port)和主机(host),那么这两个页面就属于同一个源(orgin)。

同源之外的请求都可以称之为跨域请求。
下表给出了相对http://store.company.com/dir/page.html同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功  
http://store.company.com/dir/inner/another.html 成功  
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同
http://news.company.com/dir/other.html 失败 主机名不同

我们可以简单粗暴地理解为统一站点下的页面相互访问都是同源的,跨站点的页面访问都是跨域的。

关于同源策略,请参考:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

2. 跨域资源共享

跨域资源共享(CORS)是一份浏览器技术的规范,提供了Web服务器从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是JSONP模式的现代版。与JSONP不同,CORS除了支持GET方法以外,还支持其他HTTP方法。用CORS可以让网页设计师用一般的XMLHTTPRequest,这种方式的错误处理比JSONP要来的好。另一方面,JSONP可以在不支持CORS的老旧浏览器上运作,现代的浏览器都支持CORS。

在网页http://caniuse.com/#feat=cors上列出了主流浏览器对CORS的支持情况,包含PC端和移动端的浏览器。

image

【图Cross-Orgin Resource Sharing】

3. 创建跨域示例

在Visual Studio中新建一个ASP.NET Empty Web Application项目。

image

将【同域XMLHttpRequest】中用到的same-domain-xhr.html, jquery-1.10.2.min.js文件复制到该项目下,然后将same-domain-xhr.html的名称更改为cross-domian-xhr.html。

image

修改cross-domian-xhr.html中ajax请求的URL,然后在浏览器中浏览cross-domian-xhr.html文件。

<script type="text/javascript">
    $(function () {

        $('#run').click(function () {
            
            var inputData = {
                dealerCode: $('#dealerCode').val(),
                userId: $('#userId').val(),
                ifid: $('#ifid').val()
            };

            $.ajax({
                cache: false,
                url: 'http://localhost:51969/InfoService.svc/SimpleOperation',
                data: inputData,
                type: 'GET',
                dataType: 'json',
                error: function () {
                    alert('error');
                },
                success: function (data) {
                    var d = data.d;
                    $('#status').text(d.Status);
                    $('#message').text(d.Message);
                }
            });
        });
    });
</script>

点击Run按钮,向WCF服务发送请求,在Output中没有得到返回信息。
打开chrome的F12工具,在Network页看到向WCF服务发送请求失败了,chrome也提示了错误信息:

No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:52224' is therefore not allowed access.

image

4. Access-Control-Allow-Origin配置

虽然我使用了最新版本的Chrome浏览器(支持CORS),但客户端的XMLHTTPRequest仍然请求失败。
要让客户端能够请求成功,还需要服务端的配合,在WCF服务中配置Access-Control-Allow-Origin可以解决这一问题。

Access-Control-Allow-Orgin在Response Header中配置,有两种方式配置Access-Control-Allow-Orgin。

①. 在HttpApplication的Application_BeginRequest事件中配置

public class Global : System.Web.HttpApplication
{

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
    }
}

②. 在web.config文件中配置

<system.webServer>
<httpProtocol>
  <customHeaders>
    <add name="Access-Control-Allow-Origin" value="*"/>
    <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE"/>
    <add name="Access-Control-Allow-Headers" value="Content-Type, Accept"/>
  </customHeaders>
</httpProtocol>
<modules runAllManagedModulesForAllRequests="true"/>
<directoryBrowse enabled="true"/>
system.webServer>

重新编译网站后,点击页面cross-domain-xhr.html的Run按钮,可以看到我们的XMLHTTPRequest跨域请求成功了。

image

跨域JSONP调用

1. JSONP概念

由于同源策略,一般来说不允许JavaScript跨域访问其他服务器的页面对象,但是HTML的<script>元素是一个例外。利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的 JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。

2. 创建一个支持JSONP的WCF服务

创建另一个WCF Service Application类型的工程,工程名称为WcfJSONPService,从WcfXHRService工程中将IInfoService.cs, InfoService.cs, InfoService.svc, web.config文件复制到WcfJSONPService工程下。

image

3. 配置web.config

要让WCF服务支持JSONP,关键还在于配置。

①.删除web.config中的<system.webServer>节点的内容。由于现在不使用        CORS的方式,所以Access-Control-Allow-Origin这些节点可以删掉了。

②.在<system.serviceModel>节点中添加以下配置

<bindings>
  <!--跨域Script访问启用-->
  <webHttpBinding>
    <binding name="WebBinding" crossDomainScriptAccessEnabled="true">
      <security mode="None"/>
    </binding>
  </webHttpBinding>
</bindings>

③.修改service的终结点,将bindingConfiguration属性指定为WebBinding。

<!--配置一个webHttpBinding的终结点-->
<endpoint address="" binding="webHttpBinding" contract="WcfJSONPService.IInfoService" 
          behaviorConfiguration="WebBeahvior" bindingConfiguration="WebBinding"  />

终,我们看到的web.config文件如下:

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.serviceModel>

    <services>
      <!-- Note: the service name must match the configuration name for the service implementation. -->
      <service name="WcfJSONPService.InfoService" behaviorConfiguration="MyServiceTypeBehaviors" >
        <!-- Add the following endpoint.  -->
        <!-- Note: your service must have an http base address to add this endpoint. -->
        <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />

        <!--配置一个webHttpBinding的终结点-->
        <endpoint address="" binding="webHttpBinding" contract="WcfJSONPService.IInfoService" 
                  behaviorConfiguration="WebBeahvior" bindingConfiguration="WebBinding"  />
      </service>
    </services>

    <behaviors>
      <!--服务行为-->
      <serviceBehaviors>
        <behavior name="MyServiceTypeBehaviors" >
          <!-- Add the following element to your service behavior configuration. -->
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>

      <!--终结点行为-->
      <endpointBehaviors>
        <behavior name="WebBeahvior">
          <enableWebScript />
        </behavior>
      </endpointBehaviors>

    </behaviors>

    <bindings>
      <!--跨域Script访问启用-->
      <webHttpBinding>
        <binding name="WebBinding" crossDomainScriptAccessEnabled="true">
          <security mode="None"/>
        </binding>
      </webHttpBinding>
    </bindings>

  </system.serviceModel>

</configuration>

4. 添加cross-domain-jsonp.html

在工程HtmlClient下添加一个cross-domain-jsonp.html文件,将cross-domain-xhr.html的文件内容复制过来。然后修改3处地方:

①. $.ajax函数中的url指向WcfJSONPService的svc文件
②. dataType使用jsonp
③. success函数中,直接通过data.Status和data.Message取数据

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" src="jquery-1.10.2.min.js"></script>
    <script type="text/javascript">
        $(function () {

            $('#run').click(function () {
                
                var inputData = {
                    dealerCode: $('#dealerCode').val(),
                    userId: $('#userId').val(),
                    ifid: $('#ifid').val()
                };

                $.ajax({
                    cache: false,
                    url: 'http://localhost:51972/InfoService.svc/SimpleOperation',
                    data: inputData,
                    type: 'GET',
                    dataType: 'jsonp',
                    error: function () {
                        alert('error');
                    },
                    success: function (data) {
                        $('#status').text(data.Status);
                        $('#message').text(data.Message);
                    }
                });
            });
        });
    </script>

</head>
<body>

    <h2>Input</h2>
    <table>
        <tr>
            <td>Dealer Code</td>
            <td>
                <input type="text" id="dealerCode" value="SR002" />
            </td>
        </tr>
        <tr>
            <td>User ID</td>
            <td>
                <input type="text" id="userId" value="02" />
            </td>
        </tr>
        <tr>
            <td>IFID</td>
            <td>
                <input type="text" id="ifid" value="0101" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="button" id="run" value="Run" />
            </td>
        </tr>
    </table>

    <h2>Output</h2>
    <table>
        <tr>
            <td>Status:</td>
            <td>
                <label id="status"></label>
            </td>
        </tr>
        <tr>
            <td>Message:</td>
            <td>
                <label id="message"></label>
            </td>
        </tr>
    </table>

</body>
</html>

5. 运行cross-domain-jsonp.html

image

image

6. JSONP和XMLHttpRequest的比较

①.cross-domain-jsonp.html请求的是一段Script,Type是application/x-javascript。

通过IE访问请求链接,我们得到的是SimpleOperation.js文件

image

②.请求的参数中包含callback参数,这是jQuery对jsonp的支持自动加上的参数

③.请求返回的内容是回调函数

image

④.在IE9以下以及其他旧浏览器中,客户端的请求也能执行成功

image

⑤JSONP只支持HTTP的GET方法,但XMLHTTPRequest则支持其他HTTP方法(例如POST)。