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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (30投票s)

2007年11月11日

CPOL

7分钟阅读

viewsIcon

267506

downloadIcon

16194

使用 WCF netPeerTcp 绑定进行聊天,无需中央服务器。

Login Screen

Login Screen

引言

本文试图进一步推进 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

模板样式

此文件中的亮点是样式信息。看看我们如何更改控件模板来使我们的文本框、按钮和其他控件圆角化?…… 不错。

Login Screen

实现此目的的代码如下。值得注意的是 `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: 无法在证书存储中找到清单签名证书”编译错误。
© . All rights reserved.