WPF.WCF 聊天应用通过 P2P 简化






4.65/5 (30投票s)
使用 WCF netPeerTcp 绑定进行聊天,无需中央服务器。
引言
本文试图进一步推进 Sacha Barber 的工作,他在其精彩文章中演示了如何使用 WCF 创建一个基于 TCP 的聊天服务,并将输出连接到 WPF 前端。如果您尚未阅读这篇文章,我强烈建议您先阅读,以便更好地理解 WCF 传输和构建服务类。虽然 Sacha 非常出色地阐述了 WCF 的易用性,并且还展示了一种将服务连接到 WPF 的优雅模式,但我认识到有几个领域可以改进和/或简化。
具体来说,我打算展示以下内容
在 WCF 方面
- 如何通过 netPeerTcp 绑定使用 Peer Name Resolution Protocol (PNRP) 来创建一个真正无服务器的 P2P 聊天应用,代码量极少。Sacha 的解决方案使用了 netTcpBinding 绑定和一个单独的服务主机进程,所有客户端都需要连接到该进程,从而产生大多数客户端/服务器解决方案都存在的单点故障风险。
- 如何使用 WPF 窗口充当服务主机,从而无需单独的服务主机应用程序。
- 如何确定您的服务主机在对等节点网格中的在线或离线状态,因为 WCF 开箱即用地提供了此接口,尽管它们对在线/离线的定义与我们习惯的不同。
在 WPF 方面
- 如何使用样式模板来使文本框和富文本框圆角化。
- 如何在代码和 XAML 中触发故事板动画。
我不打算做什么
- 这不是一个生产应用程序,因此您不会找到很多 try/catch 块。当然,您的生产代码将是坚如磐石的。
- 由于我打算使用最少的代码(我很忙),您不会发现 Sacha 在其解决方案中展示的架构优雅。但是,您只会发现解决方案中只有一个项目,以及少量易于理解的代码,还有很多注释。
- 我不想重新发明轮子,所以不会教您 WCF/WPF 的所有基础知识;但我会链接到您可以学习本文中包含的所有技术的资源。
背景
Peer Name Resolution Protocol (PNRP)
如果您想了解 PNRP 的所有信息,请阅读以下内容
来自MSDN"为了连接到(对等)网格,对等节点需要其他节点的 IP 地址。这通过联系解析服务来实现。该服务接收一个网格 ID,并返回与注册了该特定网格 ID 的节点相对应的地址列表。解析器维护一个已注册地址列表,该列表是通过让网格中的每个节点向服务注册来创建的。PeerChannel 支持以下两种类型的解析器:Peer Name Resolution Protocol (PNRP) - 这是一个分布式、无服务器的名称解析服务,默认使用。PNRP 默认包含在 Windows Vista 中,也可以通过安装 Advanced Networking Pack 在 Windows XP SP2 上使用。任何运行相同版本 PNRP 的两个客户端都可以使用此协议相互定位,前提是满足某些条件(例如,没有中间的企业防火墙)。请注意,Windows Vista 附带的 PNRP 版本比 XP SP2 的 Advanced Networking Pack 中的版本要新。请查看 Microsoft 下载中心了解 XP SP2 上 PNRP 的更新。"
Windows Communication Foundation (WCF)
我能推荐的关于 WCF 的绝对最好的资源是通读 SDK。然后,下载并尝试示例……然后再读一遍。:-\
Windows Presentation Foundation (WPF)
我通过反复阅读 Adam Nathan 的《Windows Presentation Foundation Unleashed》并尝试所有示例来学习 WPF……以及进行大量的谷歌搜索。大量的谷歌搜索。
系统要求
我提供了源代码以及我的源代码的发布构建输出作为下载。源代码是使用 VS2008 B2 开发的;但是,您只需要 .NET 3.0 即可执行二进制文件。要在 Windows XP 上使用 PNRP,您需要 SP2 和此服务包。如果您已经在运行 Vista,二进制文件应该可以直接工作。
使用代码
如果您想构建/运行代码或执行二进制文件,请确保运行多个实例。这样,如果 P2P 网络中没有其他人,您就可以看到另一个运行的客户端可以聊天。如果您想更改到自己的私有网络,只需在 *app.config* 中将 `endpointaddress` 属性更改为您选择的网络名称即可。
由于此解决方案的简单性,只有三个文件需要查看
- Main.xaml - 包含所有表示信息,包括动画。
- Main.xaml.cs - 代码隐藏文件,其中包含服务定义和行为代码。
- App.config - 包含服务和绑定配置信息。
Main.xaml
模板样式
此文件中的亮点是样式信息。看看我们如何更改控件模板来使我们的文本框、按钮和其他控件圆角化?…… 不错。
实现此目的的代码如下。值得注意的是 `Border` 标签,它指定了 `CornerRadius` 为 10
<Style x:Key="roundedTextBox" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background"
Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush"
Value="{StaticResource TextBoxBorder}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border Name="Border" CornerRadius="10"
Padding="2" BorderThickness="1"
Background="#FFEBECC2">
<ScrollViewer
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
x:Name="PART_ContentHost"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Border"
Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
动画故事板
建议您使用 Microsoft Blend 等工具来创建复杂的动画,因为当您尝试动画更多的元素和属性时,XAML 可能会变得有些棘手。无论如何,您可以将 `Storyboard` 定义为窗口或应用程序资源,如下所示,我们将一个网格元素从 0% 的高度/宽度缩放到 100%,耗时 0.42 秒。
<Window.Resources>
<Storyboard x:Key="OnLoaded1">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="grdLogin"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.1940000" Value="1.2"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.4230000" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="grdLogin"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.1940000" Value="1.2"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.4230000" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
XAML 元素通过 `Storyboard.TargetName` 属性被动画目标化。命名故事板可以在 XAML 中触发
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource OnLoaded1}"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnConnect">
<BeginStoryboard x:Name="OnConnect_BeginStoryboard"
Storyboard="{StaticResource OnConnect}"/>
</EventTrigger>
</Window.Triggers>
……或在代码中
//start the HideConnectStatus storyboard which is a resource of this window
((Storyboard)this.Resources["HideConnectStatus"]).Begin(this);
超级简单。
Main.xaml.cs
编写 netPeerTcp 绑定服务应用程序的好起点是 .NET 3.0 SDK 附带的示例应用程序。在您查看完 SDK 和示例后,您就会发现创建一个 P2P 应用包含三个步骤。
创建服务契约
这是我的
//this is our simple service contract
[ServiceContract(Namespace = "http://rolandrodriguez.net.samples.wpfchat",
CallbackContract = typeof(IChat))]
public interface IChat
{
[OperationContract(IsOneWay = true)]
void Join(string Member);
[OperationContract(IsOneWay = true)]
void Chat(string Member, string Message);
[OperationContract(IsOneWay = true)]
void Whisper(string Member, string MemberTo, string Message);
[OperationContract(IsOneWay = true)]
void Leave(string Member);
[OperationContract(IsOneWay = true)]
void InitializeMesh();
[OperationContract(IsOneWay = true)]
void SynchronizeMemberList(string Member);
}
创建通道接口
`DuplexChannelFactory` 需要此接口来创建通道实例。通道接口只需继承您的服务契约接口和 `System.ServiceModel.IClientChannel`。无需定义任何其他方法。
//this channel interface provides a multiple
//inheritance adapter for our channel factory
//that aggregates the two interfaces need to create the channel
public interface IChatChannel : IChat, IClientChannel
{
}
创建服务主机
现在我们只需要一个类来托管我们的服务契约。在这种情况下,我们将使用 WPF 主窗口,如下所示。
public partial class WindowMain: IChat
{
...
}
连接到对等网络很简单,由以下注释掉的函数处理。
//this method gets called from a background thread to
//connect the service client to the p2p mesh specified
//by the binding info in the app.config
private void ConnectToMesh()
{
//since this window is the service behavior use it as the instance context
m_site = new InstanceContext(this);
//use the binding from the app.config with default settings
m_binding = new NetPeerTcpBinding("WPFChatBinding");
//create a new channel based off of our composite interface "IChatChannel" and the
//endpoint specified in the app.config
m_channelFactory = new DuplexChannelFactory<IChatChannel>(m_site, "WPFChatEndpoint");
m_participant = m_channelFactory.CreateChannel();
//the next 3 lines setup the event handlers for handling online/offline events
//in the MS P2P world, online/offline is defined as follows:
//Online: the client is connected to one or more peers in the mesh
//Offline: the client is all alone in the mesh
o_statusHandler = m_participant.GetProperty<IOnlineStatus>();
o_statusHandler.Online += new EventHandler(ostat_Online);
o_statusHandler.Offline += new EventHandler(ostat_Offline);
//this is an empty unhandled method on the service interface.
//why? because for some reason p2p clients don't try to connect to the mesh
//until the first service method call. so to facilitate connecting i call this method
//to get the ball rolling.
m_participant.InitializeMesh();
}
App.config
您会注意到上面的函数引用了 `WPFChatBinding` 和 `WPFChatEndPoint`。它们来自哪里?当然是配置文件。使用 PNRP 创建 P2P 应用就是如此简单。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<!-- chat instance participating in the mesh -->
<endpoint name="WPFChatEndpoint"
address="net.p2p://WPFChatMesh/rolandrodriguez.net/wpfchat"
binding="netPeerTcpBinding"
bindingConfiguration="WPFChatBinding"
contract="WPFChatViaP2P.IChat">
</endpoint>
</client>
<bindings>
<netPeerTcpBinding>
<binding name="WPFChatBinding" port="0">
<resolver mode="Auto"/>
<security mode="None"/>
</binding>
</netPeerTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
一些值得注意的地方
- 看到端点地址了吗?*net.p2p://WPFChatMesh/rolandrodriguez.net/wpfchat*。将其更改为您选择的唯一名称,您将拥有自己的私有网络,用于您自己的聊天室。通过与多个网络通信来创建其他房间或私人房间。
- 您会注意到在 `WPFChatBinding` 定义中,我们指定了一个端口 "0"。这只是告诉框架使用第一个可用端口,这样我们就无需显式指定一个。非常简单。
- `securitymode` 可以设置为 none、password 或 x.509 证书。
在线/离线状态呢?
您会注意到在 `ConnectToMesh` 函数中,`IChatChannel` 实例 `m_participant` 公开了一个 `System.ServiceModel.IOnlineStatus` 属性,并且我们为其附加了两个事件 - `Online` 和 `Offline`。
这两个事件并非指网络连接状态的在线/离线。在 PNRP 世界中,在线意味着我们至少与网络中的另一个对等节点建立了连接。如果我们是网络中唯一的节点,我们就被视为离线。这意味着即使您可以连接到互联网,但如果网络中没有其他人,您仍可能被列为离线。
//the next 3 lines setup the event handlers for handling online/offline events
//in the MS P2P world, online/offline is defined as follows:
//Online: the client is connected to one or more peers in the mesh
//Offline: the client is all alone in the mesh
o_statusHandler = m_participant.GetProperty<ionlinestatus>();
o_statusHandler.Online += new EventHandler(ostat_Online);
o_statusHandler.Offline += new EventHandler(ostat_Offline);
摘要
差不多就到这里了。再次,请查看Sacha 的文章,了解一种利用 TCP 传输和 WPF 的更优雅架构的解决方案。但是,他的解决方案需要使用 TCP 服务地址,而正如我们所见,在 netPeerTcp 绑定的世界中并不需要它。我很有兴趣看看大家了解如何使用 WCF 和 WPF 创建真正的无服务器 P2P 应用后会做出什么。此外,如果您认为这篇文章有价值,请登录 CodeProject 并投票。感谢您的阅读。
历史
- 2007/11/11 - 更新源代码以修复“MSB3323: 无法在证书存储中找到清单签名证书”编译错误。