前言
服务端推送,又称为消息推送或通知推送,是一种让应用服务器能够主动向客户端发送信息的功能,使客户端能够实时接收信息更新和通知,从而显著提升用户体验。
服务端推送的需求背景主要体现在以下几个方面:
- 实时通知:在许多应用场景中,用户希望即时接收到应用的通知,例如新消息提示、商品活动提醒等。
- 节省资源:如果缺少服务端推送功能,客户端通常会采用轮询的方式检查新信息,这样不仅效率低下,还会造成客户端和服务器资源的浪费。而通过服务端推送技术,客户端仅需在接收到通知时作出响应,有效减少了资源消耗。
- 增强用户体验:借助服务端推送,应用可以根据用户的兴趣和偏好,向特定用户或用户群体发送定制化内容,比如促销活动、个性化推荐等,从而增强用户对应用的好感度和忠诚度。
常见的推送应用场景包括:微信消息通知、新闻资讯更新、外卖订单状态变化等。我们的推送应用场景则涵盖了文件下载进度提示、连接请求通知、直播活动提醒等。
好的,既然你打算撰写一篇关于不同实时通信技术的文章,我们可以先概述每种技术的特点,再深入介绍SSE。以下是对WebSocket、长轮询、段轮询(短轮询)以及SSE服务端推送技术的比较:
解决方案
短轮询(Short Polling)
优点:
- 实现最简单,客户端定时向服务器发送请求获取更新。
- 不需要服务器端做特殊处理,兼容所有浏览器。
缺点:
- 极大的浪费资源,特别是在没有更新的情况下频繁请求。
- 服务器负载高,因为每次请求都需要响应。
- 实时性较差,取决于轮询的时间间隔。
长轮询(Long Polling)
优点:
- 相对容易实现,只需要标准的HTTP协议。
- 兼容性好,几乎所有的浏览器和服务器环境都支持。
- 对于间歇性更新的应用来说,性能较好。
缺点:
- 效率较低,因为每个请求都需要完整的HTTP握手过程。
- 服务器压力较大,因为需要保持大量的未完成请求。
- 无法实现真正的双向通信,只能由服务器推送给客户端。
Server-Sent Events (SSE)
优点:
- 仅支持服务器向客户端单向推送数据,但实现简单,适合简单的实时应用。
- 轻量级,基于HTTP/1.1协议,大多数现代浏览器都支持。
- 开销较小,因为只在有更新时才发送数据。
缺点:
- 只支持单向通信,客户端不能向服务器发送消息。
- 如果客户端长时间没有接收数据,连接可能会被断开,需要重新建立。
- 对于复杂交互的需求,可能需要额外的技术支持。
WebSocket
优点:
- 提供全双工通信能力,即客户端和服务器都可以主动向对方发送数据。
- 一旦建立连接,就可以高效地传输大量数据。
- 减少了握手和重连的开销,因为连接是一直保持的。
- 可以用于构建复杂的实时应用,如在线游戏、聊天室等。
缺点:
- 实现复杂度较高,需要处理心跳机制、错误恢复等。
- 不是所有浏览器都支持WebSocket,尽管现代浏览器普遍支持。
- 对于简单应用来说,可能过于复杂且消耗资源较多。
大家可能对于SSE比较陌生,本文重点介绍SSE
SSE是一种轻量级的解决方案,用于实现实时更新功能。它特别适用于那些需要服务器定期向客户端推送更新数据的应用场景,如股票价格更新、新闻推送等。SSE的主要优势在于其简单易用的特性,同时它也是基于HTTP协议,这意味着它可以很好地与现有的Web架构集成。
SSE的工作原理是服务器维持一个开放的HTTP连接,并在有新的事件发生时通过这个连接将数据发送给客户端。客户端通过监听这个连接来接收服务器发送的数据。这种方式相比于WebSocket,减少了实现的复杂性,同时也比长轮询和短轮询更有效率。
客户端
这里我们采用最简单的方式,使用html/jsp也方便大家快速搭建 大家在创建springboot项目后,找到如图所示位置,直接创建文件即可(文件名任意,本文演示使用sse.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE</title>
</head>
<body>
<div id="result"></div>
<script>
// 连接服务器 需要改成自己的地址
var sseSource = new EventSource("http://localhost:8887/sse/connect");
// 连接打开
sseSource.onopen = function () {
console.log("连接打开");
}
// 连接错误
sseSource.onerror = function (err) {
console.log("连接错误:", err);
// sseSource.close()
}
// 接收到数据
sseSource.onmessage = function (event) {
console.log("接收到数据:", event);
let div = document.createElement('div'); // 创建一个div节点
div.innerHTML = event.data;
document.getElementById('result').appendChild(div); // 在result里添加子节点
}
</script>
</body>
</html>
服务端
使用spring/springboot框架之后,整合起来非常的方便,只需要引入
web
依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
后端代码如下
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
@GetMapping(path = "/connect")
public SseEmitter sse() {
SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE); // 设置最大超时时间为Long.MAX_VALUE毫秒
try {
// 在这里可以发送一个欢迎消息
sseEmitter.send(SseEmitter.event()
.id("welcome")
.name("message")// 大家可以更改此事件的名称,查看能否收到该信息
.data("Welcome to SSE with Spring!"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 可以在另一个线程中继续发送数据
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000); // 模拟延迟
sseEmitter.send("Message " + i);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
sseEmitter.complete(); // 完成发送后关闭连接
}).start();
return sseEmitter;
}
现在我们启动项目,通过访问 http://localhost:8887/sse.html
可以看到页面持续显示后端传输过来的数据。按理说,根据我们的代码逻辑,显示到【Message 9】后就应该停止。然而,令人困惑的是,为什么它会在达到【Message 9】之后,又从【Message 0】开始重复显示呢?
问题的原因在于:虽然理论上连接应该在此时终止,并且我们在 Java 代码中也确实执行了关闭操作。但当 SseEmitter
关闭连接时,浏览器中的 EventSource
对象会触发 onerror
事件。这是因为 SseEmitter
的 complete()
方法在关闭连接的同时,浏览器将其解释为一个错误信号,从而触发了重试机制,导致了无限循环的现象。
解决这一问题的一个简单方法是取消对 sseSource.close()
的注释,从而手动关闭连接。
注意:onmessage
只能监听message事件,当我们使用了其他事件时便无法接收到信息,比如我们使用了
sseEmitter.send(SseEmitter.event()
.id("welcome")
.name("testMessage") // 事件名称
.data("Welcome to SSE with Spring!"));
大家可以看看能否接收到信息。那么解决方法就是既然我们无法监听该事件了,那么我们就添加一个事件来监听不就好了
sseSource.addEventListener('testMessage', function (event) {
console.log("接收到 greeting 数据:", event);
let div = document.createElement('div');
div.innerHTML = event.data;
document.getElementById('result').appendChild(div);
});
此时还有一种更加简单的方式来实现
@GetMapping(path = "/connect")
public Flux<String> sse() {
// 作用就是每一秒发送一条信息
return Flux.interval(Duration.ofSeconds(1)).map(i -> "Data: " + i);
}
这两种方式都能实现Server-Sent Events (SSE) 的原因在于它们都满足了SSE的基本要求,即服务器能够向客户端推送数据。不过,它们的工作原理和适用场景有所不同。
当然大家也可以不访问该html
而是直接输入 http://localhost:8887/sse/connect (小伙伴们自己调整自己的url),可以看到也可以源源不断地打印我们的数据。
结论
每种技术都有其适用场景。选择哪种技术主要取决于你的应用需求、性能要求以及技术栈的复杂程度。对于需要双向通信的应用,WebSocket可能是更好的选择;而对于只需要服务器向客户端推送数据的简单实时应用,SSE是一个很好的选择。