12.29 HTML 网页套接字(Web Socket)

HTML网页套接字实现了浏览器与服务器全双工通信

桌面(或手机)本地应用都可以通过TCP协议和服务器实现全双工通信,也就是建立一个套接字连接(Socket),然后在上面双向传送数据,客户端和服务器都可以发送和接收消息。 但是在传统的网页模型中,通过HTTP协议仅能实现单向的通信,即浏览器发送请求,而服务器被动应答请求,服务器不能主动推送信息给到网页端。

那么很多网站为了实现“实时信息推送”的效果,大都采用了轮询(Polling)Comet技术,轮询就是网页定时发送ajax请求给服务器来查询特定数据的状态。

对于ajax轮询,我们可以形象的认为是下面这样的场景:

客户端:亲,有没有新信息(Request)
服务端:没有(Response)
客户端:亲,有没有新信息(Request)
服务端:没有。。(Response)
客户端:亲,有没有新信息(Request)
服务端:你好烦,没有啊。。(Response)
客户端:亲,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:亲,有没有新消息(Request)
服务端:靠,我被你烦挂了。。。(Response)

Comet是轮询技术的改进版本,该技术有两种实现方式:长轮询(Long Poll)和iframe流。

Comet这个词汇有点怪异,这是一种伞形术语,用来包含多个概念,只是一个代号,不是缩写,无需考究其单词含义。

  • 长轮询:长轮询是在打开一条连接以后以阻塞套接字模型保持,等待服务器推送来数据再关闭的方式。长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。但如果服务端的数据变更非常频繁的话,就和定时轮询一样低效。
  • iframe流:iframe流方式是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间创建一条长链接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript,通过parent接口来对父窗口DOM操作),来实时更新页面。一个典型的应用是Google Talk。

上述方法或多或少都有滥用请求的缺陷(而且每一次的 HTTP 请求和应答都带有完整的 HTTP 头信息,增加了每次传输的数据量)。 当然网站也可以把内容放在Flash这些插件上,这样也可以和服务器进行双工通信,但是依赖于第三方插件的方式又会带来兼容性问题,尤其是在移动设备上。

HTML5定义了WebSocket协议和接口来替代所有上述折衷方式,使浏览器具备像 C/S 架构(回忆一下课程前沿中提到过该概念)下桌面应用的实时通讯能力。

使用WebSocket能带来性能的大幅度提高,源于两点:

  • 节省请求次数。在一次握手后保持TCP连接,直接传送数据。
  • 节省报文流量。我们可以把WebSocket理解成一个轻量级的TCP应用连接,剔除无关的头部信息,只传送必要的数据。

Websocket.org网站对传统的轮询方式和 WebSocket 调用方式作了一个详细的测试和比较,将一个简单的 Web 应用分别用轮询方式和 WebSocket 方式来实现,在这里引用一下他们的测试结果图:

轮询和 WebSocket 实现方式的网络负载对比图

可以看到,在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有很大的性能优势。

具体而言,我们认为WebSocket适合用于实现游戏、股票交易、同步多用户文档编辑以及即时聊天等实时服务。

WebSocket 协议

协议包含两个部分:一次握手(handshake),以及数据传输。

WebSocket 握手协议

客户端到服务端: 
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: //example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务端到客户端:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

这些请求和通常的 HTTP 请求很相似,但是其中有些内容是和 WebSocket 协议密切相关的。“Upgrade:WebSocket”用来告诉服务端“我不是一个HTTP请求哦,请升级到 WebSocket 协议”。 服务端在握手中需要确认能支持该协议,这通过把客户端发来的头信息中的”Sec-WebSocket-Key”添加上一个GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)后,通过SHA-1哈希计算,再用base64-encoded放在应答中返回给客户端。 一旦连接建立,客户端和服务器端就可以通过这个通道双向传输数据了。

具体的密钥计算实例可参阅WebSocket的RFC规范:[RFC6455]

WebSocket URL格式

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

和http类似,WebSocket的访问URL由协议关键字ws:或wss:(安全的ws)开头,加上域名、端口、路径和参数。

WebSocket 浏览器支持

浏览器支持情况
ChromeSupported in version 4+
FirefoxSupported in version 4+
Internet ExplorerSupported in version 10+
OperaSupported in version 10+
SafariSupported in version 5+

WebSocket 服务器支持

为了使用WebSocket,我们需要实现支持ws协议的服务端程序,此外,如果使用了代理服务器,那么你还需要配置代理程序(如Apache和Nginx)支持该协议,使其能转发头部信息并保持连接状态。

Nginx自从1.3开始支持WebSocket协议,需要在配置中显式声明Upgrade和Connection的头信息,如下所示:

location /wsapp/ {
    proxy_pass //wsbackend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Apache支持ws协议的代理模块是mod_proxy_wstunnel,透传数据给后端服务。

后端服务器可以用socket.io(一个js库)、nodejs、php、python、lua等程序来实现。这里不做详细描述。

WebSocket JavaScript接口

握手协议通常是我们在构建 WebSocket 服务器端的实现和提供浏览器的 WebSocket 支持时需要考虑的问题,而针对 Web 开发人员的 WebSocket JavaScript 接口是非常简单的,以下是 WebSocket JavaScript 接口的定义:

[Constructor(in DOMString url, in optional DOMString protocol)] 
 interface WebSocket { 
   readonly attribute DOMString URL; 
        // ready state 
   const unsigned short CONNECTING = 0; 
   const unsigned short OPEN = 1; 
   const unsigned short CLOSED = 2; 
   readonly attribute unsigned short readyState; 
   readonly attribute unsigned long bufferedAmount; 
   //networking 
   attribute Function onopen; 
   attribute Function onmessage; 
   attribute Function onclose; 
   boolean send(in DOMString data); 
   void close(); 
 }; 
 WebSocket implements EventTarget;

其中 URL 属性代表 WebSocket 服务器的网络地址,协议是ws或wss,send 方法就是发送数据到服务器端,close 方法就是关闭连接。 除了这些方法,还有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。

下面是一段简单的 JavaScript 代码展示了怎样建立 WebSocket 连接和获取数据:

var  wsServer = 'ws://localhost:8888/Demo'; 
 var  websocket = new WebSocket(wsServer); 
 websocket.onopen = function (evt) { onOpen(evt) }; 
 websocket.onclose = function (evt) { onClose(evt) }; 
 websocket.onmessage = function (evt) { onMessage(evt) }; 
 websocket.onerror = function (evt) { onError(evt) }; 
 function onOpen(evt) { 
 console.log("Connected to WebSocket server."); 
 } 
 function onClose(evt) { 
 console.log("Disconnected"); 
 } 
 function onMessage(evt) { 
 console.log('Retrieved data from server: ' + evt.data); 
 } 
 function onError(evt) { 
 console.log('Error occured: ' + evt.data); 
 }

我们可以通过Chrome开发者工具来观测Websocket的握手消息,和通常的HTTP请求类似,打开Network标签,里面可以看到type为websocket关键字的请求就是Websocket请求。

WebSocket 实例 - 即时聊天应用

Socket.io网站有一个使用nodejs做服务器的即时聊天应用,你需要安装node.js(作为测试,你可以安装在自己的Windows/Mac/Ubuntu电脑上,使用localhost访问), 接着创建一个socket.io的app做服务端,然后使用socket.io的js客户端部分实现一个发消息的网页应用。最终的运行效果如下:

具体步骤请阅读://socket.io/get-started/chat/。这里不做重复介绍。

另外Websocket.org网站收集了一些常见的应用演示,也可以作为参考://www.websocket.org/echo.html。