随着 Web 的发展,用户对于 Web 的实时推送要求也愈来愈高,在 WebSocket 出现以前,大多数状况下是经过客户端发起轮询来拿到服务端实时更新的数据,由于 HTTP1.x 协议有一个缺陷就是通讯只能由客户端发起,服务端无法主动给客户端推送。这种方式在对实时性要求比较高的场景下,好比即时通信、即时报价等,显然会十分低效,体验也很差。为了解决这个问题,便出现了 WebSocket 协议,实现了客户端和服务端双向通讯的能力。介绍 WebSocket 以前,仍是让咱们先了解下轮询实现推送的方式。html
短轮询的实现思路就是浏览器端每隔几秒钟向服务器端发送 HTTP 请求,服务端在收到请求后,不管是否有数据更新,都直接进行响应。在服务端响应完成,就会关闭这个 TCP 链接,代码实现也最简单,就是利用 XHR , 经过 setInterval 定时向后端发送请求,以获取最新的数据。git
setInterval(function() { fetch(url).then((res) => { // success code }) }, 3000);
客户端发送请求后服务器端不会当即返回数据,服务器端会阻塞请求链接不会当即断开,直到服务器端有数据更新或者是链接超时才返回,客户端才再次发出请求新建链接、如此反复从而获取最新数据。大体效果以下:github
客户端代码以下:web
function async() { fetch(url).then((res) => { async(); // success code }).catch(() => { // 超时 async(); }) }
前面提到的短轮询(Polling)和长轮询(Long-Polling), 都是先由客户端发起 Ajax 请求,才能进行通讯,走的是 HTTP 协议,服务器端没法主动向客户端推送信息。算法
当出现相似体育赛事、聊天室、实时位置之类的场景时,轮询就显得十分低效和浪费资源,由于要不断发送请求,链接服务器。WebSocket 的出现,让服务器端能够主动向客户端发送信息,使得浏览器具有了实时双向通讯的能力。express
没用过 WebSocket 的人,可能会觉得它是个什么高深的技术。其实否则,WebSocket 经常使用的 API 很少也很容易掌握,不过在介绍如何使用以前,让咱们先看看它的通讯原理。后端
当客户端要和服务端创建 WebSocket 链接时,在客户端和服务器的握手过程当中,客户端首先会向服务端发送一个 HTTP 请求,包含一个 Upgrade 请求头来告知服务端客户端想要创建一个 WebSocket 链接。浏览器
在客户端创建一个 WebSocket 链接很是简单:安全
let ws = new WebSocket('ws://localhost:9000');
相似于 HTTP 和 HTTPS,ws 相对应的也有 wss 用以创建安全链接,本地已 ws 为例。这时的请求头以下:服务器
Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: Upgrade // 表示该链接要升级协议 Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264 Host: localhost:9000 Origin: http://localhost:9000 Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: 5fTJ1LTuh3RKjSJxydyifQ== // 与响应头 Sec-WebSocket-Accept 相对应 Sec-WebSocket-Version: 13 // 表示 websocket 协议的版本 Upgrade: websocket // 表示要升级到 websocket 协议 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
响应头以下:
Connection: Upgrade Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE= Upgrade: websocket
此时响应行(General)中能够看到状态码 status code 是 101 Switching Protocols , 表示该链接已经从 HTTP 协议转换为 WebSocket 通讯协议。 转换成功以后,该链接并无中断,而是创建了一个全双工通讯,后续发送和接收消息都会走这个链接通道。
注意,请求头中有个 Sec-WebSocket-Key 字段,和相应头中的 Sec-WebSocket-Accept 是配套对应的,它的做用是提供了基本的防御,好比恶意的链接或者无效的链接。Sec-WebSocket-Key 是客户端随机生成的一个 base64 编码,服务器会使用这个编码,并根据一个固定的算法:
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一个固定的字符串 accept = base64(sha1(key + GUID)); // key 就是 Sec-WebSocket-Key 值,accept 就是 Sec-WebSocket-Accept 值
其中 GUID 字符串是 RFC6455 官方定义的一个固定字符串,不得修改。
客户端拿到服务端响应的 Sec-WebSocket-Accept 后,会拿本身以前生成的 Sec-WebSocket-Key 用相同算法算一次,若是匹配,则握手成功。而后判断 HTTP Response 状态码是否为 101(切换协议),若是是,则创建链接,大功告成。
下面来实现一个纯文字消息类型的一对一聊天(单聊)功能,废话很少说,直接上代码,注意看注释。
客户端:
function connectWebsocket() { ws = new WebSocket('ws://localhost:9000'); // 监听链接成功 ws.onopen = () => { console.log('链接服务端WebSocket成功'); ws.send(JSON.stringify(msgData)); // send 方法给服务端发送消息 }; // 监听服务端消息(接收消息) ws.onmessage = (msg) => { let message = JSON.parse(msg.data); console.log('收到的消息:', message) elUl.innerHTML += `<li class="b">小秋:${message.content}</li>`; }; // 监听链接失败 ws.onerror = () => { console.log('链接失败,正在重连...'); connectWebsocket(); }; // 监听链接关闭 ws.onclose = () => { console.log('链接关闭'); }; }; connectWebsocket();
从上面能够看到 WebSocket 实例的 API 很容易理解,简单好用,经过 send() 方法能够发送消息,onmessage 事件用来接收消息,而后对消息进行处理显示在页面上。 当 onerror 事件(监听链接失败)触发时,最好进行执行重连,以保持链接不中断。
服务端 Node : (这里使用 ws 库)
const path = require('path'); const express = require('express'); const app = express(); const server = require('http').Server(app); const WebSocket = require('ws'); const wss = new WebSocket.Server({ server: server }); wss.on('connection', (ws) => { // 监听客户端发来的消息 ws.on('message', (message) => { console.log(wss.clients.size); let msgData = JSON.parse(message); if (msgData.type === 'open') { // 初始链接时标识会话 ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`; } else { let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`; wss.clients.forEach(client => { if (client.sessionId === sessionId) { client.send(message); // 给对应的客户端链接发送消息 } }) } }) // 链接关闭 ws.on('close', () => { console.log('链接关闭'); }); });
同理,服务端也有对应的发送和接收的方法。完整示例代码见 这里
这样浏览器和服务端就能够愉快的发送消息了,效果以下:
其中绿色箭头表示发出的消息,红色箭头表示收到的消息。
在实际使用 WebSocket 中,长时间不通消息可能会出现一些链接不稳定的状况,这些未知状况致使的链接中断会影响客户端与服务端以前的通讯,
为了防止这种的状况的出现,有一种心跳保活的方法:客户端就像心跳同样每隔固定的时间发送一次 ping ,来告诉服务器,我还活着,而服务器也会返回 pong ,来告诉客户端,服务器还活着。ping/pong 实际上是一条与业务无关的假消息,也称为心跳包。
能够在链接成功以后,每隔一个固定时间发送心跳包,好比 60s:
setInterval(() => { ws.send('这是一条心跳包消息'); }, 60000)
经过上面的介绍,你们应该对 WebSocket 有了必定认识,其实并不神秘,这里对文章内容简单总结一下。当建立 WebSocket 实例的时候,会发一个 HTTP 请求,请求报文中有个特殊的字段 Upgrade ,而后这个链接会由 HTTP 协议转换为 WebSocket 协议,这样客户端和服务端创建了全双工通讯,经过 WebSocket 的 send 方法和 onmessage 事件就能够经过这条通讯链接交换信息。