文章
姚 鑫 · 六月 3 阅读大约需 10 分钟

第十五章 Caché WebSocket

第十五章 Caché WebSocket

使用WebSockets (RFC 6455)

web是围绕请求/响应范例构建的:客户机向服务器发送请求,服务器通过向客户机发送响应进行响应。此范式和HTTP本身不允许此通信协议的反向形式,即服务器与客户机启动请求/响应周期。已经开发了许多技术来解决了这个问题,即服务器可以启动与客户机的对话。这些技术通常被称为基于推送或 comet-based的技术,它们都存在不适合在web基础设施上进行全面部署的问题。目前使用的三种主要技术如下所述。

Short Polling 短轮询

使用这种技术,客户端定期发送HTTP请求来检测服务器状态的变化,服务器被编程为立即响应。空响应表示没有变化。

问题:
- 轮询频率(和响应能力)受到客户机可以容忍的刷新延迟的限制。
- 每个请求都是一个完整的HTTP请求/响应往返过程,这会导致大量的HTTP流量,而这又会给服务器和网络基础设施带来无法接受的负担
- 每个消息交换都承载着HTTP协议的开销,如果消息大小超过了最大传输单元(MTU)(通常是以太网的1500字节),则会特别繁重。

Long Polling 长轮询

使用这种技术,客户端发送HTTP请求,但服务器只在需要通知客户端更改时才响应。客户端通常在服务器发送响应消息时发送另一个“长轮询”请求。

问题:
- 每个请求都是完整的HTTP请求/响应往返,尽管这种技术涉及的HTTP通信量比短轮询少。
- 还有维护持久连接的负担。
- 每个消息交换都带有HTTP协议的开销。
- 超时可能会对该技术的成功产生不利影响。

HTTP Streaming HTTP流

这种技术利用了HTTP协议在客户端和服务器之间保持持久(或“KeepAlive”)连接的能力。客户端发送一个HTTP请求,该请求永久保持打开状态,只有在需要通知客户端更改时,服务器才会响应。服务器在发送响应消息后不终止连接,客户机等待来自服务器的下一条消息(或向服务器发送自己的消息)。

问题:
- 整个客户机/服务器交换是在一个HTTP请求/响应往返过程中构建的,并不是所有服务器都支持这种方式。
- 这种技术的成功可能会受到代理和网关等中介行为的不利影响。(曾经手机上设置代理IP就不能正常访问请求)
- 任何一方都没有义务立即向另一方提交部分回复。
- 客户端缓冲方案可能会对该技术产生不利影响。
- 超时可能会对该技术产生负面影响。

WebSockets协议

WebSockets协议(RFC 6455)通过在客户端和服务器之间提供一个全双工的面向消息的通信通道,解决了允许服务器主动将消息推送到客户端的基本需求。该协议被设计为在客户端和服务器之间已经建立的标准TCP通道上操作,因此是安全的。换句话说,已经使用的通道支持web浏览器和web服务器之间的HTTP协议。

WebSockets协议及其API由W3C标准化,客户端部分包含在HTML 5中。

中介体(如代理和防火墙)应该设置成知道(并支持)WebSockets协议。

浏览器支持

在为WebSockets协议创建最终标准的过程中,已经进行了几次迭代,每一次都有不同程度的浏览器支持。历史概述如下。
- Hixie-75:
1. Chrome 4.0+5.0, Safari 5.0.0
- HyBi-00/Hixie-76:
1. Chrome 6.0-13.0, Safari 5.0.2+5.1, Firefox 4.0 (disabled), Opera 11 (disabled)
- HyBi-07+:
1. Chrome 14.0, Firefox 6.0, IE 9 (via Silverlight extension)
- HyBi-10:
1. Chrome 14.0+15.0, Firefox 7.0+8.0+9.0+10.0, IE 10 (via Windows 8 developer preview)
- HyBi-17/RFC 6455
1. Chrome 16
2. Safari 6
3. Firefox 11
4. Opera 12.10/Opera Mobile 12.1
5. IE 10
最后突出显示的部分对于开发可移植web应用程序是最重要的。

服务器的支持

可以说,面向服务器的基于javascriptNode.js技术提供了最复杂、目前最成熟的WebSockets协议实现。WebSockets一直与Node.js紧密联系在一起。但是,其他web服务器技术正在迅速赶上来,所有主要web服务器的最新版本现在都提供了WebSockets支持,如下所示。
- Node.js
1. 全版本
- Apache v2.2
- IIS v8.0
1. Windows 8 and Windows Server 2012
- Nginx v1.3
- Lighttpd
高亮显示的部分对于使用CSP开发可移植web应用程序来说是最重要的。

协议的细节

创建WebSocket涉及到客户端和服务器之间的有序消息交换。首先,必须进行WebSocket握手。握手基于并类似于HTTP消息交换,因此它可以毫无问题地通过现有的HTTP基础设施传递。
- 客户端发送WebSocket连接的握手请求。
- 服务器发送握手响应(如果可以的话)。

web服务器识别握手请求消息中的传统HTTP头结构,并向客户机发送类似构造的响应消息,表明它支持WebSocket协议。如果双方都同意,那么通道将从HTTP (http://)切换到WebSockets协议(ws://)

当协议成功切换后,通道允许客户端和服务器之间的全双工通信。

单个消息的数据帧很少。

典型的来自客户端的WebSocket握手消息

GET /csp/user/MyApp.MyWebSocketServer.cls HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://localhost

典型的WebSocket握手消息来自服务器

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

请注意客户端握手消息如何请求将协议从HTTP升级到WebSocket。还要注意客户机(secwebsocket - key)和服务器(secwebsocket - accept)之间唯一密钥的交换。

WebSockets客户端代码(JavaScript)

在浏览器环境中,WebSocket协议的客户端是用JavaScript代码实现的。标准教科书详细描述了使用模型。本文件将简要介绍基本知识。

创建WebSocket

  • 第一个参数表示标识WebSocket应用程序服务器端的URL
  • 第二个参数是可选的,如果有,指定服务器必须支持的子协议,以便WebSocket连接成功。
var ws = new WebSocket(url, [protocol]);

例子:

ws = new WebSocket(((window.location.protocol == "https:")
     ? "wss:" : "ws:") \
     + "//" + window.location.host
     + /csp/user/MyApp.MyWebSocketServer.cls);
  $("#conUrl").val((window.location.protocol == "https:" ? "wss:" : "ws:") + "//" + window.location.host + "/dthealth/web/PHA.COM.WebSocket.cls" )

请注意,如何将协议定义为wswss,这取决于是否使用SSL/TLS保护底层传输。

只读属性ws.readyState定义连接的状态。它可以取以下值之一:
- 0 连接尚未建立。
- 1 连接已经建立,通信是可能的。
- 2 连接以结束握手为准。
- 3 连接已关闭或无法打开。

只读属性ws.bufferedAmount定义UTF-8文本的字节数,使用send()方法排队。

WebSocket事件

以下事件是可用的。
- ws.onopen 在建立套接字连接时打触发。
- ws.onmessage 当客户机从服务器接收数据时触发。

event.data中接收的数据。
- ws.onerror 当通信中发生错误时触发。
- ws.onclose 当连接关闭时触发。

WebSocket方法

以下是可用的方法。
- ws.send(data) 将数据传输到客户端。
- ws.close() 关闭连接。

WebSockets服务器代码(CSP)

实现WebSocket服务器的基本Caché 类是%CSP.WebSocket

当客户机请求一个WebSocket连接时,初始HTTP请求(初始握手消息)指示CSP引擎初始化应用程序的WebSocket服务器。WebSocket服务器是请求URL中指定的类。

例如,如果您的WebSocket服务器被称为 MyApp.MyWebSocketServer 的设计是在用户名称空间中操作,然后用于请求WebSocket连接的URL是:

/csp/user/MyApp.MyWebSocketServer.cls
ws://127.0.0.1/dthealth/web/PHA.COM.WebSocket.cls

WebSocket事件

WebSocket服务器的实现是从基本的%CSP.WebSocket类派生出来的。实现以下事件的响应有三个关键方法。注意,CSP会话在调用任何这些方法之前都是解锁的。
- OnPreServer (optional)
使用此方法调用应该在WebSocket服务器建立之前执行的代码。必须在这里更改SharedConnection属性。

Method OnPreServer() As %Status
{
    //设置 SharedConnection属性
    set ..SharedConnection=1
    if (..WebSocketID'=""){
        set ^CacheTemp.Chat.WebSockets(..WebSocketID)=""
    }else {
        set ^CacheTemp.Chat.Error($INCREMENT(^CacheTemp.Chat.Error),"no websocketid defined")=$HOROLOG 
    }

    q $$$OK
}
  • Server (Mandatory)
    WebSocket服务。 这是WebSocket应用程序的服务器端实现。可以使用Read()Write()方法与客户机交换消息。使用EndServer()方法从服务器端优雅地关闭WebSocket
  • OnPostServer (optional)
    使用此方法调用应该在WebSocket服务器关闭后执行的代码。

WebSocket方法

提供了以下方法

Method Read(ByRef len As %Integer = 32656,
     ByRef sc As %Status,
     timeout As %Integer = 86400) As %String

该方法从客户端读取len字符。如果调用成功,状态(sc)将返回$$$OK,否则将返回以下错误代码之一:
- $$$CSPWebSocketTimeout 读取已超时。
- $$$CSPWebSocketClosed 客户端已经终止了WebSocket

Method Write(data As %String) As %Status

此方法将数据写入客户端。

Method EndServer() As %Status

此方法通过关闭与客户端的连接来优雅地结束WebSocket服务器。

Method OpenServer(WebSocketID As %String = "") As %Status

此方法打开现有的WebSocket服务器。只有异步操作的WebSocket (SharedConnection=1)可以使用此方法访问。

WebSocket属性

提供了以下属性:

SharedConnection (default: 0)

此属性确定客户端和WebSocket服务器之间的通信是通过专用网关连接进行,还是通过共享连接池异步进行。必须在OnPreServer()方法中设置此属性,可以按如下方式设置:
- SharedConnection=0 WebSocket服务器通过专用网关连接与客户端进行同步通信。在这种操作模式下,主机连接实际上是应用程序的WebSocket服务器的“私有”连接
- SharedConnection=1 WebSocket服务器通过共享网关连接池与客户端异步通信。

  • WebSocketID 此属性表示WebSocket的唯一标识。
  • SessionId 此属性表示用于创建WebSocket的托管CSP会话ID
  • BinaryData 此属性指示网关绕过将传输的数据流解释为UTF-8编码文本的功能,并在WebSocket帧头中设置适当的二进制数据字段。

在将二进制数据流写入客户机之前,应该将该值设置为1。例如:

Set ..BinaryData = 1

websocket服务器示例

以下简单的WebSocket服务器类接受来自客户机的传入连接,并简单地回显接收到的数据。超时设置为10秒,每次Read()方法超时时,客户端都会写入一条消息。这说明了支持WebSockets的关键概念之一:从服务器与客户端启动消息交换。

最后,如果客户端(即用户)发送了字符串exit, WebSocket就会优雅地关闭。

Method OnPreServer() As %Status
{
    Quit $$$OK
}

Method Server() As %Status
{
    Set timeout=10
    For  {
        Set len=32656
        Set data=..Read(.len, .status, timeout)
        If $$$ISERR(status) {
            If $$$GETERRORCODE(status) = $$$CSPWebSocketClosed {
                Quit
            }
            If $$$GETERRORCODE(status) = $$$CSPWebSocketTimeout {
               Set status=..Write(“Server timed-out at “_$Horolog)
            }
        }
        else {
        If data="exit" Quit
            Set status=..Write(data)
        }
    }
    Set status=..EndServer()
    Quit $$$OK
}

Method OnPostServer() As %Status
{
    Quit $$$OK
}


WebSockets服务器异步操作

前一节给出的示例演示了通过专用Caché 连接与客户机同步操作的WebSocket服务器。当这样的连接建立后,它会在网关系统状态表单的状态列中标记为WebSocket。使用这种模式,WebSocket可以在托管CSP会话的安全上下文中操作,并且可以轻松地访问与该会话关联的所有属性。

使用异步操作模式(SharedConnection=1),一旦创建了WebSocket对象,与客户端的后续对话就会在共享连接池中进行,此时主机连接就会被释放:来自客户机的消息通过常规的网关连接池到达Caché ,而发送到客户机的消息则通过在网关和Caché 之间建立的服务器连接池分派。

在异步模式下,WebSocket服务器与主CSP会话分离:SessionId属性持有托管会话ID的值,但是不会自动创建会话对象的实例。只需在OnPreServer()方法中设置SharedConnection属性,就可以异步运行前面给出的示例。但是,没有必要将Caché 进程与WebSocket永久关联起来。 Server()可以退出(主机进程停止),而不需要关闭WebSocket。如果保留了WebSocketID,则可以随后在不同的Caché 进程中打开WebSocket,并恢复与客户机的通信。

例如:

Class PHA.COM.YX.WebSocket Extends %CSP.WebSocket
{

Method OnPreServer() As %Status
{

    d ##class(PHA.OP.MOB.Test).Save(..WebSocketID)
    Set SharedConnection = 1
    Quit $$$OK
}

Method Server() As %Status
{
    Quit $$$OK
}

Method OnPostServer() As %Status
{
    Quit $$$OK
}

}

注意,在OnPreServer()方法中保留WebSocketID供后续使用。还要注意,OnPreServer()方法中SharedConnection属性的设置以及服务器()方法的退出。

随后检索WebSocketID:

Set WebSocketID = MYAPP.RETRIEVE()

与客户重新建立联系:

Set ws=##class(%CSP.WebSocketTest).%New()
Set %status = ws.OpenServer(WebSocketID)

给客户端读写

Set %status=ws.Write(message)
Set data=ws.Read(.len, .%status, timeout)

最后,从服务器端关闭WebSocket:

Set %status=ws.EndServer()
00
1 0 0 6
Log in or sign up to continue