WCF 通信选项 - 第 2 部分






4.85/5 (43投票s)
一篇关于 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 代码的最后一点。你会注意到回调 TempChanged
的 OperationContract
属性的 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
操作已被两个方法取代,BeginMoveTo
和 EndMoveTo
。BeginMoveTo
方法还添加了几个额外的参数(除了为 MoveTo
定义的位置参数)。这些是支持异步模式所必需的。第一个参数是一个回调委托,我们将在此指定操作完成后需要调用哪个方法。第二个参数是状态参数,通常传递代理对象,以便可以调用 end 操作。
好的,我们已准备好构建一个将测试 AGVControl 服务的客户端。这次,我们将构建一个 Windows Forms 客户端,以便 UI 更符合我们的需求。下图显示了在下载中包含的示例应用程序。我们仅使用 shell 应用程序来演示异步操作,因此包含的代码不多。
我们在这里要展示的是,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,以及它可能在何处使用。