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

WCF 4.5 有什么新功能?WebSocket 支持(第 2 部分,共 2 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2012年3月6日

CPOL

7分钟阅读

viewsIcon

49243

WebSocket 支持( 使用纯文本消息), 可实现 Web 浏览器和 WCF 之间的交互

WCF 4.5 系列的第二篇文章。第 1 部分介绍了对基于 SOAP 消息的 WebSocket 支持。第 2 部分介绍对纯文本消息的 WebSocket 支持,从而实现了 Web 浏览器和 WCF 之间的交互。

之前的文章

  1. WCF 4.5 有什么新内容?让我们从 WCF 配置开始
  2. WCF 4.5 有什么新内容?单个 WSDL 文件
  3. WCF 4.5 有什么新内容?配置文件中的工具提示和智能感知
  4. WCF 4.5 有什么新内容?配置验证
  5. WCF 4.5 有什么新内容?IIS 中单个端点支持多种身份验证
  6. WCF 4.5 有什么新内容?IIS 的自动 HTTPS 端点
  7. WCF 4.5 中的新功能? BasicHttpsBinding
  8. WCF 4.5 的新特性?ASP.NET 兼容模式的默认值已更改
  9. WCF 4.5 的新特性?改进了 IIS 宿主中的流式传输
  10. WCF 4.5 有什么新功能?UDP 传输支持
  11. WCF 4.5 有什么新功能?WebSocket 支持(第 1 部分,共 2 部分)

如果您还没有阅读第 1 部分,请先阅读它,以便您能了解 WebSockets、NetHttpBinding 以及它们在 WCF 中的用法。

在第 1 部分中,我演示了如何创建二进制编码的 SOAP 绑定和文本编码的 SOAP 绑定与 WebSockets。问题在于,在 JavaScript 中创建和解析 SOAP 消息可能会很困难——这就是为什么我们在从 JavaScript 调用 WCF 服务时,倾向于使用基于 XML/JSON 的绑定(如 WebHttpBinding),而不是基于 SOAP 的绑定(BasicHttpBinding/WsHttpBinding)。

使用 WebSocketsNetHttpBinding 和纯文本消息创建双工服务,就像创建任何其他 WCF 服务一样。

  1. 定义契约和回调契约
  2. 实现服务
  3. 配置主机
  4. 从客户端应用程序消耗服务

首先,我们将创建我们的契约。由于我们需要接收和发送消息,我们将创建一个双工契约,每个契约只有一个方法,我们将使用 action=”*” 进行标记。

合同
[ServiceContract]
public interface IWebSocketEchoCallback
{        
    [OperationContract(IsOneWay = true, Action = "*")]        
    void Send(Message message);
}

[ServiceContract(CallbackContract = typeof(IWebSocketEchoCallback))]
public interface IWebSocketEcho
{
    [OperationContract(IsOneWay = true, Action = "*")]
    void Receive(Message message);
}

回显服务本身是一个简单的实现,它接收消息并将其回显给客户端。

EchoService
public class EchoService : IWebSocketEcho
{
    IWebSocketEchoCallback _callback = null;

    public EchoService()
    {
        _callback =
            OperationContext.Current.GetCallbackChannel<IWebSocketEchoCallback>();
    }
    public void Receive(Message message)
    {
        if (message == null)
        {
            throw new ArgumentNullException("message");
        }

        WebSocketMessageProperty property = 
                (WebSocketMessageProperty)message.Properties["WebSocketMessageProperty"];
        WebSocketContext context = property.WebSocketContext;
        var queryParameters = HttpUtility.ParseQueryString(context.RequestUri.Query);
        string content = string.Empty;

        if (!message.IsEmpty)
        {
            byte[] body = message.GetBody<byte[]>();
            content = Encoding.UTF8.GetString(body);                
        }

        // Do something with the content/queryParams
        // ...
            
        string str = null;
        if (string.IsNullOrEmpty(content)) // Connection open message
        {
            str = "Opening connection from user " + 
                    queryParameters["Name"].ToString();                
        }
        else // Message received from client
        {
            str = "Received message: " + content;                
        }

        _callback.Send(CreateMessage(str));            
    }

    private Message CreateMessage(string content)
    {
        Message message = ByteStreamMessage.CreateMessage(
                new ArraySegment<byte>(
                    Encoding.UTF8.GetBytes(content)));
        message.Properties["WebSocketMessageProperty"] =
                new WebSocketMessageProperty
                { MessageType = WebSocketMessageType.Text };
                        
        return message;
    }
}

Receive 方法处理两种类型的调用。

  1. 第一个“连接升级”消息——当客户端首次连接到服务并尝试将连接从 HTTP 升级到 WebSocket 时。在此调用中,请求使用 HTTP GET 发送,因此没有正文,但我们可以访问 URL 的查询字符串。
  2. 第二个及之后的都是客户端通过 WebSocket 传输发送的消息——这些消息包含一个消息正文,没有特殊的查询字符串。

第 5-9 行展示了如何通过将回调通道存储在局部变量中来创建标准双工服务。回调通道将在代码稍后用于将消息发送回客户端。该服务使用默认的实例模式 PerSession,因此每个客户端都会创建一个新实例,并且局部变量将在每个服务实例中指向不同的回调通道。

第 17-27 行演示了消息解析技术——通过检查其查询字符串或读取消息中的字节数组并将其转换为 string

第 32-43 行检查正在处理的消息类型,是第一个连接请求,还是客户端的后续消息。在每种情况下,服务都会通过回显消息来响应客户端。

第 46-56 行展示了如何使用字节流编码创建具有简单 string 内容的 Message 对象。

注意:要使用 ByteStreamMessage 类型,请添加对 System.ServiceModel.Channels 程序集的引用。

注意:WebSocket 消息可以是文本或二进制,因此如果您计划使用二进制消息,则需要更改代码以处理字节数组而不是 string

现在我们有了契约和服务,我们需要定义我们的主机和终结点。在此示例中,我将使用 IIS 作为主机,并使用 ASP.NET 的路由机制来创建不包含烦人的“.svc”扩展名的服务 URL 地址。下面的 global.asax 代码展示了如何做到这一点。

Global.Asax
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.Add(new ServiceRoute("echo",
            new ServiceHostFactory(),
            typeof(EchoService)));
    }
}

现在是终结点配置。由于 NetHttpBinding 使用 SOAP 消息,并且没有用于传递纯字节流的“WebSocketHttpBinding”,因此我们需要创建一个自定义绑定,该绑定允许我们通过 WebSocket 接收消息,其中消息可以是文本消息或二进制消息(WebSocket API 支持这两种类型)。

WCF 的标准编码——文本、二进制和 MTOM,将不允许我们接收非 SOAP 字节流,这就是为什么我们需要使用 WCF 4 中引入的新编码——ByteStreamMessageEncoding

以下终结点和绑定配置允许我们打开一个接收简单字节流的 WebSocket 侦听器。

服务配置
<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" 
                               multipleSiteBindingsEnabled="true" />
    <services>
      <service name="UsingWebSockets.EchoService">
        <endpoint address="" 
                  binding="customBinding" 
                  bindingConfiguration="webSocket"
                  contract="UsingWebSockets.IWebSocketEcho" />
      </service>
    </services>
    <bindings>
      <customBinding>
        <binding name="webSocket">          
          <byteStreamMessageEncoding/>            
          <httpTransport>            
            <webSocketSettings transportUsage="Always" 
                               createNotificationOnConnection="true"/>
          </httpTransport>
        </binding>
      </customBinding>
    </bindings>
</system.serviceModel>

配置中的重要部分是第 13-21 行。

  1. 我们将 transportUsage 设置为 Always,以强制使用 WebSocket 而不是 HTTP。
  2. 我们将 createNotificationOnConnection 设置为 true,以允许我们的 Receive 方法在接收到连接请求消息(发送到服务的第一个 GET 请求)时被调用。
  3. 我们使用 byteStreamMessageEncoding,它允许服务接收简单的字节流作为输入,而不是复杂的 SOAP 结构。

要测试我们的代码,我们可以向项目中添加一个 HTML 页面。以下代码基于 HTML5 Labs 网站StockTicker 演示。

Echo Client
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Echo Demo</title>
    <script src="Scripts/jquery-1.4.1.js" 
    type="text/javascript"></script>
    <script>

        $(document).ready(function () {
            if (!window.WebSocket && window.MozWebSocket) {
                window.WebSocket = window.MozWebSocket;
            }

            $('#echoForm').submit(function (event) {
                $('#echoForm')
                    .add('#echoForm > *')
                    .attr('disabled', 'disabled');

                var uri = 'ws://' + window.location.hostname +
                    window.location.pathname.replace('EchoDemo.html', 'echo') +
                    '?Name=' + $("#name").val();
                connect(uri);
                event.preventDefault();
            });
        });

        function connect(uri) {
            $('#messages').prepend('<div>Connecting...</div>');

            var websocket = new WebSocket(uri);
            
            websocket.onopen = function () {
                window.focus();
                $('#echoForm').hide();
                $('#outputArea').show();
                window.setInterval(function()
                {
                   websocket.send("the time is " + new Date());
                }, 1000);                
                $('#messages').html(
                    '<div>Connected. Waiting for messages...</div>');
            };

            websocket.onclose = function () {
                if (document.readyState == "complete") {
                    var warn = $('<div>').html(
                        'Connection lost. Refresh the page to start again.').
                        css('color', 'red');
                    $('#messages').append(warn);
                }
            };

            websocket.onmessage = function (event) {
                $("#messages").append(event.data + "<br>");
            };
        };

</script>
</head>
<body>
    <form id="echoForm" action="">
    <input type="text" id="name" 
    placeholder="type your name" />
    </form>
            <div id="outputArea" style="display: none">
        <div id="messages" style="height: 80%; overflow: hidden">
        </div>
    </div>
</body>
</html>

以上代码大部分是处理传入消息的 jQuery 代码,因此让我们指出重要部分。

第 18-20 行——在这些行中,我们创建了服务的 URI。请注意 ws:// 方案的使用——这是 WebSocket 的方案,但即使我们的服务基址设置为 HTTP,它也能正常工作。

第 27-56 行——connect 函数基本上完成了所有剩余工作。WebSocket 函数基于 WebSocket API

  • 第 30 行——创建 WebSocket 对象。
  • 第 32-42 行——打开连接。
  • 第 36-49 行——每隔 1 秒运行一个函数,将当前时间发送到服务。
  • 第 44-51 行——处理 WebSocket 通道关闭。
  • 第 53-55 行——处理接收到的消息(从服务发送到客户端的消息)。

运行客户端将显示以下输出:

image

总之,要创建一个可以接收来自浏览器的消息并向浏览器发送消息的 WCF 服务,我们需要做以下事情:

  1. 创建包含简单 ReceiveSend 方法(或任何其他您喜欢的名称)的双工契约。
  2. 像实现任何其他双工服务一样,在服务中实现契约。您唯一需要注意的就是如何读取和写入消息。
  3. 创建一个使用支持 WebSockets 和简单字节流编码的自定义绑定的终结点。

尽管上述方法效果很好,但还有另一种创建此类服务的方法——通过创建一个继承自 Microsoft.WebSockets.WebSocketService 类型的服务类。可在 NuGet 上找到的 Microsoft.WebSockets 程序包支持创建基于 WebSocket 的服务。一旦您的服务继承自 WebSocketService,您就可以重写 OnMessageOnOpenOnCloseOnError 等方法。使用这些方法非常简单,如下面的代码所示:

EchoService2
public class EchoService2 : 
     Microsoft.ServiceModel.WebSockets.WebSocketService
 {
     public override void OnMessage(string message)
     {
         string str = "Received message: " + message;
         Send(str);
     }
     
     public override void OnOpen()
     {
         var queryParameters = this.QueryParameters;
         string str = "Opening connection from user " +
             queryParameters["Name"].ToString();

          Send(str);                        
     }

     protected override void OnClose()
     {
         base.OnClose();            
     }

     protected override void OnError()
     {
         base.OnError();            
     }
}

如您所见,在这种情况下,您不必直接处理字节数组。要托管此服务,您也无需定义特殊的终结点配置,因为此程序包包含一个 WebSocketHost 类,该类会自动创建和配置 WebSocket 终结点。要创建 WebSocketHost 并将其提供给 IIS,我们需要创建一个继承自 ServiceHostFactory 的类,如下面的代码所示:

WebSocketServiceHostFactory
public class WebSocketServiceHostFactory : ServiceHostFactory
{
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        var host = new WebSocketHost(serviceType, baseAddresses);
        host.AddWebSocketEndpoint();
        return host;
    }
}

注意ServiceHostFactory 声明在 System.ServiceModel.Activation 程序集中,因此不要忘记添加对它的引用。

有了新工厂后,我们可以将其注册到路由机制(第 5-7 行)。

Global.Asax
public class Global : System.Web.HttpApplication
{
      protected void Application_Start(object sender, EventArgs e)
      {
          RouteTable.Routes.Add(new ServiceRoute("echo2",
              new WebSocketServiceHostFactory(),
              typeof(EchoService2)));

          RouteTable.Routes.Add(new ServiceRoute("echo",
              new ServiceHostFactory(),
              typeof(EchoService)));
      }
}

剩下的就是更改客户端 HTML 代码中的第 19 行,使其调用“echo2”服务而不是“echo”。

您可以在 Paul Batum 的博客文章以及他的 //BUILD 会议中找到更多关于如何使用此程序包的示例。

因此,正如您所见,创建 WCF 服务以使用 WebSockets 从浏览器接收消息并将消息推送到浏览器非常容易。告别长轮询,我希望我们永远不再相见。 Smile

您可以从我的 SkyDrive 下载上述代码(两个版本)。源代码还包括一个示例自托管 WebSocket 服务和一个使用它的 HTML 页面,而不是 IIS 托管的服务。

© . All rights reserved.