聊聊 EventStream 服务器端推送
之前在写 Cardinal 平台的从 Docker 镜像部署靶机的功能时,打算在后台拉取镜像时,前端能有一个实时滚动的控制台日志给用户以反馈。 最初的想法是使用 WebSocket 实现后端和前端之间的持续通信,正准备写之前,我想到 Drone CI 在执行持续集成任务时,也有一个类似的实时日志的功能,遂打算先研究下 Drone CI 的实现,吸取下经验,结果接触到了 EventStream 这么一个“新鲜”玩意。
因为之前给协会用 Go 写了一套 CAS 统一登录系统,因此我们也在协会服务器上搭了一个 Drone CI 来实现自动部署。我直接登上了协会的 Drone,重新执行了一个已完成的构建任务,结果发现开发者工具 Network - WS 下居然没有 WebSocket 连接的建立!
最后才发现他是通过一个持久的 HTTP GET 请求来实现服务端向客户端单向推送信息。 在开发者工具里该条连接不再显示 Preview 和 Response,而是 EventStream。
MDN 上对 EventStream 的描述已经十分详细了。
EventStream 并不是协议层面的新技术,而是 HTML5 的新内容。其特点是返回的 MIME Content-Type
为text/event-stream
。一般来说,如果浏览器 HTTP 请求一个资源,当资源未全部传完时,浏览器是会一直等待的,此时页面还是空白一片。而当响应头中Content-Type: text/event-stream
时,虽然 HTTP 连接还未被关闭,但浏览器会渲染这个持久化的连接的响应内容。当有新的数据被传输过来时,浏览器会继续显示出来。就很神奇。
因此这只是浏览器在保持、处理持久的 HTTP 连接时的新机制。在 HTTP 协议层面,这只是个普通的一直在持续加载的请求。
对比 WebSocket
相比于 WebSocket 而言,我之前分别使用过 PHP + Swoole 和 Go + Gorilla 做过 WebSocket。它给我最深的印象就是对于客户端的每个请求,我必须要自己维护一个 map 一样的字典,将每个连接存放到里面。并且服务端不能主动检测到客户端的中途意外断开,只能时不时发个心跳包,通过是否发送成功来判断。若发现客户端已断开,还需要将客户端的连接从 map 中移除。且 WebSocket 连接时还需要返回 101 协议升级。
而 EventStream 是一个单纯的 HTTP 请求,如果用户在传输中途断开,这个事件在 Web 框架层面就已经处理妥当,是不需要开发者操心的。但同时开发者又可以捕获到这个事件:
func handler (w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
L:
for {
select {
case <-ctx.Done():
break L
//...
}
}
}
但 WebSocket 的好处是服务端和客户端可以双向通信,而 EventStream 基于 HTTP 请求,仅支持服务端到客户端单向通信。因此它的应用场景一般像是实时获取新的推文、刷新页面数据等。
发送的事件流格式
EventStream 的每条消息使用字段 + :
+ 内容构成。若没有冒号:
,则整段将被当成一个字段名;若冒号前没有消息类型,则这是一条注释,会被前端忽略。
以下就是三个典型的 EventStream 事件:
: this is a test stream
data: some text
data: another message
data: with two lines
最后一块是多行消息 another message\nwith two lines
。
通过event
字段可以发送一个命名事件:
event: message
data: here is message body
对于一个命名事件,在前端就可以声明相应的侦听器来监听:
const event = new EventSource("http://domainhere.com/stream")
evtSource.addEventListener("message", function(event) {
console.log(event.data)
});
而对于无类型消息,则会通过onmessage
处理函数:
evtSource.onmessage = function(event) {
console.log(event.data)
}
注意事项
EventStream 虽然是基于 HTTP,但是在前端使用EventSource
接口时,和 WebSocket 一样,并不能自定义 HTTP 请求头。鉴权方面只能通过传输同源下的 Cookie。并不能自定义一个 Authorization 头带 Token 之类的。我在这一点上不是很满意。
同时,在不使用 HTTP/2 时,会受到浏览器的最大连接数限制。在 Chrome 和 Firefox 浏览器中所有打开选项卡下同域的连接最多只能有 6 个。而使用 HTTP/2 时,HTTP 同一时间内的最大连接数由服务器和客户端之间协商(默认为100)。
以上大致就是关于 EventStream 的介绍,Go 下的具体实现可以参考 Drone CI。
Drone CI 也是使用的 Gin 框架,他们早年时 EventStream 是使用 Gin 原生的 c.Stream
以及 c.SSEvent
方法,但目前最新的版本是自己构造发送的消息体:
h := w.Header()
h.Set("Content-Type", "text/event-stream")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "keep-alive")
h.Set("X-Accel-Buffering", "no")
f, ok := w.(http.Flusher)
if !ok {
return
}
io.WriteString(w, ": ping\n\n")
f.Flush()
我尚且还不明白他们这样做的好处。因此目前在 Cardinal 中的实现还是采用 Gin 原生的方法。
喜欢这篇文章?为什么不打赏一下呢?