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

Silver Draw - 一个基于 Silverlight 的协作白板,带绘图和聊天

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (36投票s)

2009年11月1日

CPOL

9分钟阅读

viewsIcon

144582

downloadIcon

3166

Silver Draw 展示了如何使用 Silverlight 和 WCF 轮询双工服务来创建实时协作应用程序。

引言

Silverlight + WCF 轮询双工服务 = 惊人的效果。在本文中,我将展示如何使用 Silverlight + 轮询双工创建一个实时白板,该白板可以与多个参与者同步信息。Silverlight + WCF 轮询双工服务使您能够编写 Silverlight 应用程序,这些应用程序可以几乎实时地与用户通过 HTTP 共享信息。

用户可以在白板上一起绘画,并且可以互相聊天。这是客户端在两个不同浏览器中运行的快速截图。如果您更感兴趣,请在我的博客观看此应用程序的视频

该应用程序包含一个登录面板,几种绘图工具(铅笔、钢笔和画笔),一个绘图画布,一个聊天区域,一个用于选择线条和填充颜色的拾色器,以及一个用于查看会话中用户活动的通知区域。

内部实现简化

不用说,本文的目的是解释如何与 Silverlight 和 WCF 一起实现轮询双工。该应用程序有两个部分:

  1. 服务器 - 一个 WCF 服务,具有用于客户端注册、发布信息和接收通知的轮询双工终结点。
  2. 客户端 - 一个 Silverlight 客户端,它消耗该终结点来注册用户、发布用户的绘图,以及接收和绘制来自其他用户的绘图数据。

服务器端 Web 服务接口 (IDuplexDrawService) 具有这些重要方法:

  • Register - 客户端可以调用此方法来注册自己加入对话。
  • Draw - 客户端可以调用此方法请求服务器将绘图信息推送到对话中的其他客户端。
  • NotifyClients - 从 Draw 方法中,我们将调用 NotifyClients 来迭代已注册的客户端,以发布数据。

此外,由于我们使用的是轮询双工,因此还需要一个回调接口,以便服务器可以“回调”或通知客户端。(在轮询双工中,您可能需要注意,实际发生的并不是服务器到客户端的直接回调。客户端应定期通过 HTTP 轮询服务器,以获取服务器愿意传递给客户端的任何信息,从而模拟回调。)无论如何,我们的“回调”服务接口 (IDuplexDrawCallback) 包含一个 Notify 方法,供服务器在某个客户端调用 Draw 方法时通知其他客户端。

因此,简而言之——一方可以使用“Draw”发布一些信息,而其他人可以订阅发布的信息。这是发布/订阅模式的一个简单实现。

当您第一次加载客户端时,系统会要求您提供一个用户名来加入会话。从那里开始,逻辑大致如下:

在客户端

  • 客户端将尝试连接到服务终结点。
  • 如果成功,客户端将通过调用 Register 方法并传入用户名来向自身注册。
  • 客户端将在代理中挂钩几个事件处理程序。主要是 NotifyCompleted 事件。每次从服务器收到回调时,都会触发 NotifyCompleted

在服务器端

  • Register 方法内部,服务器将从当前操作上下文中获取客户端的会话 ID 和回调通道,并将其添加到列表中。
  • 每当客户端通过调用“Draw”提交信息时,服务器将通过迭代每个已注册的客户端来推送此信息,并调用其“Notify”方法。

服务器实现

让我们快速看一下服务器端实现是如何完成的。首先,您需要将 System.ServiceModel.PollingDuplexService.dll 的引用添加到 Silverlight SDK 的服务器文件夹中。对我来说,这个路径是:C:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Server\

服务和回调接口

现在,让我们为我们的服务创建一个简单的接口——IDuplexDrawService

[ServiceContract (CallbackContract = typeof(IDuplexDrawCallback))]
public interface IDuplexDrawService
{
    [OperationContract(IsOneWay = true)]
    void Register(string name);
    [OperationContract(IsOneWay = true)]
    void Draw(string data);
}

您可能会注意到,我们将 CallbackContract 属性指定为回调接口的类型。回调接口看起来像这样:

[ServiceContract]
public interface IDuplexDrawCallback
{
    [OperationContract(IsOneWay = true, 
          AsyncPattern = true, Action = DrawData.DrawAction)]
    IAsyncResult BeginNotify(Message message, 
           AsyncCallback callback, object state);
    void EndNotify(IAsyncResult result);
}

除了将我们的 Notify 方法指定为两部分以支持异步调用之外,那里没有什么特别之处。稍后,我们将从自定义对象创建一个消息,并将其作为 BeginNotify 的第一个参数传递。我们也应该指定一个消息契约。

[MessageContract]
public class DrawData
{
    public const string DrawAction = 
               "http://amazedsaint.net/SilverPaint/draw";

    [MessageBodyMember]
    public string Content { get; set; }

    [MessageBodyMember]
    public string From { get; set; }
}

好了,现在我们有了所有组件,让我们创建具体的服务。此时,您可能需要参考 DuplexDraw 服务源代码进行对照。首先,请注意我们为服务指定了一些属性。

[ServiceBehavior
        (ConcurrencyMode = ConcurrencyMode.Multiple, 
        InstanceContextMode = InstanceContextMode.Single)]
public class DuplexDrawService : IDuplexDrawService
{
   //Code here
}

InstanceContextMode 设置为 Single,这意味着只有一个实例上下文用于处理所有传入的客户端调用。此外,ConcurrencyMode 设置为 Multiple,因此服务实例将是多线程的(我们最终会进行一些显式锁定)。

我们可能不会遍历服务中的所有方法,但其核心思想如下。如前所述,创建连接后,客户端应调用 Register 方法。在服务器的 Register 方法内部,我们将获取回调通道并将其添加到字典中,以会话 ID 作为键。

string sessionId = OperationContext.Current.Channel.SessionId;
var callback = 
    OperationContext.Current.GetCallbackChannel   
                            <IDuplexDrawCallback>();

lock (syncRoot)
{
    clients[sessionId] = callback;
    userNames[sessionId] = name;
}

Draw 和 NotifyClients

服务中的 DrawNotifyClients 方法是不言自明的。当某个客户端调用 Draw 方法时,我们将迭代我们拥有的字典,将数据发布给所有订阅的客户端。这是 DrawNotifyClients 方法中发生的事情的精髓。

Draw 方法内部,我们获取传入的数据以通知已连接的客户端。

/// A client will call this, to publish the drawing data
public void Draw(string data)
{
    lock (this.syncRoot)
    {
        string sessionId = 
          OperationContext.Current.Channel.SessionId;
        if (userNames.ContainsKey(sessionId))
            NotifyClients("@draw:" + data, userNames
           [sessionId],sessionId);
    }
}

在服务中的 NotifyClients 方法内部,我们实际上是将数据推送到各个客户端。NotifyClients 方法如下。我们从 DrawData 创建一个消息缓冲区。

//In the actual code, this is a global static variable
static TypedMessageConverter messageConverter = 
TypedMessageConverter.Create(
  typeof(DrawData),
  DrawData.DrawAction,
  "http://schemas.datacontract.org/2004/07/SilverPaintService");


/// Send the notification to all clients
public void NotifyClients(string data,string from,string sessionId)
{
    MessageBuffer notificationMessageBuffer = 
        messageConverter.ToMessage
        (new DrawData 
      { Content = data, From = from }).CreateBufferedCopy
        (65536);
    foreach (var client in clients.Values)
    {
        try
        {
           client.BeginNotify
               (notificationMessageBuffer.CreateMessage(), 
               onNotifyCompleted, client);
        }
        catch
        {}
    }
}

如果您想知道 data 变量中有什么,它是一个 JSON 序列化字符串,其中包含绘图板信息。当我们遍历客户端部分时,我们将看到更多关于它的内容。

终结点配置

最后,关于配置终结点,简单说几句。查看服务器 web.config 中的 system.ServiceModel 部分。特别是,查看 extensions 部分,我们在其中添加了一个名为 pollingDuplex 的新绑定扩展。然后,我们需要创建一个自定义绑定类型 (customBinding) 并将其指定为我们终结点的绑定。

<extensions>
    <bindingElementExtensions>
      <add name="pollingDuplex" 
        type="System.ServiceModel.Configuration.PollingDuplexElement, 
              System.ServiceModel.PollingDuplex"/>
    </bindingElementExtensions>
    <bindingExtensions>
      <add name="pollingDuplex" 
        type="System.ServiceModel.Configuration.PollingDuplexElement, 
              System.ServiceModel.PollingDuplex"/>
    </bindingExtensions>
  </extensions>
  <behaviors>
    <serviceBehaviors>
      <behavior
         name="SilverdrawServiceBehaviour">
        <serviceMetadata httpGetEnabled="true"/>
        <serviceDebug 
            includeExceptionDetailInFaults="true"/>
        <serviceThrottling maxConcurrentSessions
                 ="2147483647"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <bindings>
    <customBinding>
      <binding name="SilverdrawServiceBinding">
        <binaryMessageEncoding />
        <pollingDuplex 
          inactivityTimeout="02:00:00" 
          serverPollTimeout="00:05:00"
          maxPendingMessagesPerSession="2147483647" 
          maxPendingSessions="2147483647" />
        <httpTransport />
      </binding>
    </customBinding>
  </bindings>
  <services>
    <service behaviorConfiguration="SilverdrawServiceBehaviour" 
         name="Silverdraw.Server.DuplexDrawService">
      <endpoint address="" 
         binding="customBinding"
         bindingConfiguration="SilverdrawServiceBinding"
         contract="Silverdraw.Server.IDuplexDrawService"/>
      <endpoint address="mex" 
         binding="mexHttpBinding" 
         contract="IMetadataExchange"/>
    </service>
  </services>

现在,让我们检查一下客户端部分有什么。

Silverlight 客户端

您需要将 System.ServiceModel.PollingDuplexService.dll 的引用添加到 Silverlight SDK 的客户端文件夹中。对我来说,这个路径是:C:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Client\

连接到服务器

在客户端,大部分工作都在 DuplexClientHelper 类中完成。首先,我们需要添加一个服务引用,并为我们创建的服务创建一个代理。

DuplexClientHelper 是对服务生成的代理的薄包装。

当 Silverlight 控件加载时,我们将显示一个快速登录面板,并通过 Page.xaml.cs 中的 InitServiceConnection 方法,从那里的按钮点击事件调用 DuplexClientHelper.cs 中的 Initialize 方法。

在那里,我们需要创建一个新的客户端实例,并挂钩事件以接收通知。

public void Initialize(string endPointAddress)
{
    this.client = new Proxy.DuplexDrawServiceClient(
    new PollingDuplexHttpBinding(),
    new EndpointAddress(endPointAddress));

    this.client.NotifyReceived += new 
        EventHandler<proxy.notifyreceivedeventargs>
                (client_NotifyReceived);
}

正如您可能猜到的,每当服务器向客户端推送消息时,都会调用 NotifyReceived。收到通知后,我们可以从请求中获取 DrawingData。这是一个简化的事件处理程序版本:

/// Callback to get the notification
void client_NotifyReceived
   (object sender, Proxy.NotifyReceivedEventArgs e)
{
    var data = e.request.GetBody<drawdata>();
}

绘图逻辑

大部分绘图逻辑在我们的 Silverlight 客户端的 DrawingArea.csPage.xaml.cs 文件中。大部分代码是不言自明的——我们使用传统的方式绘图——在鼠标按下时将一个布尔标志设置为 true,在鼠标移动时绘制元素。

形状被绘制到画布上,具体取决于选择的工具(画笔、钢笔等),通过在 Page.xaml.cs 的鼠标按下事件中调用 DrawingArea.cs 中的几个方法来实现。

请记住——当我们向画布添加形状时,我们需要将此信息发布给其他客户端,对吧?为此,我们在 DrawingArea 类中有一个临时形状列表——我们在其中保留添加到本地画布的形状。当我们临时列表中的形状数量达到特定计数时,我们将形状序列化为 JSON 字符串,并使用前面描述的 Draw 方法将其传递给服务器进行发布。

您可能还注意到,在进行实际序列化之前,我们将形状转换为可序列化的对象模型(参见 ScreenObject.cs)。在 CanvasHelper.cs 文件中包含将形状与 ScreenObject 类型进行双向转换的方法。

我们如何将其转换为 JSON?我们在 JsonSerializerHelper.cs 文件中提供了一些扩展方法。另外,我在这里有一篇博文,介绍了这些扩展方法以及 JSON 的工作原理。

未来构想

源代码在 CodePlex 上可用——加入并贡献以发展应用程序。以下是一些想法:

  • 登录时的注册和用户身份验证(可能使用 ASP.NET membership providers)
  • 使用 MEF 动态加载形状
  • 将应用程序发展成为一个功能齐全的原型工具——功能类似于 balsamiq.com
  • 多个绘图房间 + 用户可以应邀加入房间
  • 持久化消息来回绘制——这样我们就可以像 Google Wave 一样重播活动,并在连接时将迄今为止的绘图数据推送到用户

结论

归功于——这个想法本身是在阅读了 Tomek on Software 博客上的这篇帖子后产生的,并且该帖子的源代码被用作主要参考。绘图应用程序中的拾色器控件来自 designerwpf.com。另外,感谢我的妻子让我花了一个完整的周六周日来完成这个。哈哈!

还有几点——当然,要构建一个可扩展的架构以在商业应用程序中使用它需要付出更多的努力。实时信息共享并非新鲜事——但 Silverlight + WCF 轮询双工是“通过 HTTP”实现的——这使得它对更广泛的受众可用。我在这里有一些关于此的更多想法

最后,订阅我的博客或将其添加到书签——获取更多内容:http://amazedsaint.blogspot.com

© . All rights reserved.