SSE – Server Sent Events – 服务端主动推送

发布时间 2023-11-15 00:04:47作者: X-Wolf

SSE则是部署在 HTTP协议之上的,现有的服务器软件都支持此协议。

SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。但SSE只支持单向交互(服务器给客户发送),Websocket支持双向交互。

SSE默认支持断线重连,WebSocket则需要额外部署。

数据格式方面, SSE 使用的是 UTF8 编码的文本格式。

SSE的HTTP response 里header Content-Type 的值是 text/event-stream。不可变!

SSE的数据格式

每个SSE的消息响应分为4个元素:

  • retry:重试时间,单位毫秒,只能为数字(SSE请求失败,就会发送新的请求)
  • id:消息ID(自定义)
  • event:时间类型(自定义)
  • data:消息的内容(自定义)

下图是4个消息,注意,多个消息之间中间会有个空行(\n\n)。单个消息之间元素间隔是换行\n

retry: 5000
id: 1cd7bb64-4341-4f5d-a690-4298b8a8ae20
event: eventType
data: Sun Nov 20 18:23:11 CST 2022

retry: 5000
id: 2f57295d-3eaa-4e5c-a787-55fff58d9b05
event: eventType
data: Sun Nov 20 18:23:12 CST 2022

retry: 5000
id: 6a9618de-99e7-4c03-91f8-dcdd7601a8d0
event: eventType
data: Sun Nov 20 18:23:13 CST 2022

retry: 5000
id: c5fdcc90-b1f7-4058-9a3a-d63881ffea8b
event: eventType
data: Sun Nov 20 18:23:14 CST 2022

 

使用示例:

golang

 

 

安装包:

go get gopkg.in/antage/eventsource.v1  

 

广播模式SSE

广播模式SSE指不设置事件名,或者说是不设置通道,所有客户端接收同样的数据。

main.go

package main  
  
import (  
    "fmt"  
    "gopkg.in/antage/eventsource.v1"  
    "log"  
    "net/http"  
    "time"  
)  
  
// 广播模式SSE,不设置事件名称(也可理解为通道)  
func main() {  
    es := eventsource.New(nil, nil)  
    defer es.Close()  

    http.Handle("/", http.FileServer(http.Dir("./public")))  
    http.Handle("/events", es)  
    go func() {  
        for {  
        // 只设置发送数据,不添加事件名  
            es.SendEventMessage(fmt.Sprintf("send data: %s", time.Now().Format("2006-01-02 15:04:05")), "", "")  
            log.Printf("客户端连接数: %d", es.ConsumersCount())  
            time.Sleep(2 * time.Second)  
        }  
    }()  

    log.Println("Open URL http://localhost:8080/ in your browser.")  
    err := http.ListenAndServe(":8080", nil)  
    if err != nil {  
        log.Fatal(err)  
    }  
}  

 

public/index.html

<!DOCTYPE html>  
<html>  
<head>  
<title>SSE test</title>  
<script type="text/javascript">  
    const es = new EventSource("http://localhost:8080/events");  
    es.onmessage = function (e) {  
        document.getElementById("test")  
            .insertAdjacentHTML("beforeend", "<li>" + e.data + "</li>");  
    }  
    es.onerror = function (e) {  
        // readyState说明  
        // 0:浏览器与服务端尚未建立连接或连接已被关闭  
        // 1:浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据  
        // 2:浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接  
        console.log("readyState = " + e.currentTarget.readyState);  
    }  
</script>  
</head>  
<body>  
    <h1>SSE test</h1>  
    <div>  
        <ul id="test">  
        </ul>  
    </div>  
</body>  
</html>  

 启动服务:

go run main 

 

 

点对点模式SSE

实现方式和广播模式差不多,只需做简单修改:

服务端代码只需添加事件名称:

// 设置事件名称为:test-event  
es.SendEventMessage(fmt.Sprintf("send data: %s", time.Now().Format("2006-01-02 15:04:05")), "test-event", "")  

 

前端修改接收方式

es.addEventListener("test-event", (e) => {  
    document.getElementById("test")  
        .insertAdjacentHTML("beforeend", "<li>" + e.data + "</li>");  
});  

 

支持跨域的SSE

现在项目开发基本上都是前后端分离,这样就会存在跨域问题,SSE解决跨域的方式只需要在new方法内增加允许跨域请求头:

 

es := eventsource.New(  
    eventsource.DefaultSettings(),  
    func(req *http.Request) [][]byte {  
        return [][]byte{  
            []byte("X-Accel-Buffering: no"),  
            []byte("Access-Control-Allow-Origin: *"),  
        }  
    })  

 

前端创建sse连接时也可添加允许跨域参数:

const es = new EventSource("http://localhost:8080/events", { withCredentials: true });  

 

 

解决火狐浏览器断开不会自动重连问题

点击查看源代码

在Chrome浏览器中sse断开后会自动重连,但firefox浏览器中断开后不会重连,解决办法是,前端js通过判断连接状态主动进行重连请求,通过判断readyState的值进行重新调用初始化操作

readyState说明:

  • 0:浏览器与服务端尚未建立连接或连接已被关闭
  • 1:浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据
  • 2:浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接

前端代码可修改为如下:

<!DOCTYPE html>  
<html>  
<head>  
    <title>SSE test</title>  
    <script type="text/javascript">  
        let es = null;  
        // 解决火狐浏览器断开不会自动重连问题  
        function initES() {  
            if (es == null || es.readyState == 2) {  
                es = new EventSource("http://localhost:8080/events", {withCredentials: true});  
                es.addEventListener("test-event", (e) => {  
                    document.getElementById("test")  
                        .insertAdjacentHTML("beforeend", "<li>" + e.data + "</li>");  
                });  
                es.onerror = function (e) {  
                    // readyState说明  
                    // 0:浏览器与服务端尚未建立连接或连接已被关闭  
                    // 1:浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据  
                    // 2:浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接  
                    console.log("readyState = " + e.currentTarget.readyState);  
                    if (es.readyState == 2) {  
                        setTimeout(initES, 5000)  
                    }  
                }  
            }  
        }  
        initES()  
    </script>  
</head>  
<body>  
<h1>SSE test</h1>  
    <div>  
        <ul id="test">  
        </ul>  
    </div>  
</body>  
</html>