简介
在像 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-datadiv 的内容中,每条消息占一行。
服务端代码
服务端的核心逻辑(使用 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 实现可以提供稳健应用程序所需的必要灵活性和控制。