实时场景的旧处理方案

考虑网页中的以下场景:

  • 股票 K 线图
  • 聊天
  • 警报、重要通知
  • 余座
  • 抢购页面的库存

上述场景有一个共同特点——实时性

这种对实时性有要求的页面,会带来一些问题:由于 HTTP 协议是请求-响应模式,请求必须在前,响应必须在后,这就导致了服务器无法「主动」的把消息告诉客户端

短轮询(short polling)

短轮询是一种「话痨式」的方式,客户端每隔一小段时间就向服务器请求一次,询问有没有新消息

实现短轮询是非常简单的,客户端只需要设置一个轮询器不断发送请求即可

这种方案的缺陷是非常明显的:

  • 会产生大量无意义的请求
  • 会频繁打开关闭连接
  • 实时性并不高

长轮询(long polling)

我们的前辈在有限的条件下,充分发挥智慧,来解决短轮询的问题,于是演化为长轮询

长轮询有效的解决了「话痨问题」,让每一次请求和响应都是有意义的

但长轮询仍然存在问题:

  • 客户端长时间收不到响应会导致超时,从而主动断开和服务器的连接
    • 这种情况是可以处理的,当 AJAX 请求因为超时而结束时,立即重新发送请求到服务器即可
    • 虽然这种做法会让之前的请求变得无意义,但毕竟比短轮询好多了
  • 由于客户端可能「过早的」请求了服务器,服务器不得不挂起这个请求直到新消息的出现。这会让服务器长时间的占用资源却没什么实际的事情可做。

WebSocket

伴随着 HTML5 出现的 WebSocket,从协议上赋予了服务器主动推送消息的能力(实践上是全双工)

  • 全双工:通信允许数据在两个方向上同时传输,如 WebSocket
  • 半双工:通信允许数据在两个方向上传输,但不能同时,如 HTTP

实际上 TCP 是支持全双工的,但是 HTTP 为了保证请求-响应模型,只利用了半双工的通信

  • WebSocket 也是建立在 TCP 协议之上的,利用的是 TCP 全双工通信的能力
  • 使用 WebSocket,会经历两个阶段:握手阶段、通信阶段

虽然优于轮询方案,但 WebSocket 仍然是有缺点的:

  • 兼容性:WebSocket 是 HTML5 新增的内容,因此古董版本的浏览器并不支持
  • 维持 TCP 连接需要耗费资源:对于那些消息量少的场景,维持 TCP 连接确实会造成资源的浪费
    • 为了充分利用 TCP 连接的资源,在使用了 WebSocket 的页面,可以放弃 AJAX,都使用 WebSocket 进行通信,当然这会带来程序设计上的一些问题,需要权衡。

握手

WebSocket 协议是一个高扩展性的协议,详细内容会比较复杂,这里仅讲解面试中会问到的握手协议

当客户端需要和服务器使用 WebSocket 进行通信时,首先会使用 HTTP 协议完成一次特殊的请求-响应,这一次请求-响应就是 WebSocket 握手

在握手阶段,首先由客户端向服务器发送一个请求,请求地址格式如下:

ws://mysite.com/path # 使用HTTP  
wss://mysite.com/path # 使用HTTPS

请求头如下:

Connection: Upgrade # 升级协议
Upgrade: websocket # 协议升级为 WebSocket
Sec-WebSocket-Version: 13 # WebSocket 的版本
Sec-WebSocket-Key: YWJzZmFkZmFzZmRhYw== # 暗号:天王盖地虎

服务器如果同意,就应该响应下面的消息:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade # 协议升级了
Upgrade: websocket # 升级到 Websocket
Sec-WebSocket-Accept: ZzIzMzQ1Z2V3NDUyMzIzNGVy # 暗号:小鸡炖蘑菇

握手完成,后续消息收发不再使用 HTTP,而是使用 WebSocket 进行全双工通信,任何一方都可以主动发消息给对方

WebSocket API

MDN

const ws = new WebSocket("地址"); // 创建 Websocket 连接,浏览器自动握手
 
// 事件:握手完成后触发
ws.onopen = function () {
  console.log('连接到了服务器');
};
 
// 事件:收到服务器消息后触发
ws.onmessage = function (e) {
  console.log(e.data); // e.data:服务器发送的消息
};
 
// 事件:连接关闭后触发
ws.onclose = function () {
  console.log('连接关闭了');
};
 
// 发送消息到服务器
ws.send(消息);
 
// 连接状态:0-正在连接中;1-已连接;2-正在关闭中;3-已关闭
ws.readyState

Socket.IO

官网

上图是一段时间中服务器给客户端推送的数据,你能区分这些数据都是什么意思吗?

这就是问题所在:WebSocket 连接双方可以在任何时候发送任何类型的数据,另一方必须要清楚这个数据的含义是什么?

虽然我们可以自行解决这些问题,但毕竟麻烦。Socket.IO 是对原生 WebSocket API 做的一层封装,帮助我们解决了这些问题,它把消息放到不同的事件中,通过监听和触发事件来实现对不同消息的处理:

客户端和服务器双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以把各种消息进行有序管理了

注意

Socket.IO 为了实现这些要求,对消息格式进行了特殊处理,因此如果一方要使用 Socket.IO,双方必须都使用

  • 在约定事件名时要注意,Socket.IO 有一些预定义的事件名,比如 messageconnect 等,为了避免冲突,建议自定义事件名使用一个特殊的前缀,比如 app:
  • Socket.IO 对低版本浏览器还进行了兼容处理,如果浏览器不支持 WebSocket,Socket.IO 将使用长轮询(long polling)处理
  • 另外,Socket.IO 还支持使用命名空间来进一步隔离业务,要了解这些高级功能,以及 Socket.IO 的更多 API,请参阅其 官方文档