• SpringBoot / Vue 对SSE的基本使用(简单上手)


    一、SSE是什么?

    SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。

    注意:因为EventSource对象是SSE的客户端,可能会有浏览器对其不支持

    二、sse 与 websoket

    SSE(Server-Sent Events)

    是 HTML5 遵循 W3C 标准提出的客户端和服务端之间进行实时通信的协议。

    优点

    • SSE 客户端可以接收来自服务器的“流”数据,而不需要进行轮询。由于没有浪费的请求,因此 SSE 对于减轻服务器的压力非常有用。
    • SSE 使用纯 JavaScript 实现简单,不需要额外的插件或库来处理消息。客户端可以使用 EventSource 接口轻松地与 SSE 服务器通信。
    • SSE 天生具有自适应性,由于 SSE 是基于 HTTP 响应使用 EventStream 传递消息,因此它利用了 HTTP 的开销和互联网上的结构。
    • SSE 可以与任何服务器语言和平台一起使用,因为 SSE 是一种规定了消息传递方式的技术,不依赖于具体的服务器语言和平台。

    缺点

    • SSE 是单向通信,只能从服务器推送到客户端。如果应用程序需要双向通信,就需要使用 Websocket。
    • SSE无法发送二进制数据,只能发送 UTF-8 编码的文本。如果应用程序需要发送二进制数据,就需要使用 Websocket。
    • SSE 不是所有浏览器都支持。虽然 SSE 是 HTML5 的一部分,但具体的浏览器支持性可能会有差异。

    Websocket

    是 HTML5 的一部分,提供了一种双向通信的机制。

    优点

    • Websocket 支持双向通信。使用 Websocket 可以同时向客户端发送和接收数据。
    • Websocket 协议可以传输二进制数据,这使得 Websocket 更加灵活和强大。
    • Websocket 连接长期存在,而不需要仅仅为了接收数据而保持 HTTP 连接打开。
    • Websocket 的实现支持跨域的通信,可以方便地进行跨域通信。

    缺点

    • Websocket 不支持所有浏览器。特别是老浏览器可能不支持 Websocket 协议。
    • Websocket 是一种全双工的通信方式。由于 Websocket 长期存在,会占用服务器资源。在高并发场景下,应该考虑使用 SSE。

    三、前端示例代码:

    1. // 建立连接
    2. createSseConnect(clientId){
    3. if(window.EventSource){
    4. const eventSource = new EventSource('http://127.0.0.1:8083/sse/createSseConnect?clientId='+clientId);
    5. console.log(eventSource)
    6. eventSource.onmessage = (event) =>{
    7. console.log("onmessage:"+clientId+": "+event.data)
    8. };
    9. eventSource.onopen = (event) =>{
    10. console.log("onopen:"+clientId+": "+event)
    11. };
    12. eventSource.onerror = (event) =>{
    13. console.log("onerror :"+clientId+": "+event)
    14. };
    15. eventSource.close = (event) =>{
    16. console.log("close :"+clientId+": "+event)
    17. };
    18. }else{
    19. console.log("你的浏览器不支持SSE~")
    20. }
    21. console.log(" 测试 打印")
    22. },

    四、后端示例代码:

    SseController

    1. package com.joker.cloud.linserver.controller;
    2. import com.joker.cloud.linserver.conf.sse.sseUtils;
    3. import com.joker.common.message.Result;
    4. import lombok.extern.slf4j.Slf4j;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.web.bind.annotation.*;
    7. import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
    8. import java.util.Map;
    9. /**
    10. * SseController
    11. *
    12. * @author joker
    13. * @version 1.0
    14. * 2023/8/9 11:18
    15. **/
    16. @RestController
    17. @Slf4j
    18. @CrossOrigin
    19. @RequestMapping("/sse")
    20. public class SseController {
    21. @Autowired
    22. private sseUtils sseUtils;
    23. @GetMapping(value = "/createSseConnect", produces="text/event-stream;charset=UTF-8")
    24. public SseEmitter createSseConnect(@RequestParam(name = "clientId", required = false) Long clientId) {
    25. return sseUtils.connect(clientId);
    26. }
    27. @PostMapping("/sendMessage")
    28. public void sendMessage(@RequestParam("clientId") Long clientId, @RequestParam("message") String message){
    29. sseUtils.sendMessage(clientId, "123456789", message);
    30. }
    31. @GetMapping(value = "/listSseConnect")
    32. public Result> listSseConnect(){
    33. Map sseEmitterMap = sseUtils.listSseConnect();
    34. return Result.success(sseEmitterMap);
    35. }
    36. /**
    37. * 关闭SSE连接
    38. *
    39. * @param clientId 客户端ID
    40. **/
    41. @GetMapping("/closeSseConnect")
    42. public Result closeSseConnect(Long clientId) {
    43. sseUtils.deleteUser(clientId);
    44. return Result.success();
    45. }
    46. }

    sseUtils工具类

    1. package com.joker.cloud.linserver.conf.sse;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.stereotype.Component;
    4. import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
    5. import java.util.Map;
    6. import java.util.UUID;
    7. import java.util.concurrent.ConcurrentHashMap;
    8. /**
    9. * sseUtils
    10. *
    11. * @author joker
    12. * @version 1.0
    13. * 2023/8/9 11:20
    14. **/
    15. @Slf4j
    16. @Component
    17. public class sseUtils {
    18. private static final Map sseEmitterMap = new ConcurrentHashMap<>();
    19. /**
    20. * 创建连接
    21. */
    22. public SseEmitter connect(Long userId) {
    23. if (sseEmitterMap.containsKey(userId)) {
    24. SseEmitter sseEmitter =sseEmitterMap.get(userId);
    25. sseEmitterMap.remove(userId);
    26. sseEmitter.complete();
    27. }
    28. try {
    29. UUID uuid = UUID.randomUUID();
    30. String str = uuid.toString();
    31. String temp = str.substring(0, 8) + str.substring(9, 13) + str.substring(14, 18) + str.substring(19, 23) + str.substring(24);
    32. // 设置超时时间,0表示不过期。默认30秒
    33. SseEmitter sseEmitter = new SseEmitter(30*1000L);
    34. sseEmitter.send(SseEmitter.event().id(temp).data(""));
    35. // reconnectTime(10*1000L)
    36. // 注册回调
    37. sseEmitter.onCompletion(completionCallBack(userId));
    38. // sseEmitter.completeWithError(errorCallBack(userId));
    39. sseEmitter.onTimeout(timeoutCallBack(userId));
    40. sseEmitterMap.put(userId, sseEmitter);
    41. log.info("创建sse连接完成,当前用户:{}", userId);
    42. return sseEmitter;
    43. } catch (Exception e) {
    44. log.info("创建sse连接异常,当前用户:{}", userId);
    45. }
    46. return null;
    47. }
    48. /**
    49. * 给指定用户发送消息
    50. *
    51. */
    52. public boolean sendMessage(Long userId,String messageId, String message) {
    53. if (sseEmitterMap.containsKey(userId)) {
    54. SseEmitter sseEmitter = sseEmitterMap.get(userId);
    55. try {
    56. sseEmitter.send(SseEmitter.event().id(messageId).data(message));
    57. // reconnectTime(10*1000L)
    58. log.info("用户{},消息id:{},推送成功:{}", userId,messageId, message);
    59. return true;
    60. }catch (Exception e) {
    61. sseEmitterMap.remove(userId);
    62. log.info("用户{},消息id:{},推送异常:{}", userId,messageId, e.getMessage());
    63. sseEmitter.complete();
    64. return false;
    65. }
    66. }else {
    67. log.info("用户{}未上线", userId);
    68. }
    69. return false;
    70. }
    71. /**
    72. * 删除连接
    73. * @param userId
    74. */
    75. public void deleteUser(Long userId){
    76. removeUser(userId);
    77. }
    78. private static Runnable completionCallBack(Long userId) {
    79. return () -> {
    80. log.info("结束sse用户连接:{}", userId);
    81. removeUser(userId);
    82. };
    83. }
    84. private static Throwable errorCallBack(Long userId) {
    85. log.info("sse用户连接异常:{}", userId);
    86. removeUser(userId);
    87. return new Throwable();
    88. }
    89. private static Runnable timeoutCallBack(Long userId) {
    90. return () -> {
    91. log.info("连接sse用户超时:{}", userId);
    92. removeUser(userId);
    93. };
    94. }
    95. /**
    96. * 断开
    97. * @param userId
    98. */
    99. public static void removeUser(Long userId){
    100. if (sseEmitterMap.containsKey(userId)) {
    101. SseEmitter sseEmitter = sseEmitterMap.get(userId);
    102. sseEmitterMap.remove(userId);
    103. sseEmitter.complete();
    104. }else {
    105. log.info("用户{} 连接已关闭",userId);
    106. }
    107. }
    108. public Map listSseConnect(){
    109. return sseEmitterMap;
    110. }
    111. }

    五、模拟测试:

    模拟浏览器发送建立连接的请求:

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

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

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

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

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

  • 相关阅读:
    计算机是如何工作的下篇
    Navicat 现已支持 OceanBase 企业版
    GO微服务实战第三十三节 如何处理 Go 错误异常与并发陷阱?
    小测试:HashSet可以插入重复的元素吗?
    【GAMES104 Lec3】组件化思想 优于 继承
    优雅处理返回信息状态码:Result对象在Spring Boot中的应用
    Vue中如何进行瀑布流布局与图片加载优化
    centos下的yum安装出现的问题
    接口测试及常用接口测试工具
    固定VMware中Linux系统的ip地址
  • 原文地址:https://blog.csdn.net/Jokers_lin/article/details/132733496