HTML5 SSE

HTML5 服务器发送事件(Server-Sent Events) 使用。

我们都知道服务端与客户端之间通信现在多数用 WebSocket,在此之前,还有传统 Ajax 短轮询、Comet 技术,以及本文要提及的 SSE(Server-sent Events)

What

服务器推送事件(Server-sent Events),简称SSE,是 HTML 5 规范中的一个组成部分,是一种服务器向客户端发送事件和数据的单向通讯协议。客户端不会关闭连接,会一直等着服务器发过来的新的数据流,当客户端收到消息后会再次发送请求,周而复始。

When

基于上述定义,可以知道网页客户端自动获取来自服务器的更新数据,所以 SSE 适合诸如主从控制场景、服务端下发消息无互动、自动更新、物联网信息下发等。

Support

浏览器支持情况:主流浏览器均支持服务器发送事件,除了 IE👎。

How

带着以上认知,我们可以简单搭建一个 demo,用来演示服务端发送消息给网页客户端。其中服务端实现协议部分,而网页端使用的主要 API 为 EventSource:通过 new EventSource(url) 生成实例对象建立连接,然后就是处理事件的监听:

  • open:onopen/addEventListener("open", function(event) {// handle open event}, false)
  • message: onMessage/addEventListener("message",function(event) {// handle open event}, false))
  • error: onError/addEventListener("error",function(event) {// handle open event}, false))
  • 自定义事件:addEventListener("cfz",function(event) {// handle open event}, false))

其中,event 对象中属性有data(服务端数据)origin(服务器端URL的域名部分)lastEventId(服务端发送的数据的编号)

Enviroment

  • go 1.13.7
  • Win 10 x64
  • goland 2019

选择 golang 是因为我们可以更方便地模拟跨线程处理消息的情况——服务端(S)接收一方(M)发来的消息,然后下发到客户端(C)。这其中 M 端使用 POST 请求发送数据,S 端利用协程将处理好的数据下发给 C。C 收到信息后,会再次请求服务端数据。

S 端

我们采用 gin 作为服务处理以及转发请求,另外就是加载 HTML 模拟 M 和 C 的动作:

  • 创建服务器

    1
    2
    3
    4
    5
    6
    7
    app := gin.Default()
    sseRouter := app.Group(`/sse`, func(context *gin.Context) {})
    // 逻辑处理
    //...
    if err := app.Run(fmt.Sprintf(":%d", 9529)); err != nil {
    log.Fatal(err)
    }
  • 处理请求:gin 中已经封装好相关的逻辑,调用即可,PS:其实不用 gin,用 golang 本身的 http 库也完全可以实现,发送的时候,类型记得要设置成 text/event-stream

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
     import "github.com/gin-gonic/gin"
    // 消息管道
    var msgChan = make(chan string, 10)

    // M 发送消息
    sseRouter.POST("/move.cfz", func(context *gin.Context) {
    msg := context.PostForm("msg")
    log.Printf("receive from M: %s", msg)
    go func(m string) {
    // 发送
    msgChan <- m
    }(msg)
    })

    // C 端接收消息
    sseRouter.GET("/go.cfz", func(context *gin.Context) {
    w := context.Writer
    h := w.Header()
    //h.Set("Cache-Control", "no-cache")
    //h.Set("Connection", "keep-alive")
    h.Set("Content-Type", "text/event-stream")
    //h.Set("X-Accel-Buffering", "no")
    if msg, ok := <-msgChan; ok {
    event := sse.Event{
    Id: time.Now().String(),
    Event: "message",
    Data: msg,
    }
    if e := sse.Encode(w, event); e != nil {
    log.Println(e.Error())
    return
    }
    log.Printf("send to C: %s", msg)
    }
    })

浏览器端处理

C 端实现 SSE 方法即可,简单易用:

client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!window.EventSource) {
console.log("web is not support");
} else {
var source = new EventSource('/sse/go.cfz');
source.addEventListener('message', function (e) {
console.log("get message" + e.data);
document.getElementById('message').innerText = e.data;
});

source.addEventListener('open', function (e) {
console.log("connect is open");
}, false);

source.addEventListener('error', function (e) {
if (e.readyState === EventSource.CLOSE) {
console.log("connect is close");
} else {
console.log(e.readyState);
}
}, false);
}
clien.html
1
<div id="message"></div>

M 端发送消息:

master.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function send() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
// loaded
if (xhr.status == 200) {
} else {
alert("Problem retrieving XML data");
}
}
};

var msg = document.getElementById('message').value;
xhr.open("POST", '/sse/move.cfz', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(`msg=${msg}`);
}
master.html
1
2
3
4
<div id="message">
<textarea id="message">哈哈</textarea>
<button onclick="send()">发送</button>
</div>

The End

通过以上 demo,我们可以知道在数据单向流动时,选择 SSE 是比较好的,除了协议足够简单外,在各个语言的实现也更容易,基于现有的 HTTP(s) 服务即可,并且支持断线重连和自定义数据格式;而 WebSocket 则需要升级协议,适用于双向通讯的情况。总而言之,两者都是 HTML 5 规范里的标准技术,对于解决遗留的轮询等技术问题能达到很好的效果。

Reference

-------------本文结束感谢您的阅读-------------