Code Copied

WCF回调原理和示例

WCF回调原理

回调操作又称之为“双向操作”,WCF的回调操作可以理解为Service端和Client端之间的互相调用,或者说Service端和Client端的消息交换。

在回调期间,Service端将成为Client端,Client端将成为Service端(见下图)。在涉及到服务发生的事件需要通知客户端时,回调非常有用。

image

在回调模式操作中,Client端和Service端实际发生了4次通信

image

ServiceRequest:客户端向服务端发出调用,在调用的过程中会把回调实例的引用一起发送到服务端。

CallbackRequest:服务端从上一步的请求中取得对回调实例的引用,通过该回调实例向客户端发出的回调请求。

CallbackResponse:客户端执行完回调后向服务端返回的响应,如果回调契约中的方法契约被定义为IsOnWay的话,这一步不会发生。

ServiceResponse:服务端方法执行完后向客户端返回信息(方法的返回值等)。

示例创建

接下来我们通过一个案例演示WCF的回调:

Service端提供Login操作,并判定Login是否成功。如果成功则返回用户的昵称;失败则返回空。

Client端提供Welcome回调操作,根据Login操作提供的昵称来显示信息。如果用户昵称不为空,则显示欢迎信息;用户昵称为空,则提示用户登录失败。

image

1. 创建工程

打开Visual Studio,新建一个解决访问项目,解决方案名称WCFCallbackSample

image

添加一个新项目,项目类型WCF Service Application,项目名称Service

image

添加一个新项目,项目类型Console Application,项目名称Client

image

移除Service下默认的Service1.svc相关的文件,并创建一个UserService.svc文件

image

2. 声明服务契约和回调契约

using System.ServiceModel;

namespace Service
{
    /// <summary>
    /// 服务契约声明
    /// </summary>
    [ServiceContract(CallbackContract = typeof(ILoginCallback))]
    public interface IUserService
    {
        [OperationContract]
        void Login(string userName, string password);
    }

    /// <summary>
    /// 回调契约声明
    /// </summary>
    public interface ILoginCallback
    {
        [OperationContract]
        void Welcome(string nickName);
    }
}

3. 实现服务契约

using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;

namespace Service
{
    /// <summary>
    /// 服务契约实现
    /// </summary>
    public class UserService : IUserService
    {
        private readonly IList<User> _userList = new List<User>
        {
            new User{UserName = "sunnypeng", Password = "123456", NickName = "Sunny Peng"},
            new User{UserName = "chrischen", Password = "123456", NickName = "Chris Chen"}
        };

        public void Login(string userName, string password)
        {
            var user = _userList.FirstOrDefault(u => u.UserName == userName && u.Password == password);

            string nickName = user == null ? "" : user.NickName;
            
        }
    }

    public class User
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string NickName { get; set; }
    }
}

编译Service项目,并浏览UserService.svc文件

image

image

当为服务契约(ServiceContract)配置了回调契约时(Callback Contract),在没有配置web.config文件的情况下,浏览UserService.svc文件将出现错误信息。

WCF Service Application默认采用BasicHttpBinding绑定,因为HTTP本质上是无连接的,所以基于BasicHttpBinding或WSHttpBinding的绑定无法使用回调。

WCF中支持回调操作的绑定有3种 :

  1. NetTcpBinding
  2. NetNamedPipeBinding
  3. WSDualHttpBinding

从本质上讲,TCP和IPC协议都支持双向通信,但是为了让HTTP支持回调操作,WCF提供了WSDualHttpBinding绑定, 它实际上设置了两个HTTP通道:一个用于Client调用Server,一个用于Server调用Client。

4. 配置WSDualHTTPBinding

web.config文件中的其他配置节点仍然可以保留,在<system.serviceModel>节点内,只需要做2步配置就能够让service支持双攻。

  1. 配置wsDualHttpBinding
  2. 将wsDualHttpBinding应用于service的endpoint(终结点)配置中
<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, set the value below to false before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />

    <!--配置wsDualHttpBinding-->
    <bindings>
      <wsDualHttpBinding>
        <binding name="dualBinding">
          <security mode="None" />
        </binding>
      </wsDualHttpBinding>
    </bindings>

    <!--配置Service的ABC,并使用wsDualHttpBinding绑定配置-->
    <services>
      <service name="Service.UserService">
        <endpoint address="" binding="wsDualHttpBinding" contract="Service.IUserService"
                  bindingConfiguration="dualBinding" />
      </service>
    </services>

  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <!--
        To browse web app root directory during debugging, set the value below to true.
        Set to false before deployment to avoid disclosing web app folder information.
      -->
    <directoryBrowse enabled="true"/>
  </system.webServer>

</configuration>

再次编译并浏览UserService.svc文件,服务已经能够正常显示了。

image

5. 回调实现

image

image

/// <summary>
/// 实现Service端的ILoginCallback接口
/// </summary>
public class LoginCallback : IUserServiceCallback
{
    public void Welcome(string nickName)
    {
        if (!string.IsNullOrEmpty(nickName))
        {
            Console.WriteLine("欢迎您, {0}!", nickName);
        }
        else
        {
            Console.WriteLine("登录失败,请重新登录!");
        }
        
    }
}

这里的回调接口名称是IUserServiceCallback,和Service端声明的接口名称不一样。当使用VS自动生成代理时,回调接口命名默认是以服务契约接口名称+Callback,而不是原先在Service端定义的回调接口


image

 

6. 服务端回调调用

前面也说了,回调是Service端调用Client端,所以回调的调用方法要在Service端执行,我们决定在User执行登录的时候调用Client端的回调方法。
OperationContext类为服务提供了方便访问回调引用的路径,回调对象通过OperationContext对象的GetCallbackChannel<T>方法获得。

public void Login(string userName, string password)
{
    var user = _userList.FirstOrDefault(u => u.UserName == userName && u.Password == password);

    string nickName = user == null ? "" : user.NickName;
    
    // Service端回调Client端
    var callback = OperationContext.Current.GetCallbackChannel<ILoginCallback>();
    callback.Welcome(nickName);
}

7. 调用演示

在Client端调用Service对操作前,首先要创建回调契约实例,并将上下文(InstanceContext)作为它的宿主,然后用代理把回调引用传给Service端。

class Program
{
    static void Main(string[] args)
    {

        InstanceContext context = new InstanceContext(new LoginCallback());
        var client = new UserServiceClient(context);
        Console.WriteLine("---------------------用户1登录--------------------------");
        Console.WriteLine();
        client.Login("sunnypeng","123456");
        Console.WriteLine();
        Console.WriteLine("--------------------------------------------------------");

        Console.WriteLine("---------------------用户2登录--------------------------");
        Console.WriteLine();
        client.Login("chrischen", "123456789");
        Console.WriteLine();
        Console.WriteLine("--------------------------------------------------------");

        Console.Read();
    }
}

Service端在回调Client端的方法时,产生了System.InvalidOperationException异常。

image

8. 重入和单向操作

在默认情况下,服务类被配置为单线程访问的:Service实例上下文与锁关联,在任何时刻都只能有一个线程拥有锁,也只能有一个线程访问上下文中的Service实例。在操Operation调用期间,在Client端发出的调用需要阻塞服务线程,并调用回调。当单线程的Service实例试图调用返给它的客户端,为了避免deadlock,WCF会抛出InvalidOperationException异常。

有3种方案来解决这个问题。

方案1:配置Service,允许多线程访问。由于服务实例与锁无关,因此允许正在调用的Client端回调。但是这种情况也会增加开发者的负担,因为它需要为Service提供同步本文中我们将不演示方案1的多线程同步。

方案2:将Service配置为重入(Reentrant),一旦配置为重入,Service实例就与锁关联,同时只允许单线程访问。如果Service正在回调它的Client端,WCF就会先释放锁。

在ServiceBeahvior特性中使用ConcurrencyMode属性,可以将Service的并发行为配置为多线程(Multiple)或重入(Reentrant)。

// Decompiled with JetBrains decompiler
// Type: System.ServiceModel.ConcurrencyMode
// Assembly: System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// MVID: 20B40BB3-D8EC-4525-AF58-C929A5670363
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.ServiceModel.dll

namespace System.ServiceModel
{
  public enum ConcurrencyMode
  {
    Single,
    Reentrant,
    Multiple,
  }
}

 

image

方案3:将回调契约操作配置为单向操作,这样Service就能安全地将调用返回给客户端。因为没有任何应答消息会竞用锁,即使并发模式设置为单线程,服务也能够支持回调。(服务的默认并发模式为单线程)

image

无论采用方案2还是用方案3,程序最终都能够正常执行。

image

9. 总结

通过以上的讲解和示例,Service端和Client端分别按照以下步骤就能够创建一个完整WCF的回调解决方案。

Service端

1. 声明并实现Service Contract(服务契约)

2. 声明Callback Contract(回调契约)接口

3. 将ServiceContract的ServiceBehavior特性的CurrencyMode属性配置为重入(Reentrant),或者将回调契约操作配置为单向操作(One Way)

4. 通过OperationContext对象的GetCallbackChannel<T>方法获取客户端回调实例,并回调客户端方法

5. 配置web.config设定wsDualHttpBinding

Client端

1. 实现Callback Contract(回调契约)接口

2. 实例化回调契约并创建InstanceContext,通过代理将回调引用传给Service端

3. 通过代理调用Service端操作方法

参考和下载

参考内容:WCF服务编程第5章

源码下载:http://blog.64cm.com/source/WCF/WCFCallbackSample.zip