估计会很多...先写在前面,即时通讯技术有很多 列出来有 短轮询、comet(长轮询)、websocket、sse
- 客户端轮询:传统意义上的短轮询(Short Polling)
- 服务器端轮询:长轮询(Long Polling)
- 单向服务器推送:Server-Sent Events(SSE)
- 全双工通信:WebSocket
前言
1996年IETF HTTP工作组发布了HTTP协议的1.0版本 ,到现在普遍使用的版本1.1,HTTP协议经历了17 年的发展。这种分布式、无状态、基于TCP的请求/响应式、在互联网盛行的今天得到广泛应用的协议,相对于互联网的迅猛发展,它似乎进步地很慢。互联网从兴起到现在,经历了门户网站盛行的web1.0时代,而后随着ajax技术的出现,发展为web应用盛行的web2.0时代,如今又朝着web3.0的方向迈进。反观http协议,从版本1.0发展到1.1,除了默认长连接之外就是缓存处理、带宽优化和安全性等方面的改进。它一直保留着无状态、请求/响应模式,似乎从来没意识到这应该有所改变。
好在HTML5的时代已经到来,为Web端即时通讯的实现带来了WebSocket和SSE(Server-sent Events)两种技术方案。
一、短轮询
定义
指在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。
优点
实现简单,不需要额外开发,仅需要定时发起请求,解析响应即可。
缺点
- 请求中有大半是无用,难于维护,浪费带宽和服务器资源;
- 响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。
- 轮询间隔不好控制。如果实时性要求较高,短轮询是明显的短板,但如果设置太长,会导致消息延迟。
var xhr = new XMLHttpRequest();
setInterval(function(){
xhr.open('GET','/user');
xhr.onreadystatechange = function(){
};
xhr.send();
},1000)
二、长轮询
定义
客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息,监听的内容有改变才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。(或者在一定的时间内,请求还得不到返回,就会因为超时自动断开连接)
优点
在无消息的情况下不会频繁的请求,耗费资源小。
缺点
- 服务器hold连接会消耗资源
- 返回数据顺序无保证,难于管理维护。
- 浏览器端对统一服务器同时 http 连接有最大限制, 最好同一用户只存在一个长轮询;
实例:WebQQ、Hi网页版、Facebook IM。
function ajax(){
var xhr = new XMLHttpRequest();
xhr.open('GET','/user');
xhr.onreadystatechange = function(){
ajax();
};
xhr.send();
}
基于iframe的长轮询
实现原理
- 在页面中嵌入一个隐藏的iframe,地址指向轮询的服务器地址,然后在父页面中放置一个执行函数,比如execute(data);
- 当服务器有内容改变时,会向iframe发送一个脚本;
- 通过发送的脚本,主动执行父页面中的方法,达到推送的效果。
缺点:
基于iframe的长轮询底层还是长轮询技术,只是实现方式不同,而且在浏览器上会显示请求未加载完成,图标会不停旋转,简直是强迫症杀手,个人不是很推荐。
三、Web Socket
定义:
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。
Websocket是基于HTTP协议的,在和服务端建立了链接后,服务端有数据有了变化后会主动推送给前端。
实现原理
客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws:// example.com/socket) 。请求中包含一些 特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。
服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。
客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应,客户端和服务端相互进行通信。
优点
1、请求响应快,不浪费资源。(与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,一旦WebSocket连接建立,就不需要像HTTP那样频繁地建立和关闭连接。)
2、由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。
3、http协议的头部太大,且每个请求携带的几百上千字节的头部大部分是重复的,WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销)
缺点
1、连接状态保持:长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担
2、复杂性:与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面
3、主流浏览器支持的Web Socket版本不一致;服务端没有标准的API。
服务器端
let express = require('express');
const path = require('path');
let app = express();
let server = require('http').createServer(app);
app.get('/', function (req, res) {
res.sendFile(path.resolve(__dirname, 'index.html'));
});
app.listen(3000);
//-----------------------------------------------
let WebSocketServer = require('ws').Server;
let wsServer = new WebSocketServer({ port: 8888 });
wsServer.on('connection', function (socket) {
console.log('连接成功');
socket.on('message', function (message) {
console.log('接收到客户端消息:' + message);
socket.send('服务器回应:' + message);
});
});
客户端
<script>
let ws = new WebSocket('ws://localhost:8888');
ws.onopen = function () {
console.log('客户端连接成功');
ws.send('hello');
}
ws.onmessage = function (event) {
console.log('收到服务器的响应 ' + event.data);
}
</script>
- onmessage收到服务器响应时执行
- onerroe 出现异常时执行
- onopen 建立起连接时执行
- onclose 断开连接时执行
实现流程
- 客户端:申请协议升级
首先客户端发起协议升级请求
请求采用的是标准的HTTP报文格式,且只支持GET方法
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA==
| 字段 | 含义 |
|---|---|
| Connection: Upgrade | 表示要升级协议 |
| Upgrade: websocket | 表示要升级到websocket协议 |
| Sec-WebSocket-Version: 13 | 表示websocket的版本 |
| Sec-WebSocket-Key | 与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意义的连接 |
- 服务端:响应协议升级
服务端返回内容如下 状态代码101表示协议切换 到此完成协议升级,后续的数据交互都按照新的协议来
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
| 字段 | 含义 |
|---|---|
| Connection: Upgrade | 升级协议 |
| Upgrade: websocket | 升级到websocket协议 |
| Sec-WebSocket-Accept | Accept字符串 |
Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来 计算公式为:
将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接 通过SHA1计算出摘要,并转成base64字符串
const crypto = require('crypto');
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function toAcceptKey(wsKey) {
return crypto.createHash('sha1').update(wsKey + CODE).digest('base64');;
}
const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
console.log(toAcceptKey(webSocketKey));//aWAY+V/uyz5ILZEoWuWdxjnlb7E=
Sec-WebSocket-Key/Accept的作用
- 避免服务端收到非法的websocket连接
- 确保服务端理解websocket连接
- 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的
- Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)
数据帧格式 WebSocket客户端、服务端通信的最小单位是帧,由1个或多个帧组成一条完整的消息(message) 发送端 将消息切割成多个帧,并发送给服务端 接收端 接收消息帧,并将关联的帧重新组装成完整的消息
bit和byte 比特就是bit 二进制数系统中,每个0或1就是一个位(bit),位是数据存储的最小单位 其中8个bit就称为一个字节(Byte)
WebSocket库:socket.io
定义
Socket.IO是一个WebSocket库,包括了客户端的js和服务器端的nodejs,它的目标是构建可以在不同浏览器和移动设备上使用的实时应用。
特点
- 易用性:socket.io封装了服务端和客户端,使用起来非常简单方便。
- 跨平台:socket.io支持跨平台,这就意味着你有了更多的选择,可以在自己喜欢的平台下开发实时应用。
- 自适应:它会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5。
sse(Server-Sent Event)
定义:
SSE是一种可以主动从服务端推送消息的技术。
HTML5规范中提供了服务端事件EventSource,浏览器在实现了该规范的前提下创建一个EventSource连接后,便可收到服务端的发送的消息,这些消息需要遵循一定的格式,对于前端开发人员而言,只需在浏览器中侦听对应的事件皆可。
总的来说,就是一个客户端去从服务器端订阅一条流,之后服务端可以发送消息给客户端直到服务端或者客户端关闭该“流”。
本质:
SSE的本质其实就是一个HTTP的长连接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream。所以客户端不会关闭连接,会一直等着服务器发过来的新的数据流。
实现原理
- 客户端向服务端发起HTTP长连接,服务端返回stream响应流。客户端收到stream响应流并不会关闭连接而是一直等待服务端发送新的数据流。
- 客户端向服务器发送一个GET请求,带有指定的header,表示可以接收事件流类型,并禁用任何的事件缓存。
- 服务器返回一个响应,带有指定的header,表示事件的媒体类型和编码,以及使用分块传输编码(chunked)来流式传输动态生成的内容。
- 服务器在有数据更新时,向客户端发送一个或多个名称:值字段组成的事件,由单个换行符分隔。事件之间由两个换行符分隔。服务器可以发送事件数据、事件类型、事件ID和重试时间等字段。
- 客户端使用EventSource接口来创建一个对象,打开连接,并订阅onopen、onmessage和onerror等事件处理程序来处理连接状态和接收消息。
- 客户端可以使用GET查询参数来传递数据给服务器,也可以使用close方法来关闭连接。
优点
- SSE 使用 HTTP 协议,现有的服务器软件都支持
- SSE提供了从服务器到客户端的单向通信,对于那些只需要服务器推送数据到客户端的应用(如股票行情、新闻更新等),SSE 属于轻量级,使用简单
- SSE 默认支持断线重连,简化了客户端的重连逻辑
- SSE复用现有的HTTP端口(通常为80或443),因此在部署时不需要额外的网络配置
- SSE 一般只用来传送文本,二进制数据需要编码后传送
- SSE 支持自定义发送的消息类型
缺点
「单向通信限制」:SSE只支持服务器到客户端的单向通信,如果需要客户端向服务器发送消息,则需要使用其他的HTTP请求方式。
「流量消耗」:对于需要频繁更新的应用,SSE可能会因为持续的HTTP连接而消耗更多的流量和服务器资源。
「缺乏协议支持」:由于SSE是基于HTTP的,它不支持二进制数据传输,这在传输大量数据时可能不如WebSocket高效。
「带宽占用」:尽管SSE通常传输的数据量不大,但持续的连接和频繁的数据推送仍然会占用一定的带宽。对于高流量应用,这可能会成为限制因素。
「状态管理」:服务器需要维护每个SSE连接的状态,包括发送的数据、重连尝试等。状态管理的复杂性随着连接数的增加而增加。 可以使用数据库或缓存来存储和管理SSE连接状态。
「内存泄漏」:长时间运行的SSE连接可能会导致内存泄漏,特别是如果不正确地管理事件监听器和相关资源。
适用场景 chatGPT 返回的数据 就是使用的SSE 技术
简单实现
浏览器端
-
浏览器端,需要创建一个EventSource对象,并且传入一个服务端的接口URI作为参
-
默认EventSource对象通过侦听message事件获取服务端传来的消息
-
open事件则在http连接建立后触发
-
error事件会在通信错误(连接中断、服务端返回数据失败)的情况下触发
-
同时EventSource规范允许服务端指定自定义事件,客户端侦听该事件即可
<script>
var eventSource = new EventSource('/eventSource');
eventSource.onmessage = function(e){
console.log(e.data);
}
eventSource.onerror = function(err){
console.log(err);
}
</script>
服务端
事件流的对应MIME格式为text/event-stream,而且其基于HTTP长连接。针对HTTP1.1规范默认采用长连接,针对HTTP1.0的服务器需要特殊设置。
event-source必须编码成utf-8的格式,消息的每个字段使用"\n"来做分割,并且需要下面4个规范定义好的字段:
- Event: 事件类型
- Data: 发送的数据
- ID: 每一条事件流的ID
- Retry: 告知浏览器在所有的连接丢失之后重新开启新的连接等待的时间,在自动重新连接的过程中,之前收到的最后一个事件流ID会被发送到服务端
let express = require('express');
let app = express();
app.use(express.static(__dirname));
let sendCount = 1;
app.get('/eventSource',function(req,res){
res.header('Content-Type','text/event-stream',);
setInterval(() => {
res.write(`event:message\nid:${sendCount++}\ndata:${Date.now()}\n\n`);
}, 1000)
});
app.listen(8888);
let express = require('express');
let app = express();
app.use(express.static(__dirname));
const SseStream = require('ssestream');
let sendCount = 1;
app.get('/eventSource',function(req,res){
const sseStream = new SseStream(req);
sseStream.pipe(res);
const pusher = setInterval(() => {
sseStream.write({
id: sendCount++,
event: 'message',
retry: 20000, // 告诉客户端,如果断开连接后,20秒后再重试连接
data: {ts: new Date().toTimeString()}
})
}, 1000)
res.on('close', () => {
clearInterval(pusher);
sseStream.unpipe(res);
})
});
app.listen(8888);
安全问题
使用HTTPS加密数据传输
SSE基于HTTP协议,因此容易受到中间人攻击或数据泄露的风险。为了保护数据的安全性,应该使用HTTPS来加密客户端和服务器之间的数据传输。
// 在Servlet中设置HTTPS
response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
response.setHeader("Content-Security-Policy", "default-src 'self'");
防止CSRF攻击
SSE连接本身不会触发CSRF(跨站请求伪造)攻击,因为SSE是服务器向客户端的单向通信。然而,如果SSE用于触发客户端的某些操作,那么应该确保这些操作的安全性,比如通过验证请求来源或使用CSRF令牌。
防止XSS攻击
由于SSE允许服务器动态地向客户端页面发送数据,如果不正确处理,可能会成为XSS攻击的载体。确保对所有接收到的数据进行适当的清理和编码,避免直接插入到DOM中。
eventSource.onmessage = function(event) {
const safeData = encodeURI(event.data); // 对数据进行URL编码
const messageElement = document.createElement('div');
messageElement.textContent = safeData; // 安全地将数据添加到页面
document.getElementById('messages').appendChild(messageElement);
};
验证连接请求
验证所有SSE连接请求,确保它们来自可信的源。可以通过检查Referer头或使用身份验证令牌来实现。
// 检查请求来源
String refererHost = request.getHeader("Referer");
if (refererHost == null || !refererHost.contains("trusted-domain.com")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
优化
连接优化
- 「连接复用」:尽可能复用现有的连接,减少连接建立和关闭的开销。
- 「批量发送」:如果可能,批量发送数据而不是单个事件,减少数据包的数量。
- 「使用高效的序列化」:选择高效的数据序列化方法,减少数据传输的大小。
- 「超时和自动重连」:合理设置超时时间和自动重连策略,避免不必要的资源浪费。
流量消耗优化
- 优化数据传输:优化SSE的流量消耗通常涉及减少传输数据的大小和频率。使用GZIP压缩可以显著减少传输的数据量。
- 减少不必要的数据传输:仅在数据实际发生变化时才发送更新,避免发送重复或无关紧要的信息。
- 批量更新:如果可能,考虑将多个更新合并为一个数据包发送,减少消息的频率。
- 条件更新:只在客户端需要更新时才发送数据,例如通过客户端的请求参数来确定发送哪些数据。
- 连接超时和重连策略:设置合理的连接超时时间,并提供明确的重连策略,避免不必要的连接保持和频繁的重连尝试。
- 使用缓存:在客户端使用缓存来存储重复的数据,减少对服务器的请求。
- 监控流量使用:实施监控机制来跟踪SSE的流量使用情况,以便及时发现和解决流量消耗问题。
- 断开空闲连接:对于长时间空闲的连接,服务器可以主动断开,避免无谓的资源占用。
- 客户端流量控制:允许客户端控制接收数据的频率和量,例如提供暂停和恢复数据流的能力。