简介

在像 ChatGPT 这样的服务表现出色的实时交互时代,对于开发者来说,利用能够在应用程序中实现无缝数据流的技术至关重要。本文将深入探讨 HTML5 服务器发送事件 (Server-Sent Events, SSE) 的世界,这是一个类似于对话式 AI 接口背后的强大工具。就像 ChatGPT 通过流式传输数据来提供即时响应一样,SSE 使网络浏览器能够从服务器接收更新,而无需重复的客户端请求。无论你是构建聊天应用程序、实时通知系统,还是任何需要实时数据流的服务,本指南都将为你提供在应用程序中高效实施 SSE 的知识,确保响应迅速且引人入胜的用户体验。

理解服务器发送事件 (SSE)

服务器发送事件 (SSE) 是一种 Web 技术,它促进了服务器通过已建立的 HTTP 连接向客户端发送实时更新的能力。客户端可以通过 EventSource JavaScript API 接收连续的数据流或消息,该 API 包含在 WHATWG 的 HTML5 规范中。SSE 的官方媒体类型是 text/event-stream

下面是一个典型 SSE 响应的说明性示例:

event:message
data:The Current Time Is 2023-12-30 23:00:21

event:message
data:The Current Time Is 2023-12-30 23:00:31

event:message
data:The Current Time Is 2023-12-30 23:00:41

event:message
data:The Current Time Is 2023-12-30 23:00:51

SSE 消息中的字段

通过 SSE 传输的消息可能包含以下字段:

  • event: 指定事件类型的字符串。如果指定了,浏览器会将事件分派给相应的事件名称监听器。使用 addEventListener() 来监听命名事件。如果未指定事件名称,则调用 onmessage 处理程序。

  • data: 此字段包含消息内容。如果 EventSource 接收到以 data: 开头的多个连续行,它会将它们连接起来,在每一行之间插入一个换行符。任何末尾的换行符都会被去除。

  • id: 一个标识符,用于设置 EventSource 对象的最后事件 ID 值。

  • retry: 以毫秒为单位的重新连接时间。如果服务器连接断开,浏览器将在尝试重新连接之前等待此持续时间。非整数值将被忽略。

示例实现

让我们看一个使用 Go Gin SSE 的简单示例来演示 SSE 功能。

客户端代码

在客户端,只需要少量的 HTML 和 JavaScript。

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Server Sent Event Example</title>
</head>

<body>
<div id="event-data"></div>
<script>
    // JavaScript 中的 EventSource 对象监听来自我们 Go 服务器的流式事件并显示消息。
    const stream = new EventSource("/stream");
    stream.onmessage = function (e) {
        // 将消息数据追加到 event-data div 中
        const eventDataElement = document.getElementById('event-data');
        eventDataElement.innerHTML += e.data + "<br>";
    };
</script>

</body>
</html>

解释

  • EventSource 对象自动建立与服务器端点 /stream 的连接,该端点应设置为发送连续事件。
  • stream 对象添加了一个 “message” 事件的事件监听器,以处理传入的消息。
  • 来自事件的数据 (e.data) 被追加到 event-data div 的内容中,每条消息占一行。

服务端代码

服务端的核心逻辑(使用 Go channel)允许管理多个客户端。

// 中间件,用于设置 SSE 必要的头部
func HeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Content-Type", "text/event-stream")
        c.Writer.Header().Set("Cache-Control", "no-cache")
        c.Writer.Header().Set("Connection", "keep-alive")
        c.Writer.Header().Set("Transfer-Encoding", "chunked")
        c.Next()
    }
}

// 向客户端流式传输消息的端点
authorized.GET("/stream", HeadersMiddleware(), func(c *gin.Context) {
    clientChan, ok := c.Get("clientChan")
    if !ok {
        return
    }
    c.Stream(func(w io.Writer) bool {
        if msg, ok := <-clientChan; ok {
            c.SSEvent("message", msg)
            return true
        }
        return false
    })
})

让我们探索一下 Gin 框架中的 SSEvent 方法:

// SSEvent 将服务器发送事件写入 body 流。
func (c *Context) SSEvent(name string, message any) {
    c.Render(-1, sse.Event{
        Event: name,
        Data:  message,
    })
}

// SSE 的 Event 结构体
type Event struct {
    Event string
    Id    string
    Retry uint
    Data  interface{}
}

应对挑战

虽然 SSE 的客户端和服务端代码看起来很简单,但构建生产级 SSE 应用程序会引入复杂性。JavaScript SSE API 有一定的局限性:

  • 仅支持 GET 方法。
  • 无法发送请求体。
  • 不允许自定义头部,这对于传递身份验证令牌可能至关重要。
  • 对重试策略的控制有限,浏览器在停止之前会尝试重新连接几次。

增强灵活性的解决方案

为了克服 JavaScript EventSource API 的限制,可以考虑使用第三方库,例如 Azure 的 fetch-event-source。该库利用 Fetch API 来管理服务器发送事件,提供了对请求方法、头部和消息体更大的控制权。

fetch-event-source 库的使用示例:

// 使用 fetch-event-source 库以获得更大的灵活性
import { fetchEventSource } from '@microsoft/fetch-event-source';

const ctrl = new AbortController();

fetchEventSource('/api/sse', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-access-token'
    },
    body: JSON.stringify({ foo: 'bar' }),
    signal: ctrl.signal,
    onmessage(ev) {
        console.log(ev.data);
    },
    onopen(response) {
        // 处理打开事件
    },
    onerror(err) {
        // 处理错误
    },
    onclose() {
        // 处理关闭事件
    }
});

结论

虽然使用 SSE 和 EventSource API 进行 HTTP 流式传输直观且简单,但有限的自定义选项可能会阻碍全面的生产应用程序的开发。使用第三方 EventSource 实现可以提供稳健应用程序所需的必要灵活性和控制。

参考资料