轻松实现服务器事件推送:Spring SseEmitter 详解
引言
服务器推送技术背景简介
服务器推送(Server Push)技术允许网站和应用在有新内容时主动向用户推送更新,而无需用户主动查询。与传统的"拉"模型不同,服务器推送采用"推"的方式将信息直接发送到客户端。其优点主要有:
- 用户体验更流畅:用户无需刷新页面即可获取最新内容,系统会在有新消息时自动推送给客户端。
- 更高效:服务器仅在有新内容时主动推送,减少了不必要的客户端请求。
常见的服务器推送技术包括:
- 长轮询:客户端发起一个长时间的请求,服务器在有新内容时响应。虽然效率不高,但兼容性较好。
- SSE (Server Sent Events):服务器持续向客户端推送事件,客户端只需监听一个事件源。兼容性一般。
- WebSocket:基于TCP的双向通信,服务器和客户端建立持久连接,允许双向实时消息传输。兼容性较差但效率高。
Spring 的 SseEmitter
使用了 SSE 技术来实现服务器推送,与传统的 HTTP 长连接不同,它允许 Spring 服务主动向浏览器推送消息,显著提升用户体验。例如,在聊天应用中,只有在有新消息时才会主动推送,让用户感觉信息即时到达。
SseEmitter 的功能和用途
SseEmitter
的主要功能是允许服务器主动将信息推送给浏览器客户端。其主要特点包括:
- 主动向单个客户端推送消息:
SseEmitter
能匹配唯一的客户端请求,并与之保持持久连接,通过该连接随时推送事件。 - 推送重复的消息:允许服务器不停发送相同的消息,形成连续的事件流,客户端只需监听该事件流即可。
- 支持延迟和定时推送:通过
@Scheduled
注解,服务器可以在指定时间推送延迟的事件。 - 支持不同类型的事件:客户端可根据事件名称区分不同类型的事件,并作出相应响应。
- 支持推送基本数据类型和 POJO 对象:服务器可以推送
String
、int
等基本类型,也可推送任意的 Java 对象。 - 主动通知客户端关闭连接:通过调用
complete()
或error()
方法,服务器可以主动告知客户端连接已关闭。 - 解耦服务器端和客户端:服务器端仅负责推送事件,与具体的客户端无关。
总的来说,SseEmitter
让服务器端能够主动推送信息给单个浏览器客户端,实现服务器推送的功能。这对于实时通信、实时消息推送非常有用,能够显著提高用户体验。
准备工作
引入 Maven 依赖
SseEmitter
包含在 spring-webmvc
包中,如果是 Spring Boot 项目,确保已引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
使用 SseEmitter
以下是 Controller
接口的代码示例。首先同步返回一个建立的 SseEmitter
连接给客户端,然后在异步线程中进行数据推送。为了防止串流以及支持客户端主动停止推流,每次请求需携带唯一的客户端 ID。
@GetMapping(value = "test/{clientId}", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
@ApiOperation(value = "建立连接")
public SseEmitter test(@PathVariable("clientId") @ApiParam("客户端 id") String clientId) {
final SseEmitter emitter = service.getConn(clientId);
CompletableFuture.runAsync(() -> {
try {
service.send(clientId);
} catch (Exception e) {
throw new BusinessException("推送数据异常");
}
});
return emitter;
}
@GetMapping("closeConn/{clientId}")
@ApiOperation(value = "关闭连接")
public Result<String> closeConn(@PathVariable("clientId") @ApiParam("客户端 id") String clientId) {
service.closeConn(clientId);
return Result.success("连接已关闭");
}
Service 层相关代码
private static final Map<String, SseEmitter> SSE_CACHE = new ConcurrentHashMap<>();
@Override
public SseEmitter getConn(@NotBlank String clientId) {
final SseEmitter sseEmitter = SSE_CACHE.get(clientId);
if (sseEmitter != null) {
return sseEmitter;
} else {
final SseEmitter emitter = new SseEmitter(600_000L);
emitter.onTimeout(() -> {
logger.info("连接已超时,准备关闭,clientId = {}", clientId);
SSE_CACHE.remove(clientId);
});
emitter.onCompletion(() -> {
logger.info("连接已关闭,准备释放,clientId = {}", clientId);
SSE_CACHE.remove(clientId);
});
emitter.onError(throwable -> {
logger.error("连接异常,准备关闭,clientId = {}", clientId, throwable);
SSE_CACHE.remove(clientId);
});
SSE_CACHE.put(clientId, emitter);
return emitter;
}
}
@Override
public void send(@NotBlank String clientId) throws IOException {
final SseEmitter emitter = SSE_CACHE.get(clientId);
emitter.send("此去经年", MediaType.APPLICATION_JSON);
emitter.send("此去经年,应是良辰好景虚设");
emitter.send("此去经年,应是良辰好景虚设,便纵有千种风情");
emitter.send("此去经年,应是良辰好景虚设,便纵有千种风情,更与何人说");
emitter.complete();
}
@Override
public void closeConn(@NotBlank String clientId) {
final SseEmitter sseEmitter = SSE_CACHE.get(clientId);
if (sseEmitter != null) {
sseEmitter.complete();
}
}
接口调试
如果在推送数据过程中客户端主动停止推送,可以直接调用关闭连接的接口。
注意事项
推送数据结束后,不要在 finally
块中调用 emitter.complete()
来关闭连接,否则可能触发 502 Bad Gateway 的异常。建议参考排查过程避免类似问题。
与 WebSocket 对比
SSE
和 WebSocket
的主要区别在于:
连接方式:
- SSE:客户端发送一个长连接请求,服务器通过 HTTP 响应推送事件。
- WebSocket:建立双向通信,保持实时双向消息传输。
传输效率:
- SSE:需频繁建立和关闭连接,效率不如 WebSocket。
- WebSocket:保持长连接,效率更高。
兼容性:
- SSE:原生支持的浏览器较少,需要 Polyfill。
- WebSocket:现代浏览器全面支持。
传输内容:
- SSE:只支持推送文本,不支持二进制数据。
- WebSocket:支持推送文本和二进制数据。
功能:
- SSE:仅支持服务器主动推送,客户端被动接收。
- WebSocket:支持双向通信,客户端和服务器均可主动发送消息。
使用场景:
- SSE:适用于服务器单向推送文本事件的场景。
- WebSocket:适用于需要实时双向交互的场景。
总的来说,SSE 适用于服务器单向推送的场景,兼容性稍差但效率较高;而 WebSocket 更适合实时双向通信的场景,效率更高但兼容性要求较高。