12.29 HTML 网页套接字(Web Socket)
桌面(或手机)本地应用都可以通过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 方案相比传统的 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 浏览器支持
浏览器 | 支持情况 |
---|---|
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported 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/。这里不做重复介绍。