Server-Side Events: 简单高效的服务端推送

sorcererxw /

介绍

通常我们在做后端推送方案选型的时候,如果不使用 WebSocket,我会选用什么方案呢?

先说最基础的 轮询 Polling,client 每隔一段时间请求 server 查询是否有更新数据,但是这样没办法保证数据的实时,而且也需要耗费大量的流量。

那我们进一步,如果没有数据更新,server 就不要响应 client,而是 hold connection,直到数据更新了,再将最新的数据返回给 client,随后 client 再建立新的连接。这就是长轮询 Long Polling 机制。相比简单的轮询,更加实时,网络开销更小,但是也不是非常优雅,需要不断重连。

那么是不是可以让服务端响应数据的时候不要断开 HTTP 连接?那我们只需要将所有响应作为 stream 下发,client 不断处理下发的事件即可。这便形成了 Server-Side Events(下面简称 SSE) 的基本原理。

相比 WebSocket,SSE 完全基于 HTTP,对于服务端来说实现更为简单轻量。

服务端实现

以下我使用 Golang+echo 实现了一个简单的 SSE 服务端:

e:=echo.New()
e.GET("/sub", func(c echo.Context) error {
	c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
	c.Response().WriteHeader(http.StatusOK)
	
	for {
			select {
			case <-c.Request().Context().Done():
				return nil
			default:
			}
			fmt.Fprint(c.Response(), "data: hi\n\n")
			c.Response().Flush()
			time.Sleep(1 * time.Second)
	}
	return nil
})

前端实现

EventSource 是 W3C 标准的正式成员,浏览器也为其提供了完整的实现,只需要直接创建 EventSource 就够直接订阅一个后端数据。

const evtSrc = new EventSource("http://example.com/eventsource");
evtSrc.onmessage = function (event) {
	console.log(event.data)
}

很多情况下后端服务或者网关会给一个 HTTP 请求设定最长连接时长,一旦超时,会自动断开 client 的连接。这就需要 client 实现自动重连机制,以确保数据的实时。而 EventSource 已经原生实现自动重连机制,一旦连接断开,浏览器不不断重试直到连接恢复。

适用场景

经过上面的介绍,可以看到 SSE 无论对于前端还是后端都及其友好,非常容易实现,可以非常好地满足事件下发的需求。对于前端展示的消息订阅,非常适合使用 SSE 实现。

Consuming streaming data
Tips for consuming streaming data.
https://developer.twitter.com/en/docs/tutorials/consuming-streaming-data

这是 Twitter 开发者文档,可以看到 Twitter 的事件推送也是基于 HTTP Stream 实现。