SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。
注意:因为EventSource对象是SSE的客户端,可能会有浏览器对其不支持
是 HTML5 遵循 W3C 标准提出的客户端和服务端之间进行实时通信的协议。
优点
缺点
是 HTML5 的一部分,提供了一种双向通信的机制。
优点
缺点
- // 建立连接
- createSseConnect(clientId){
- if(window.EventSource){
- const eventSource = new EventSource('http://127.0.0.1:8083/sse/createSseConnect?clientId='+clientId);
- console.log(eventSource)
-
- eventSource.onmessage = (event) =>{
- console.log("onmessage:"+clientId+": "+event.data)
- };
-
- eventSource.onopen = (event) =>{
- console.log("onopen:"+clientId+": "+event)
- };
-
- eventSource.onerror = (event) =>{
- console.log("onerror :"+clientId+": "+event)
- };
-
- eventSource.close = (event) =>{
- console.log("close :"+clientId+": "+event)
- };
-
- }else{
- console.log("你的浏览器不支持SSE~")
- }
- console.log(" 测试 打印")
- },
SseController
- package com.joker.cloud.linserver.controller;
-
- import com.joker.cloud.linserver.conf.sse.sseUtils;
- import com.joker.common.message.Result;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
- import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
- import java.util.Map;
-
- /**
- * SseController
- *
- * @author joker
- * @version 1.0
- * 2023/8/9 11:18
- **/
- @RestController
- @Slf4j
- @CrossOrigin
- @RequestMapping("/sse")
- public class SseController {
-
- @Autowired
- private sseUtils sseUtils;
-
-
- @GetMapping(value = "/createSseConnect", produces="text/event-stream;charset=UTF-8")
- public SseEmitter createSseConnect(@RequestParam(name = "clientId", required = false) Long clientId) {
- return sseUtils.connect(clientId);
- }
-
-
- @PostMapping("/sendMessage")
- public void sendMessage(@RequestParam("clientId") Long clientId, @RequestParam("message") String message){
- sseUtils.sendMessage(clientId, "123456789", message);
- }
-
- @GetMapping(value = "/listSseConnect")
- public Result
- Map
sseEmitterMap = sseUtils.listSseConnect(); - return Result.success(sseEmitterMap);
- }
-
-
- /**
- * 关闭SSE连接
- *
- * @param clientId 客户端ID
- **/
- @GetMapping("/closeSseConnect")
- public Result closeSseConnect(Long clientId) {
- sseUtils.deleteUser(clientId);
- return Result.success();
- }
-
- }
sseUtils工具类
- package com.joker.cloud.linserver.conf.sse;
-
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Component;
- import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
- import java.util.Map;
- import java.util.UUID;
- import java.util.concurrent.ConcurrentHashMap;
-
-
- /**
- * sseUtils
- *
- * @author joker
- * @version 1.0
- * 2023/8/9 11:20
- **/
- @Slf4j
- @Component
- public class sseUtils {
-
- private static final Map
sseEmitterMap = new ConcurrentHashMap<>(); -
- /**
- * 创建连接
- */
- public SseEmitter connect(Long userId) {
- if (sseEmitterMap.containsKey(userId)) {
- SseEmitter sseEmitter =sseEmitterMap.get(userId);
- sseEmitterMap.remove(userId);
- sseEmitter.complete();
- }
- try {
- UUID uuid = UUID.randomUUID();
- String str = uuid.toString();
- String temp = str.substring(0, 8) + str.substring(9, 13) + str.substring(14, 18) + str.substring(19, 23) + str.substring(24);
-
- // 设置超时时间,0表示不过期。默认30秒
- SseEmitter sseEmitter = new SseEmitter(30*1000L);
- sseEmitter.send(SseEmitter.event().id(temp).data(""));
- // reconnectTime(10*1000L)
- // 注册回调
- sseEmitter.onCompletion(completionCallBack(userId));
- // sseEmitter.completeWithError(errorCallBack(userId));
- sseEmitter.onTimeout(timeoutCallBack(userId));
- sseEmitterMap.put(userId, sseEmitter);
- log.info("创建sse连接完成,当前用户:{}", userId);
- return sseEmitter;
- } catch (Exception e) {
- log.info("创建sse连接异常,当前用户:{}", userId);
- }
- return null;
- }
-
- /**
- * 给指定用户发送消息
- *
- */
- public boolean sendMessage(Long userId,String messageId, String message) {
- if (sseEmitterMap.containsKey(userId)) {
- SseEmitter sseEmitter = sseEmitterMap.get(userId);
- try {
- sseEmitter.send(SseEmitter.event().id(messageId).data(message));
- // reconnectTime(10*1000L)
- log.info("用户{},消息id:{},推送成功:{}", userId,messageId, message);
- return true;
- }catch (Exception e) {
- sseEmitterMap.remove(userId);
- log.info("用户{},消息id:{},推送异常:{}", userId,messageId, e.getMessage());
- sseEmitter.complete();
- return false;
- }
- }else {
- log.info("用户{}未上线", userId);
- }
- return false;
- }
-
- /**
- * 删除连接
- * @param userId
- */
- public void deleteUser(Long userId){
- removeUser(userId);
- }
-
- private static Runnable completionCallBack(Long userId) {
- return () -> {
- log.info("结束sse用户连接:{}", userId);
- removeUser(userId);
- };
- }
-
- private static Throwable errorCallBack(Long userId) {
- log.info("sse用户连接异常:{}", userId);
- removeUser(userId);
- return new Throwable();
- }
-
- private static Runnable timeoutCallBack(Long userId) {
- return () -> {
- log.info("连接sse用户超时:{}", userId);
- removeUser(userId);
- };
- }
-
- /**
- * 断开
- * @param userId
- */
- public static void removeUser(Long userId){
- if (sseEmitterMap.containsKey(userId)) {
- SseEmitter sseEmitter = sseEmitterMap.get(userId);
- sseEmitterMap.remove(userId);
- sseEmitter.complete();
- }else {
- log.info("用户{} 连接已关闭",userId);
- }
- }
-
- public Map
listSseConnect(){ - return sseEmitterMap;
- }
- }
模拟浏览器发送建立连接的请求:

切换到时间栏目,可以看到长连接始终保持着的:

切换到eventStream:可以看到后端通信的streams流数据

使用postMan 模拟后端服务器推送给客户端消息

浏览器建立的连接中会看到服务器推送到客户端的消息内容及ID等基础信息

控制台也可以监听到事件的变化并输出
