#
第十五章 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应用程序是最重要的。 ## 服务器的支持 可以说,面向服务器的基于`javascript`的`Node.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`握手消息 ```java 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`握手消息来自服务器 ```java 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`连接成功。 ```java var ws = new WebSocket(url, [protocol]); ``` 例子: ```java 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" ) ``` 请注意,如何将协议定义为`ws`或`wss`,这取决于是否使用`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`属性。 ```java 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`方法 提供了以下方法 ```java Method Read(ByRef len As %Integer = 32656, ByRef sc As %Status, timeout As %Integer = 86400) As %String ``` 该方法从客户端读取`len`字符。如果调用成功,状态`(sc)`将返回`$$$OK`,否则将返回以下错误代码之一: - `$$$CSPWebSocketTimeout` 读取已超时。 - `$$$CSPWebSocketClosed` 客户端已经终止了`WebSocket`。 ```java Method Write(data As %String) As %Status ``` 此方法将数据写入客户端。 ```java Method EndServer() As %Status ``` 此方法通过关闭与客户端的连接来优雅地结束`WebSocket`服务器。 ```java 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。例如: ```java Set ..BinaryData = 1 ``` # websocket服务器示例 以下简单的`WebSocket`服务器类接受来自客户机的传入连接,并简单地回显接收到的数据。超时设置为10秒,每次`Read()`方法超时时,客户端都会写入一条消息。这说明了支持`WebSockets`的关键概念之一:从服务器与客户端启动消息交换。 最后,如果客户端(即用户)发送了字符串`exit`, `WebSocket`就会优雅地关闭。 ```java 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`,并恢复与客户机的通信。 例如: ```java 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`: ```java Set WebSocketID = MYAPP.RETRIEVE() ``` 与客户重新建立联系: ```java Set ws=##class(%CSP.WebSocketTest).%New() Set %status = ws.OpenServer(WebSocketID) ``` 给客户端读写 ```java Set %status=ws.Write(message) Set data=ws.Read(.len, .%status, timeout) ``` 最后,从服务器端关闭WebSocket: ```java Set %status=ws.EndServer() ```