第一次接触 WCF 命名管道传输(双工)






4.89/5 (10投票s)
引言
我曾使用 TCP 传输来实现我的项目。我的一些项目运行在不同的机器上,它们使用 WCF 进行通信。例如,我们需要从彭博的合法服务中获取市场数据,然后实时地将这些数据传输到安装在我同事电脑上的其他应用程序。在这种情况下,WCF TCP 传输是一个很好的选择。但我甚至在数据只需要在同一台电脑上传输时也使用 TCP 传输。事实上,它确实如我所愿地工作。但我忽略了另一个问题是,任何其他人都可以访问这台电脑的 WCF 服务。如果黑客想让我们的业务瘫痪,这可能会非常危险。我修改了一次 WCF 传输,发现了一种可能有助于逃避黑客攻击的方法。那就是 WCF 命名管道传输。
MSDN 的定义,何时使用命名管道传输?
- 命名管道是 Windows 操作系统内核中的一个对象,例如一段可供进程用于通信的共享内存。命名管道有一个名称,可用于同一台机器上进程之间的一路或双路通信。
- 当同一台计算机上的不同 WCF 应用程序之间需要通信,并且您想阻止任何来自另一台计算机的通信时,请使用命名管道传输。另一个限制是,从 Windows 远程桌面运行的进程可能仅限于同一个 Windows 远程桌面会话,除非它们具有提升的权限。
命名管道本身只是窗口内核中的一个对象。它允许不同的客户端访问一个共享资源。管道由其名称标识,可用于单向或双向通信场景。虽然可能只有一个管道服务器,但可能有一个以上的客户端应用程序使用管道。通过 NetNamedPipeBinding 类,WCF 提供了管道对象的抽象,使其非常容易用于进程间通信。我正在尝试编写一个示例。我遇到了很多问题。我查阅了几篇文章以及 Juval Löwy 撰写的书籍,找到了一些好文章:WCF 的通信选项 - 第 2 部分、WCF 示例:netNamedPipeBinding、WCF 命名管道的进程间双工通信。
示例
这是我正在编写的一个示例,该示例用于为同一台机器上运行的多个进程创建通信路由。(示例只能从 CodeProject 页面下载)
在示例中,它包含两个客户端 WPF 应用程序(Client1 和 Client2),一个主机 WPF 应用程序(WpfWcfNamedPipeBinding)。客户端可以发送消息给主机,主机也可以反过来发送消息给客户端。Client1 也可以通过主机发送消息给 Client2。
——————————-开发过程中,我遇到了一些不理解的问题————————————
- 我无法创建命名管道双工服务引用。客户端测试 WCF 说元数据包含一个无法解析的引用。一旦我从服务契约中删除了
CallbackContract
,它就能正常工作了。因此,我不得不自己创建客户端代理,而不是使用 Visual Studio。(SessionMode.Required
、CallbackContract
和InstanceContextMode.PerSession
)
————————————————————————————————————————————-
创建 Wcf 服务库 为此服务编写通信契约。
我想有一个双工契约,因为它更实用。例如,我们有一个服务器。Client1 向服务器请求某些数据。在客户端请求更多信息后,服务器有足够的时间检索这些信息,然后会从服务器调用回调函数。
MSDN:因此,双工服务可以向客户端终结点发送消息,提供类似事件的行为。当客户端连接到服务并向服务提供一个通道,服务可以在该通道上向客户端发送消息时,就会发生双工通信。请注意,双工服务的类似事件的行为仅在会话中有效。要创建双工契约,您需要创建一对接口,一个服务契约和一个回调契约。回调契约是定义服务可以在客户端终结点上调用的操作的接口。双工契约不需要会话,尽管系统提供的双工绑定会使用它们。要定义回调契约,ServiceContract
属性提供了类型为 Type 的 CallbackContract
属性。
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(INamedPipeBindingCallbackService))]
public interface INamedPipeBindingService
[ServiceContract]
public interface INamedPipeBindingCallbackService
在此示例中,服务使用PerSession 实例模式来维护每个会话的结果。因为服务器还需要在客户端不请求的情况下发送消息给客户端,所以服务器需要维护客户端的连接会话。这在服务契约的实现中设置。
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class NamedPipeBindingService : INamedPipeBindingService
下面是此示例中定义的完整服务契约接口。它包含一个服务契约和一个回调服务契约。在 OperationContract 属性中,我设置了IsOneWay = true,因为服务可能希望在契约操作执行期间调用传入的回调引用。但是,默认情况下不允许此类调用。默认情况下,服务类配置为单线程访问:服务实例上下文与锁相关联,一次只有一个线程可以拥有锁并访问该上下文内的服务实例。在操作调用期间调用客户端需要阻塞服务线程并调用回调。问题是,一旦回调返回,在同一通道上处理来自客户端的回复消息需要重新进入同一个上下文并协商同一锁的所有权,这将导致死锁。请注意,服务仍然可以调用其他客户端的回调或调用其他服务;只有回调给调用它的客户端才会导致死锁。将 IsOneWay 设置为 true 是避免死锁的一种方法。与另外两种方法相比,将 IsOneWay 设置为 true 可以在不释放锁的情况下将服务保持为单线程。将 IsOneWay 设置为 true 使服务即使在并发模式设置为单线程时也能调用回调,因为不会有回复消息争夺锁。
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(INamedPipeBindingCallbackService))]
public interface INamedPipeBindingService
{
[OperationContract(IsOneWay = true)]
void Message(CommunicatUnit composite);
#region Callback Connection Management
[OperationContract(IsOneWay = true)]
void Connect();
[OperationContract(IsOneWay = true)]
void Disconnect();
#endregion
}
[ServiceContract]
public interface INamedPipeBindingCallbackService
{
[OperationContract(IsOneWay = true)]
void Message(CommunicatUnit composite);
}
在服务契约中,有一个“回调连接管理”区域。这是因为 WCF 提供的回调机制,我们不得不自己设计应用程序级别的协议或一致的模式来管理连接的生命周期。只有当客户端通道仍然打开时,服务才能回调到客户端,这通常是通过不关闭代理来实现的。保持代理打开还将防止回调对象被垃圾回收。您可能总是希望在有会话的服务中添加 Connect()
和 Disconnect()
对作为一项功能,因为它允许客户端决定在会话期间何时开始或停止接收回调。
如果服务维护着对回调终结点的引用,并且客户端代理已关闭或客户端应用程序本身已不存在,当服务调用回调时,它将从服务通道收到 ObjectDisposedException
。因此,最好是让客户端在不再希望接收回调时或客户端应用程序即将关闭时通知服务。在 NamedPipeBindingService
中,InstanceContextMode = PerSession
,请阅读 NamedPipeBindingService
上的注释以获取更多信息。
创建服务契约后,就该实现契约了。您可以在示例的源代码中找到契约的实现。对于完整的命名管道绑定服务部分,我们剩下的工作就是定义服务终结点。
服务终结点,这总是我学习 WCF 的一个障碍,这次我决定阅读 Juval Löwy 的书中的所有关于终结点的内容。
- 每个终结点都必须具有三个元素:地址、契约和绑定,并且主机公开该终结点。从逻辑上讲,终结点是服务的接口,类似于 CLR 或 COM 接口。
- 服务上的所有终结点都具有唯一的地址,并且单个服务可以公开多个终结点。
- 使用完全限定的类型名称来指定服务和契约类型。
- 如果终结点提供基地址,则该地址方案必须与绑定一致,例如 HTTP 与 WSHttpBinding
- 管理配置(App.config)提供了灵活性,可以在不重新构建和重新部署服务的情况下更改服务地址、绑定,甚至公开的契约。
- 使用
bindingConfiguration
来启用事务传播。默认绑定会自动配置所有未显式引用绑定配置的终结点。每个绑定类型只有一个默认绑定配置。 - 对于 IPC 命名管道绑定,似乎不应定义端口,如“net.tcp//:10001/”
元数据交换终结点。发布服务元数据的两种选择:通过 HTTP-GET 发布元数据和使用专用终结点。通过 HTTP-GET 发布元数据仅仅是 WCF 的一项功能,不能保证您交互的其他平台会支持它。通过专用终结点发布元数据是更好的方法。
WCF 为 HTTP、HTTPS、TCP 和 IPC 协议提供了专用的绑定传输元素。无需启用 HTTP-GET 选项。主机实现服务的 MEX 终结点包括行为中的 serviceMetadata 标签。
<system.serviceModel> <bindings> <netNamedPipeBinding> <binding name="NamedPipeBinding_INamedPipeBindingService" closeTimeout="00:05:00" openTimeout="00:20:00" receiveTimeout="00:20:00" sendTimeout="00:20:00" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" maxConnections="10" maxBufferPoolSize="50000000" maxBufferSize="50000000" maxReceivedMessageSize="50000000"> <readerQuotas maxDepth="32" maxStringContentLength="50000000" maxArrayLength="50000000" maxBytesPerRead="50000000" maxNameTableCharCount="50000000" /> <security mode="Transport" /> </binding> </netNamedPipeBinding> </bindings> <services> <service name="WcfServiceLibraryNamedPipe.NamedPipeBindingService" behaviorConfiguration="MEX"> <endpoint address="net.pipe:///WcfServiceLibraryNamedPipe /NamedPipeBindingService" binding="netNamedPipeBinding" bindingConfiguration="NamedPipeBinding_INamedPipeBindingService" contract="WcfServiceLibraryNamedPipe.INamedPipeBindingService" /> <endpoint address="net.pipe:///WcfServiceLibraryNamedPipe/ NamedPipeBindingService/mex" binding="mexNamedPipeBinding" contract="IMetadataExchange" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name="MEX"> <serviceMetadata httpGetEnabled="False" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
宿主
一旦定义了服务契约和回调契约,我们就可以创建一个主机来使此命名管道服务工作。这里没有什么特别之处,这个示例使用 ServiceHost 类来创建和维护主机服务。只有一件事,这个示例在主机项目中重新实现了服务契约,所以请检查管理配置。在主机实现服务契约时,我们仍然设置InstanceContextMode = PerSession(如上所述,它指定了 InstanceContext
对象的生命周期,WCF 可以为每个会话创建一个新的 InstanceContext 对象,InstanceContext 管理通道与用户定义服务对象之间的关联)。
在契约服务实现中,它记录了每个客户端的回调通道引用。主机使用这些回调通道引用来向客户端重发消息。
客户端
我的示例没有通过 Visual Studio 生成代理,而是自己创建了客户端代理。因为我没有成功在我的客户端项目中添加服务引用。这个示例使用 DuplexChannelFactory
连接到主机。示例中的 Client1 使用回调服务契约的实现作为 DuplexChannelFactory
构造函数的第一个参数,示例中的 client2 使用 InstanceContext
作为参数。
它还需要为创建客户端和主机之间的双工通道定义终结点名称。但这比主机端更容易。我们只需要添加客户端终结点,以便程序找到其通道的入口。
<system.serviceModel>
<client>
<endpoint address="net.pipe:///WpfWcfNamedPipeBinding/NamedPipeBindingService"
binding="netNamedPipeBinding"
contract="WcfServiceLibraryNamedPipe.INamedPipeBindingService"
name="NamedPipeBindingServiceEndpoint" />
</client>
</system.serviceModel>
一旦基本概念清晰明了,剩下的工作就很简单了。请查阅示例的源代码以了解更多信息。
关注点
由于遇到了问题,我未能通过服务引用实现进程间通信。但我使用了另一种方法实现了 WCF 双工命名管道绑定进程间通信。
本文并未着重介绍我的示例,而是更多地探讨了基本 WCF 概念。我们可以通过 CodeProject 的示例轻松编写 WCF 应用程序。但我们应该为我们的项目负责!