Part 01
什么是即时通信?
即时通信是一个实时通信系统,允许两人或多人使用网络实时的传递文字消息、文件、语音与视频交流。即时通信技术在Native App中通过TCP、UDP等协议可以轻松实现,在Native应用较为流行。
受HTTP协议以及Web客户端框架限制,想在Web中实现真正的即时通信,可谓是技术上尽脑汁,极尽所能。从传统的短轮询、长轮询到Comet(长轮询的变体)技术,再到HTML5标准发布之后的WebSocket、 SSE这类技术的横空出现,使Web端即时通信的技术方案越来越多,实现也越来越容易。但是对于技术人员面对不同的场景该如何选择更实用的技术方案呢?
Part 02
Web即时通信实现方案有哪些?
2.1 轮询的原理与实现
轮询分为短轮询与长轮询👇
短轮询:是客户端定期向服务器发起请求查询并获取数据,无论是否有数据服务端都立即响应客户端的请求,该方案较为简单不做过多介绍。
长轮询:是比短轮询更有效的一种技术,当服务器收到浏览器请求后如果有数据, 服务器立刻响应请求; 如果没有数据服务器就会hold一段时间,尽可能长时间的保持浏览器的连接打开;这段时间内如果有数据,服务器立刻响应请求; 如果时间到了还没有数据, 则响应http请求;浏览器收到http响应后,立即再发送一个同样http请求查询是否有数据;如此重复下去。下图是长轮询的交互示意图:
长轮询浏览器代码实现如下:
/* Client - subscribing to the test events */
subscribe: (callback) => {
const pollUserEvents = () => {
$.ajax({
method: 'GET',
url: 'http://localhost:8080/testEvents',
success: (data) => {
callback(data) // process the data
},
complete: () => {
pollUserEvents();
},
timeout: 30000
})
}
2.2 WebSocket的原理与实现
在2008年中期,开发人员Michael Carter和Lan Hickson敏锐的感受到Comet在实现复杂交互时带来的的苦恼和局限,他们制定了一项计划并引入现代实时双向通信的新标准,创造了WebSocket,并进入了W3C HTML草案标准,2011年RFC 6455-WebSocket协议被发布到了IETF网站。
WebSocket是一个构建在设备TCP/IP堆栈上的瘦传输层,一个独立的基于TCP的协议,为Web应用程序开发人员提供尽可能接近原始的TCP通信层,WebSocket与HTTP唯一的关系是它的握手是由HTTP服务器解释为一个Upgrade请求。
WebSocket是一种事件驱动的协议,这意味着可以将其用于真正的实时通信。WebSocket的实现了一次连接,双方多次通信的能力。首先由客户端发出WebSocket连接请求,服务器端进行响应,实现类似TCP握手的动作。这个连接一旦建立起来,在客户端和服务器之间会一直保持该连接,两者之间可以直接的进行数据的互相传送,并且在连接被关闭前可以进行多次交互。
WebSocket浏览器侧代码实现:
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost/ws');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
WebSocket服务端代码实现:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
2.3 SSE的原理与实现
SSE是Server-Sent Events的简称,是一种可以主动从服务端推送消息给浏览器的技术。SSE也是使用HTTP协议进行交互,严格地说HTTP协议无法做到服务器主动推送信息给浏览器,都是浏览器主动去请求服务器端获取最新的数据,SSE技术使服务器与浏览器之间维护一个HTTP长连接,当有新数据时,服务端可以通过这个长连接将数据发送给浏览器。
浏览器主动请求建立一个格式为text/event-stream的stream流 ,服务器给浏览器发送的数据不是一次性的数据包,而是一个stream流,并且浏览器不会关闭连接,会一直等着服务器发送新的数据流报文。
客户端代码实现:
var SSENotification = {
source: null,
subscribe: function() {
if ('EventSource' in window) {
this.source = new EventSource('/sse');
this.source.addEventListener('message', function(res) {
const d = res.data;
window.ChatroomDOM.renderData(JSON.parse(d));
});
}
return this.unsubscribe;
},
unsubscribe: function () {
this.source && this.source.close();
}
}
服务器端代码实现:
router.get('/sse', function(req, res) {
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
const response = { code: 200, data: { connectors: connectors, messages: messages } };
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("data: " + JSON.stringify(response) + "\n\n");
var unsubscribe = Event.subscribe(function() {
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
const response = { code: 200, data: { connectors: connectors, messages: messages } };
res.write("data: " + JSON.stringify(response) + "\n\n");
});
req.connection.addListener("close", function () {
unsubscribe();
}, false);
});
Part 03
Web即时通信方案对比
三种即时通信技术方案各有优缺点,三种技术方案特性对比如下:
从对比可以得出👇
长轮询:兼容性好,实现容易;但是由于需要不停向服务器请求查询,很多时候都是无效报文,效率低,客户端数量多时服务端的压力较大;适用于扫码登录,流程状态变化等场景。
WebSocket:支持双向通信,可以实现真正的实时通信,支持二进制数据传输,基本可以胜任各种即时通信场景,而且由于WebSocket的首部信息很小,报文有效载荷高,在海量并发和客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势;但是WebSocket协议相对于HTTP协议较复杂,不支持断线重连,有一定维护成本,因此实际应用中需要基于WebSocket开源SDK进行开发;适用于大型聊天App或大型多人在线游戏的场景。
SSE:属于轻量级方案,主流浏览器都已经支持,使用简单,默认支持断线重连,支持自定义事件类型;但是SSE长连接只支持从服务器向客户端发数据,并且不支持二进制传输(需要编码后传输)。适用于服务器持续向客户端发送数据的场景,如:实时股价流图、在线音视频播放、监视器客户端查看服务器实时信息等。
Part 04
结束语
轮询、WebSocket、SSE三种技术方案中该如何选择?在消息推送时SSE似乎是我们解决问题的最终选择;但是SSE对于大型游戏、聊天室时又有很多力不从心,不如WebSocket全面;而面对扫码登录等简单场景时轮询似乎更加简单便捷。因此Web端即时通信方案并不能简单的说古老的技术会被新技术替代,项目实施中我们要合理选择,采用更简单、更高效的方案来完成需求。