使用SSE进行实时消息推送!替换WebSocket,轻量好用~

SSE简介

sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。

长链接是一种HTTP/1.1的持久连接技术,它允许客户端和服务器在一次TCP连接上进行多个HTTP请求和响应,而不必为每个请求/响应建立和断开一个新的连接。长连接有助于减少服务器的负载和提高性能。

SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且SSE使用的是http协议(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。

注意:IE大魔王不支持SSE

SSE对于各大浏览器的兼容性↓

注意,上图是SSE对于浏览器的兼容不是对于服务端的兼容。

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相关文档:

https://www.w3cschool.cn/nwfchn/wpi3cozt.html

如何实操一个SSE链接?Demo

这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。

后端选用语言是node,框架是Express。

理论上,把这两段端代码复制过去跑起来就直接可以用了。

  • 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  • 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行

    npm init //初始化npm
    npm i express //下载node express框架
    node index //启动服务

在这一层文件夹下执行命令。

完成以上操作就可以把项目跑起来了

前端代码Demo

复制代码
<!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{
    thrownewError("当前浏览器不支持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>

后端代码Demo(node的express)

复制代码
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}`)
})

JAVA编写SSE服务,来进行创建链接和发送消息

Service:
*

复制代码
  package com.zillion.aggregate.app.controller;


  import lombok.extern.slf4j.Slf4j;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.stereotype.Service;
  import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

  import java.io.IOException;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;

  @Slf4j
  @Service
  public class SSEService {

      private static final Map<String,SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

      public SseEmitter crateSse(String uid) {
          SseEmitter sseEmitter = new SseEmitter(0L);
          sseEmitter.onCompletion(() -> {
              log.info("[{}]结束链接" , uid);
              sseEmitterMap.remove(uid);
          });
          sseEmitter.onTimeout(() -> {
              log.info("[{}]链接超时",uid);
          });
          sseEmitter.onError(throwable -> {
              try{
                  log.info("[{}]链接异常,{}",uid,throwable.toString());
                  sseEmitter.send(SseEmitter.event()
                          .id(uid)
                          .name("发生异常")
                          .data("发生异常请重试")
                          .reconnectTime(3000));
                  sseEmitterMap.put(uid,sseEmitter);
              }catch (IOException e){
                  e.printStackTrace();
              }
          });
          try{
              sseEmitter.send(SseEmitter.event().reconnectTime(5000));
          }catch (IOException e){
              e.printStackTrace();
          }
          sseEmitterMap.put(uid,sseEmitter);
          log.info("[{}]创建sse连接成功!",uid);
          return sseEmitter;
      }

      public boolean sendMessage(String uid,String messageId,String message){
          if(StringUtils.isEmpty(message)){
              log.info("[{}]参数异常,msg为空",uid);
              return false;
          }
          SseEmitter sseEmitter = sseEmitterMap.get(uid);
          if(sseEmitter == null){
              log.info("[{}]sse连接不存在",uid);
              return  false;
          }
          try{
              sseEmitter.send(SseEmitter.event().id(messageId).reconnectTime(60000).data(message));
              log.info("用户{},消息ID:{},推送成功:{}",uid,messageId,message);
              return true;
          }catch (IOException e){
              sseEmitterMap.remove(uid);
              log.info("用户{},消息ID:{},消息推送失败:{}",uid,messageId,message);
              sseEmitter.complete();
              return false;
          }
      }

      public void closeSse(String uid){
          if(sseEmitterMap.containsKey(uid)){
              SseEmitter sseEmitter = sseEmitterMap.get(uid);
              sseEmitter.complete();
              sseEmitterMap.remove(uid);
          }else {
              log.info("用户{}连接已关闭",uid);
          }
      }

  }
  • Controller:

复制代码
  package com.zillion.aggregate.app.controller;


  import cn.hutool.core.util.IdUtil;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.stereotype.Controller;
  import org.springframework.web.bind.annotation.*;
  import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;

  @Controller
  @RequestMapping("/aggregate/api/pay")
  public class TestController {

      private static final Map<String,Boolean> SEND_MAP = new ConcurrentHashMap<>();

      @Autowired
      private SSEService sseService;
      @GetMapping("createSse")
      @CrossOrigin
      public SseEmitter createSse(String uid)
      {
          return sseService.crateSse(uid);
      }

      @GetMapping("/sendMsg")
      @ResponseBody
      @CrossOrigin
      public SseEmitter sendMsg(@RequestParam("uid") String uid) throws InterruptedException {
          SseEmitter sseEmitter = sseService.crateSse(uid);
          if (SEND_MAP.get(uid)==null ||  !SEND_MAP.get(uid)){
               new Thread(()->{
                   int i=0;
                   while (true){
                       try {
                           i++;
                           String message = "uid:"+uid+" number:"+i+" message:"+IdUtil.fastUUID().replace("-", "");
                           sseService.sendMessage(uid,"消息"+i,message);
                           SEND_MAP.put(uid,true);
                           Thread.sleep(1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                           closeSse(uid);
                       }
                   }
               }).start();
           }

          return sseEmitter;
      }

      @GetMapping("closeSse")
      @CrossOrigin
      public void closeSse(String uid){
          sseService.closeSse(uid);
      }
  }

5.前端实现消息监听

复制代码
  <!doctype html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>SSE消息推送监听</title>
  </head>
  <body>
      <div id="conMsg"></div>
  <script>
      let uid = 1;
      let chat = document.getElementById("conMsg");
      if(window.EventSource){
          var eventSource = new EventSource(`http://localhost:9001/aggregate/aggregate/api/pay/sendMsg?interfaceId=CEDB297CECCC9DCBAD348204ACDD5BAD&uid=${uid}`);
          eventSource.onopen = ()=>{
              console.log("链接成功");
          }
          eventSource.onmessage = (ev)=>{
              if(ev.data){
                  chat.innerHTML += ev.data+"<br>";
              }
          }
          eventSource.onerror = ()=>{
              console.log("sse链接失败")
          }
      }else{
          alert("当前浏览器不支持sse")
      }
  </script>
  </body>
  </html>
  • SSE比websocket更轻

  • SSE是基于http/https协议的

  • websocket是一个新的协议,ws/wss协议

  • 如果只需要服务端向客户端推送消息,推荐使用SSE

  • 如果需要服务端和客户端双向推送,请选择websocket

  • 不论是SSE还是websocket,对于浏览器的兼容性都不错

  • 轮询是下策,很占用客户端资源,不建议使用。(不过偷懒的时候他确实方便)

  • IE不支持SSE

  • 小程序不支持sse

相关推荐
Muroidea8 小时前
Kafka4.1.0 队列模式尝鲜
java·kafka
wudl55668 小时前
python字符串处理与正则表达式--之八
开发语言·python·正则表达式
程序员爱钓鱼8 小时前
Python编程实战 - 面向对象与进阶语法 - 继承与多态
后端·python·ipython
程序员爱钓鱼8 小时前
Python编程实战 - 面向对象与进阶语法 - 封装与私有属性
后端·python·ipython
nvd118 小时前
python异步编程 -- 理解协程函数和协程对象
开发语言·python
IT_陈寒8 小时前
Spring Boot 3.2性能翻倍!我仅用5个技巧就让接口响应时间从200ms降到50ms
前端·人工智能·后端
.豆鲨包8 小时前
【Android】Lottie - 实现炫酷的Android导航栏动画
android·java
陌路208 小时前
Linux22 进程与线程以及内核级线程
linux·开发语言
风象南8 小时前
Spring Boot 手撸一个自助报表系统
后端