小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
与websocket这种重量级选手不同,浏览器为我们提供了一个轻量级的从服务端推送数据到浏览器的办法 EventSource Api。它实际上的效果类似于 chunk传输数据,也就是说,它只能服务端单向推送数据。
在html EventSource的使用是这样的:
<ul></ul>
<script>
window.onload = () => {
var evtSource = new EventSource('/event');
var eventList = document.querySelector('ul');
evtSource.onmessage = e => {
var newElement = document.createElement("li");
newElement.textContent = e.data;
eventList.appendChild(newElement);
}
}
</script>
其中 /event 是服务端提供的接口,值得注意的是这个EventSource 是有跨域限制 的。
不过这里不是重点,这个API明显是经过高度封装的,重点在服务端应该如何传输数据给网页。
首先,响应头的MIME 类型应该是 text/event-stream ,我们才可以传输消息数据。
消息内容应该以utf-8编码,消息的内容其实是类似 Json 键值对的一段字符。
它可以包括这几个字段(并非都是必须的),额外的字段将被忽略。
-
event : 事件类型
-
data:数据
-
id:事件id
-
retry:重连时间间隔
举个例子,我们发送的数据可以是这个字符串的字节编码
"id: %d\n event: message\n data: 这是消息 %d\n\n"
每个字段使用换行符分割,字段和字段的值使用冒号分割,用两个换行表示一个消息的结束。
注意:这里的冒号附近的空格是会被忽略的( data:1 和data: 1)是完全一样的
了解了这些我们就可以开始写接口代码了
func PushEvent(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
flusher := w.(http.Flusher)
for i := 0; i < 10; i++ {
w.Write([]byte(fmt.Sprintf("id: %d\n event: message\ndata: 这是消息 %d\n\n", i, i)))
flusher.Flush()
time.Sleep(time.Second)
}
}
由于go默认是不会直接传输数据的,所以我们在代码里使用手动的刷新。然后用sleep假装延时操作。
到这里我们的消息推送已经可以使用了,在网页里,我们可以看到每隔一秒,列表会增加新的一项。
但其实这里还有一个比较大的问题,以上面的代码为例,在网页中,当10个列表加载完之后,他其实又会再加载十个列表,然后重复循环,根本停不下来。
原因在于 EventSource 自带自动重连功能,当服务器断开连接之后,他会自动重新连接,我们如果不想它继续连接,就得想个办法让它关闭。
EventSource它自己会在请求到HTTP 204的响应码之后自动关闭。不过呢我们也可以手动关闭连接,也就是调用它自身的close方法。
为了在一个合适的时候关闭我们可以让服务器推送一个特别的消息表明需要它自己关闭连接。
例如像下面这样:
服务端
w.Write([]byte("event: message\ndata: close\n\n"))
flusher.Flush()
网页中则在每次收到信息是检查一下消息内容是否表明需要关闭连接:
if (e.data == "close") {
console.log("closed");
evtSource.close()
}
通过这样也可以比较简单的控制EventSource连接的完全关闭。
到这里我们目的已经达到了。 与websocket相比,EventSource的确是相当轻量,只有浏览器为我们提供的一个基础的语义,还不能双向传输。对于一些要求双向传输的它几乎没有用武之地,但是对于类似热重载这种只要求单向传输的行为却是有大有作为。