#
第十五章 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()
```