65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 通信选项 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (43投票s)

2006年7月20日

CPOL

12分钟阅读

viewsIcon

161699

一篇关于 WCF 通信选项的文章 - 第二部分。

引言

第一部分中,我们定义了什么是服务,并创建了一个简单的 WCF 服务和客户端来演示在 WCF 中创建和使用服务的过程。在本文中,我们将开始探索 WCF 中一些更高级的通信选项。本文中所有示例的源代码都在第一部分的下载中提供。

一体化服务

让我们继续实现一个服务和客户端都存在于同一进程中的示例!为什么有人会想要这样做?嗯,我至少需要过。假设你有一个模块,其中包含多个服务。这些服务实际上是抽象和隔离数据库访问的数据库服务器。嗯,其中一个服务可能实际上需要由(同一进程中的)另一个服务提供的功能。既然它们都在同一个宿主中,你就可以在本地实例化所需的服务(类)。但是,那样的话,你将以不同于其他客户端的方式访问服务,并且你还会绕过 ServiceHost 提供的隔离和其他功能。无论如何,当需求合适时,这只是另一种做事方式。在后续部分,我们将实际做相反的事情,即创建一个单例,并且我们只需要服务类的一个实例。下面的源代码显示了修改 LocalTimeService 示例所需的更改。

namespace AllInOneTimeService
{
    class Client
    {
        public bool keepClocking = true;
        LocalTimeProxy proxy = null;
        public Client()
        {
            proxy = new LocalTimeProxy();
        }
        public void ClockingThread()
        {
            while (keepClocking)
            {
                Console.WriteLine(proxy.GetLocalTime());
                Thread.Sleep(1000);
            }
            proxy.Close();
        }
        static void Main(string[] args)
        {
            Uri baseAddress = new 
              Uri(ConfigurationManager.AppSettings["basePipeTimeService"]);
            ServiceHost serviceHost = new 
              ServiceHost(typeof(LocalTimeService), baseAddress);
            serviceHost.Open();
            //The service is open for business
            Console.WriteLine("Service is running...." + 
                              "press any key to terminate.");
            //Start a client thread
            Client client = new Client();
            Thread thread = new Thread(new 
                   ThreadStart(client.ClockingThread));
            thread.Start();

            Console.ReadKey();
            client.keepClocking = false;

            //wait 2 seconds
            Thread.Sleep(2000);
            //Close up shop
            serviceHost.Close();
        }
    }
}

服务启动后,我们只需实例化一个客户端。当然,这只是为了演示服务可以被内部外部源访问。通常,这会是作为客户端请求的结果,一个服务访问另一个服务。唯一值得注意的其他更改是,配置文件需要同时定义服务客户端的终结点。继续启动此版本的服务。然后,启动多个独立客户端的实例。正如你所见,服务可以从不同源(内部和外部)访问,并且它们都由同一个主机提供服务。

一石二鸟

我们一直在使用的 LocalTimeService 示例仅支持 IPC(使用命名管道的内部客户端)。让我们改变这一点,并添加对 TCP 客户端的支持。最酷的是,我们不需要做太多更改,大部分都是配置文件更改。下面的代码是我们对服务代码所做的唯一更改。我们正在使用服务的 AllInOne 版本,以便你能看到我们如何同时支持这两种传输类型。内部客户端将使用命名管道,而外部客户端将使用 TCP。

static void Main(string[] args)
{
    Uri baseAddress = new Uri(
      ConfigurationManager.AppSettings["baseTcpTimeService"]);
    ServiceHost serviceHost = new ServiceHost(
      typeof(LocalTimeService), baseAddress);
    baseAddress = new Uri(
      ConfigurationManager.AppSettings["basePipeTimeService"]);
    serviceHost.AddServiceEndpoint(typeof(ILocalTime), 
              new NetNamedPipeBinding(), baseAddress);
    serviceHost.Open();
    ...
}

首先,我们实例化 ServiceHost,并将它要支持的终结点(TCP)的名称传递给它。然后,我们以编程方式添加第二个终结点(命名管道)。所有必需的信息都在配置文件中,如下所示

<configuration>
  <appSettings>
    <add key="baseTcpTimeService" 
         value="net.tcp://:9000/LocalTimeService" />
    <add key="basePipeTimeService" 
         value="net.pipe:///LocalTimeService" />
  </appSettings>
  <system.serviceModel>
    <services>
      <service name="LocalTimeService">
        <endpoint 
           address="" 
           binding="netTcpBinding" 
           contract="ILocalTime" 
         />
      </service>
    </services>
    <client>
      <endpoint name ="LocalTimeService"
                address="net.pipe:///LocalTimeService"
                binding="netNamedPipeBinding"
                contract="ILocalTime" />
    </client>
  </system.serviceModel>
</configuration>

在独立客户端上唯一需要的更改是在配置文件中,如下所示

<configuration>
  <system.serviceModel>
    <client>
      <endpoint name ="LocalTimeService"
                address="net.tcp://:9000/LocalTimeService"
                binding="netTcpBinding"
                contract="ILocalTime" />
    </client>

  </system.serviceModel>
</configuration>

像以前一样,继续启动服务,然后启动几个独立的客户端。没有区别,只是外部客户端正在使用 TCP 与服务通信,而内部客户端使用命名管道。这不是很酷吗?!但是等等,还有更多。

惰性客户端

现在让我们考虑一个不同的服务。这是一个监控反应器温度的服务。尽快检测到反应器温度变化非常重要。所以,每个客户端需要以非常短的时间间隔与服务进行检查。那会是多少?每秒一次,每秒十次,每秒一百次?显然,如果客户端数量很大,并且温度不经常变化,那么将会有很多流量浪费。或者,假设客户端决定每 100 毫秒检查一次温度,但仍然有可能在此时间间隔内温度已经变化了几次(上下)。因此,如果客户端需要知道每一次温度变化,那么它就会错过一些。

所以,一个更好的(或者说“更棒的”,正如我的一位朋友所偏爱的)解决方案是实现发布/订阅模式。服务(传感器)将简单地监控温度,如果温度变化超过指定阈值,它将向任何已请求通知的客户端发送消息。客户端只需订阅该服务,然后等待任何通知。当通知到来时,每个客户端将根据其角色执行任何必要的操作:日志记录、报警、控制等。这是一个温度传感器服务的代码

//The interface the service exposes
[ServiceContract(Session = true, 
   CallbackContract = typeof(ITempChangedHandler))]
public interface ITempChangedPub
{
    [OperationContract(IsInitiating = true)]
    void Subscribe();
    [OperationContract(IsTerminating = true)]
    void Unsubscribe();
}
//The interface that clients must implement...
public interface ITempChangedHandler
{
    [OperationContract(IsOneWay = true)]
    void TempChanged(int newTemp);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class TempSensor : ITempChangedPub
{
    //a list of who's interested
    ArrayList subscribers = new ArrayList();
    TempSimulator sim;  //our temperature simulator
    public TempSensor()
    {
        //Start simulating the temperature
        sim = new TempSimulator(this);
        Thread simThread = new Thread(new 
               ThreadStart(sim.SimThread));
        simThread.Start();
    }
    public void Subscribe()
    {
        subscribers.Add(
          OperationContext.Current.GetCallbackChannel<ITempChangedHandler>());
    }
    public void Unsubscribe()
    {
        ITempChangedHandler caller = 
          OperationContext.Current.GetCallbackChannel<ITempChangedHandler>();
        foreach (ITempChangedHandler h in subscribers)
            if (h == caller)
            {
                subscribers.Remove(h);
                break;
            }
    }
    public void PublishTempChanged(int temp)
    {
        foreach (ITempChangedHandler h in subscribers)
            h.TempChanged(temp);
    }
    public void StopSim()
    {
        sim.runSim = false;
        Thread.Sleep(1000);
    }
}

上面代码中的一些注意事项。首先,你会注意到服务类已经被 ServiceBehavior 属性装饰。这个属性有几个选项,但它本质上指定了我们希望如何控制服务的“实例化行为”。在这种情况下,我们指定“Single”,因为我们只需要服务的一个实例。它必须保留下来才能接收来自客户端的订阅消息。ServiceBehavior 属性还有另外两个参数。首先,Session=true 表示服务将为每个客户端创建和维护一个会话。会话的生命周期由 OperationContract 属性的 IsInitiating/IsTerminating 属性定义。因此,会话可以由客户端 大致 控制。让我们再看一眼。

考虑下面可能用于控制 AGV 的接口。自动导引车 (AGV) 本质上是自行导航的叉车。它们车载激光系统,可以知道它们在物理环境中的位置。然后你可以像控制遥控汽车一样控制它们。

[ServiceContract(Session=true)]
public interface IAGVControl
{
    [OperationContract(IsInitiating=true)]
    int AcquireVehicle();
    [OperationContract(IsTerminating=true)]
    void ReleaseVehicle();
    [OperationContract]
    void MoveTo(int location);
    [OperationContract]
    void Load();
    [OperationContract]
    void Unload();
}

客户端首先调用 AcquireVehicle 来建立会话(以及获取一个物理 AGV 进行操作)。现在,客户端可以对车辆执行任意数量的操作。车辆的拥有权本质上是通过会话管理来控制的!当然,我们知道“能力越大,责任越大”。一旦客户端调用 ReleaseVehicle,就不应该再有后续的请求。

关于之前的 TempSensor 代码的最后一点。你会注意到回调 TempChangedOperationContract 属性的 IsOneway 属性设置为 true。此属性定义了是否需要(或不需要)回复消息。即使返回值是 void,如果 IsOneway 属性设置为 false(默认值),仍然会生成回复消息。需要回复消息提供了一种将服务抛出的异常返回给客户端的机制。另外请注意,如果你将 IsOneway 设置为 true 并且你确实有返回值,你将得到一个异常。

现在,这是温度服务的其余代码

static void Main(string[] args)
{
    //Now start hosting the service
    TempSensor sensor = new TempSensor();
    Uri baseAddress = new Uri(
      ConfigurationManager.AppSettings["basePipeTempService"]);
    ServiceHost serviceHost = new ServiceHost(sensor, baseAddress);
    serviceHost.Open();
    Console.WriteLine("Service is running...." + 
                 "press any key to terminate.");
    Console.ReadKey();
    
    sensor.StopSim();

    serviceHost.Close();
}

上面唯一需要注意的是,我们创建了服务类对象,然后将它的引用传递给 ServiceHost

因此,温度服务提供了一个接口,允许客户端 订阅取消订阅 温度变化。在这种情况下,每当温度变化超过 5 度时,服务将向每个已订阅的客户端发送一条消息。订阅/取消订阅接口没有什么特别之处,你可以随意命名(事实上,如果你有不止一个客户端可以订阅的内容,你需要一个不同的名称),并且你可以传递参数。例如,你可能允许每个客户端指定它希望在什么粒度或温度级别获得通知。当然,服务在这种情况下需要做更多的工作,以跟踪哪个客户端想要什么。

你还会注意到定义了第二个接口。这是客户端需要实现的接口,以便接收来自服务的通知。当为客户端创建代理时,还将包含一个“回调”接口,以便客户端知道它们需要实现什么。这是服务的代理类。

[ServiceContract(CallbackContract=typeof(
                 ITempChangedPubCallback), Session=true)]
public interface ITempChangedPub
{
    [OperationContract]
    void Subscribe();
    
    [OperationContract]
    void Unsubscribe();
}

public interface ITempChangedPubCallback
{
    [OperationContract]
    void TempChanged(int newTemp);
}

public interface ITempChangedPubChannel : ITempChangedPub, 
                       System.ServiceModel.IClientChannel
{
}

public partial class TempChangedPubProxy : 
       System.ServiceModel.DuplexClientBase<ITempChangedPub>, 
       ITempChangedPub
{
    public TempChangedPubProxy(
           System.ServiceModel.InstanceContext callbackInstance) : 
           base(callbackInstance)
    {
    }
    
    public void Subscribe()
    {
        base.InnerProxy.Subscribe();
    }
    
    public void Unsubscribe()
    {
        base.InnerProxy.Unsubscribe();
    }
}

因此,温度服务客户端比 LocalTimeService 的客户端要复杂一点。TempChanged 客户端需要做两件事。首先,它必须实例化一个实现了 ITempChangedPubCallback 接口的类,以便服务可以将消息发送回客户端。另一件事是客户端需要提供某种回调宿主,类似于 ServiceHost 为服务提供的功能。这是消息如何被检测并路由到正确位置的方式。提供该功能的类是 InstanceContext 类(如果你查看类定义,你会注意到它有一个 host 成员)。当我们创建 InstanceContext 时,我们将它传递给将服务温度变化消息的处理程序类(已实现 ItempChangedPubCallback)。

class TempChangedHandler : ITempChangedPubCallback
{
    public void TempChanged(int temp)
    {
        Console.WriteLine(temp.ToString());
    }
}
static void Main(string[] args)
{
    InstanceContext siteTempChangedHandler = null;
    TempChangedPubProxy proxyTempChanged;

    siteTempChangedHandler = new InstanceContext(new TempChangedHandler());
    proxyTempChanged = new TempChangedPubProxy(siteTempChangedHandler);
    proxyTempChanged.Subscribe();

    Console.WriteLine("Client is running....press any key to terminate.");
    Console.ReadKey();
    
    proxyTempChanged.Unsubscribe();
}

这就是代码的全部内容,除了配置文件中的终结点定义。在这里,我们需要同时指定“客户端”终结点(我们正在与之通信的服务)以及客户端回调的终结点定义。

<configuration>
  <appSettings>
    <add key="basePipeTempChangedHandler" 
       value="net.pipe:///TempSensorHandler" />
  </appSettings>
  <system.serviceModel>
    <services>
      <service 
      name="TempSensorHandler">
        <!-- use base address provided by host -->
        <endpoint name="pipeEndpoint" address=""
                  binding="netNamedPipeBinding"
                  contract="ITempChangedPubCallback" />
      </service>
    </services>
    <client>
      <endpoint name ="TempChangedPub"
                address="net.pipe:///TempSensor"
                binding="netNamedPipeBinding"
                contract="ITempChangedPub" />
    </client>
  </system.serviceModel>
</configuration>

启动服务,然后启动一个客户端实例来测试代码。我很好奇温度是否能随着时间的推移保持在 100 左右。

异步性

有时,服务需要提供的功能是一个漫长的过程,客户端无法等待响应。在这种情况下,有两种不同的方法可以提供对服务的异步访问。第一种基于 .NET 异步模式,其中断开连接由客户端基础结构提供。第二种方法使用双工通信模式,其中客户端实现回调接口。让我们分别看一下这两种方法。

考虑前面描述的 AGV 应用程序。AGV 是相对系统处理时间而言非常慢的车辆。它们最多以每秒 1 英尺的速度行驶。所以,要行驶 10 英尺,需要 10 秒。此外,加速、减速和转弯延迟也会影响总时间。因此,当客户端发送 MoveTo 请求时,它需要等待一段时间才能收到操作已完成的响应。

对于异步客户端,服务方面不需要做任何特别的事情。所有功能都在客户端方面提供。所以我们将从实现前面描述的 IAGVControl 接口的服务开始。这是我们将称之为 AGVController 的类的代码。

[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class AGVControl : IAGVControl
{
    public int AcquireVehicle()
    {
        int vehicleID = 0;
        return vehicleID;
    }
    public void ReleaseVehicle()
    {
        //place vehicle back in the pool
    }
    public void MoveTo(int location)
    {
        //This takes a long time...
        Thread.Sleep(5000);
    }
    public void Load()
    {
        //load it
    }
    public void Unload()
    {
        //unload it
    }
}

如你所见,这里(字面上)什么都没有。除了 MoveTo 操作需要 5 秒才能完成。因此,任何调用 MoveTo 的方法都不会立即返回。该类的实例化行为被指定为 PerSession。这意味着,ServiceHost 将为每个客户端会话创建一个 AGVControl 实例。并且会话生命周期是使用 IsInitiating/IsTerminating 属性在接口中定义的。将 AGVControl 编译成服务 DLL,以便我们可以使用 svcutil 生成代理。我们将在构建异步客户端时描述代理生成。

像往常一样,我们需要为服务构建一个宿主。与之前的示例没有区别,所以在这里:

static void Main(string[] args)
{
    Uri baseAddress = 
        new Uri(ConfigurationManager.AppSettings["basePipeAGVCtlr"]);
    ServiceHost serviceHost = 
        new ServiceHost(typeof(AGVControl), baseAddress);
    serviceHost.Open();
    Console.WriteLine("Service is running....press any key to terminate.");
    Console.ReadKey();
    serviceHost.Close();
}
<configuration>
  <appSettings>
    <add key="basePipeAGVCtlr" value="net.pipe:///AGVControl" />
  </appSettings>
  <system.serviceModel>
    <services>
      <service name="AGVController.AGVControl">
        <endpoint 
           address="" 
           binding="netNamedPipeBinding" 
           contract="AGVController.IAGVControl" 
         />
      </service>
    </services>
  </system.serviceModel>
</configuration>

编译并启动服务以确保一切正常。现在,让我们将注意力转向所有魔法发生的地方——客户端。再次,我们需要做的第一件事是生成一个代理,以便客户端能够做任何事情。

svcutil 的选项之一是生成异步方法以匹配异步模式。本质上,这意味着为每个服务方法生成两个方法签名,一个用于 开始 异步操作,一个用于 结束。异步模式所需的另一项是回调方法,当异步操作完成时将调用该方法。那时我们将获得服务操作的结果。

因此,针对我们上面创建的 AGVControl 服务 DLL 执行 svcutil。这将生成我们之前看到的“xsd”和“wsdl”文件。然后,重新运行 svcutil,但这次,指定刚刚创建的 *.xsd*.wsdl 文件,包含“/a”选项。Svcutil 将生成一个代理,其中包含服务公开的每个方法的异步签名。

由于 svcutil 只是一个小的辅助工具,我决定对生成的文件进行一些编辑。我知道唯一耗时的方法是 MoveTo 方法(我知道这一点是因为我编写了延迟代码)。所以,我只想将 *该方法* 实现为异步操作,所有其他方法都保持同步调用。这是代理的编辑版本

[ServiceContract]
public interface IAGVControl
{
    [OperationContract]
    int AcquireVehicle();
    [OperationContract]
    void Load();

    [OperationContract(AsyncPattern = true)]
    System.IAsyncResult BeginMoveTo(int location, 
           System.AsyncCallback callback, object asyncState);
     void EndMoveTo(System.IAsyncResult result);
 
    [OperationContract]
    void ReleaseVehicle();
    [OperationContract]
    void Unload();
}

public interface IAGVControlChannel : 
       IAGVControl, System.ServiceModel.IClientChannel
{
}

public partial class AGVControlProxy : 
       System.ServiceModel.ClientBase<IAGVControl>, 
       IAGVControl
{
    public AGVControlProxy()
    {
    }
    public int AcquireVehicle()
    {
        return base.InnerProxy.AcquireVehicle();
    }
    public void Load()
    {
        base.InnerProxy.Load();
    }
    public System.IAsyncResult BeginMoveTo(int location, 
           System.AsyncCallback callback, object asyncState)
    {
        return base.InnerProxy.BeginMoveTo(location, 
                               callback, asyncState);
    }
    public void EndMoveTo(System.IAsyncResult result)
    {
        base.InnerProxy.EndMoveTo(result);
    }
    public void ReleaseVehicle()
    {
        base.InnerProxy.ReleaseVehicle();
    }
    public void Unload()
    {
        base.InnerProxy.Unload();
    }
}

你会注意到 MoveTo 操作已被两个方法取代,BeginMoveToEndMoveToBeginMoveTo 方法还添加了几个额外的参数(除了为 MoveTo 定义的位置参数)。这些是支持异步模式所必需的。第一个参数是一个回调委托,我们将在此指定操作完成后需要调用哪个方法。第二个参数是状态参数,通常传递代理对象,以便可以调用 end 操作。

好的,我们已准备好构建一个将测试 AGVControl 服务的客户端。这次,我们将构建一个 Windows Forms 客户端,以便 UI 更符合我们的需求。下图显示了在下载中包含的示例应用程序。我们仅使用 shell 应用程序来演示异步操作,因此包含的代码不多。

agvclient_screen.jpg

我们在这里要展示的是,MoveTo 操作与正在服务中进行的实际处理是断开连接的。为了证明这一点,客户端代码创建了一个单独的线程,该线程将在服务进行操作并且我们正在等待响应时更新状态栏(完整源代码在第一部分的下载中)。

public partial class Form1 : Form
{
    AGVControlProxy proxy = null;
    bool gotVehicle = false;
    bool gotResponse = false;
    ...
    private void btnMoveTo_Click(object sender, EventArgs e)
    {
        if (txtPosition.Text.Length > 0)
        {
            int position = System.Convert.ToInt32(txtPosition.Text);
            proxy.BeginMoveTo(position, MoveCallback, proxy);
            //Start a thread to display activity
            gotResponse = false;
            Thread thread = new Thread(new ThreadStart(WaitingThread));
            thread.Start();
        }
    }
    private void MoveCallback(IAsyncResult ar)
    {
        ((AGVControlProxy)ar.AsyncState).EndMoveTo(ar);
        gotResponse = true;
    }
    private void WaitingThread()
    {
        int waitCount = 1;
        while (gotResponse == false)
        {
            toolStripStatusLabel1.Text = 
                "Waiting..." + waitCount.ToString();
            waitCount++;
            Thread.Sleep(100);
        }
        toolStripStatusLabel1.Text = "Got response";
    }
    ...
}

编译客户端,并确保服务应用程序正在运行。按下 Acquire 按钮,然后按下 MoveTo 按钮。你会看到状态栏正在更新,直到收到服务响应。回调方法控制线程的终止,因此你也知道何时发生。

现在就到这里。在第三部分(也是最后一部分!)中,我们将完成异步示例,实现与上面相同的功能,但利用双工通信模式。我们还将研究第四种传输类型——消息队列ing,以及它可能在何处使用。

© . All rights reserved.