使用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:
- Chrome 4.0 5.0, Safari 5.0.0
- HyBi-00/Hixie-76:
- Chrome 6.0-13.0, Safari 5.0.2 5.1, Firefox 4.0 (disabled), Opera 11 (disabled)
- HyBi-07 :
- Chrome 14.0, Firefox 6.0, IE 9 (via Silverlight extension)
- HyBi-10:
- 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
- Chrome 16
- Safari 6
- Firefox 11
- Opera 12.10/Opera Mobile 12.1
- IE 10 最后突出显示的部分对于开发可移植web应用程序是最重要的。
服务器的支持
可以说,面向服务器的基于javascript
的Node.js
技术提供了最复杂、目前最成熟的WebSockets
协议实现。WebSockets
一直与Node.js
紧密联系在一起。但是,其他web服务器技术正在迅速赶上来,所有主要web服务器的最新版本现在都提供了WebSockets
支持,如下所示。
- Node.js
- 全版本
- Apache v2.2
- IIS v8.0
- 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]);
例子:
代码语言:javascript复制ws = new WebSocket(((window.location.protocol == "https:")
? "wss:" : "ws:")
"//" window.location.host
/csp/user/MyApp.MyWebSocketServer.cls);
代码语言:javascript复制 $("#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
代码语言:javascript复制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
方法
提供了以下方法
代码语言:javascript复制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
此方法将数据写入客户端。
代码语言:javascript复制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。例如:
代码语言:javascript复制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
,并恢复与客户机的通信。
例如:
代码语言:javascript复制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()
与客户重新建立联系:
代码语言:javascript复制Set ws=##class(%CSP.WebSocketTest).%New()
Set %status = ws.OpenServer(WebSocketID)
给客户端读写
代码语言:javascript复制Set %status=ws.Write(message)
Set data=ws.Read(.len, .%status, timeout)
最后,从服务器端关闭WebSocket:
代码语言:javascript复制Set %status=ws.EndServer()