如果让你实现实时消息推送你会用什么技术?轮询、websocket还是sse
前言
在日常的开发过程中,我们经常能碰见到需要主动推送消息给客户端数据的业务场景,比如数据大屏幕实时数据,聊天消息推送等等。
本文介绍sse:
服务端向客户端推送数据的方式有哪几种呢?
- WebSocket
- SSE
- 长轮询
轮询简介
长轮询是一种模拟实时通信的技术。在传统的Http请求中,客户端向服务端发送请求,并且在完成请求后立即响应,然后连接关闭。这意味着客户端需要不停的发送请求来更新数据。
相比之下,长轮询的思想是客户端发送一个Http到服务端,服务端不立即返回响应。相反,服务端会保持该请求打开,直到有新的数据可用或超时。如果有新的数据可用,服务端会立即返回响应,并关闭连接。此时,客户端会重新发起一个新的请求,继续等待新的数据。
使用长轮询的优势在于,它在大部分的浏览器中有更好的兼容性,因为它使用的是Http协议。缺点就是较高的延迟性、较大的资源消耗、以及大量并发操作可能导致服务端资源的瓶颈和一些浏览器对并发请求数目进行了限制比如chorme最大并发数目为6,这个限制前提是针对同一个域名下,超过这一限制后续请求就会堵塞。
websocket简介
websocket是一个双向通信的协议,它支持客户端和服务端彼此之间进行通信。功能强大。
缺点就是是一个新的协议,ws/wss,也就是说支持http协议的不一定支持ws协议。相比较websocket结构复杂并且比较重。
SSE简介
sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。
SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且_SSE使用的是http协议_(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。
websocket和SSE有什么区别?
轮询
对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。
Websocket和SSE
我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。
SSE的官方对于SSE和Websocket的评价是
- WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。
- WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。
- SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。
- SSE默认支持断线重连,WebSocket则需要额外部署。
- SSE支持自定义发送的数据类型。
Websocket和SSE分别适用于什么业务场景?
对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。
比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。
对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯_。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。
SSE有哪些主要的API?
建立一个SSE链接 :var source = new EventSource(url);
SSE连接状态
source.readyState
- 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。
- 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
- 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。
SSE相关事件
- open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)
- message事件(收到数据就会触发message事件)
- error事件(如果发生通信错误(比如连接中断),就会触发error事件)
数据格式
Content-Type: text/event-stream //文本返回格式
Cache-Control: no-cache //不要缓存
Connection: keep-alive //长链接标识
如何实操一个SSE链接?Demo↓
这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。后端选用语言是node,框架是Express。
理论上,把这两段端代码复制过去跑起来就直接可以用了。
- 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件
- 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行
npm init //初始化npm
npm i express //下载node express框架
node index //启动服务
上面三行之中,第一行的Content-Type
必须指定 MIME 类型为event-steam
。
每一次发送的信息,由若干个message
组成,每个message
之间用\n\n
分隔。每个message
内部由若干行组成,每一行都是如下格式。
[field]: value\n
上面的field
可以取四个值。
- data
- event
- id
- retry
此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
: This is a comment
data 字段
数据内容用data
字段表示
data: message\n\n
如果数据很长,可以分成多行,最后一行用\n\n
结尾,前面行都用\n
结尾。
data: begin message\n
data: continue message\n\n
下面是一个发送 JSON 数据的例子。
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
id 字段
数据标识符用id
字段表示,相当于每一条数据的编号。
id: msg1\n
data: message\n\n
浏览器用lastEventId
属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID
头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
event 字段
event
字段表示自定义的事件类型,默认是message
事件。浏览器可以用addEventListener()
监听该事件。
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: bar\n
data: a bar event\n\n
retry 字段
服务器可以用retry
字段,指定浏览器重新发起连接的时间间隔。
retry: 10000\n
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
上面的代码创造了三条信息。第一条的名字是foo
,触发浏览器的foo
事件;第二条未取名,表示默认类型,触发浏览器的message
事件;第三条是bar
,触发浏览器的bar
事件。
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul id="ul"></ul>
</body>
<script>
//生成li元素
function createLi(data) {
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}
//判断当前浏览器是否支持SSE
let source = "";
if (!!window.EventSource) {
source = new EventSource("http://localhost:8088/sse/");
} else {
throw new Error("当前浏览器不支持SSE");
}
//对于建立链接的监听
source.onopen = function (event) {
console.log(source.readyState);
console.log("长连接打开");
};
//对服务端消息的监听
source.onmessage = function (event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li);
};
//对断开链接的监听
source.onerror = function (event) {
console.log(source.readyState);
console.log("长连接中断");
};
</script>
</html>
后端代码
const express = require("express"); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口
//设置跨域访问
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
//允许的header类型
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
app.get("/sse", (req, res) => {
res.set({
"Content-Type": "text/event-stream", //设定数据类型
"Cache-Control": "no-cache", // 长链接拒绝缓存
Connection: "keep-alive", //设置长链接
});
console.log("进入到长连接了");
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing");
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`,
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
});
//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`);
});
参考文章:http://www.ruanyifeng.com/blog/2017/0…
来源:juejin.cn/post/7340621143009067027