初探 http-streaming

基础概念

HTTP 流简单的理解为:服务端维持一个 HTTP 连接,通过这个 HTTP 连接源源不断、持续的输出内容至客户端。相较于我们常见的 HTTP 请求(一次返回),流处理的特点在于持续返回。

我们举个简单的应用场景:股票网站的股价更新。你可以使用轮询的方式,前端设置定时器周期性的请求股价接口来刷新股价。除此之外,可以使用 HTTP Streaming 的方式,只需要维持一个 HTTP 链接,就可以由后端自行将最新的股价信息 Push 到客户端来完成实时刷新。

同时需要注意,为了实现这种持续返回的效果,服务端需要在客户端返回的 Header 中设置 Transfer Encoding: chunked [2]。

To achieve an indefinite response, the server must respond to client requests by specifying Transfer Encoding: chunked in the header. This sets up a persistent connection from server to client and allows the server to send response data in chunks of newline-delimited strings. These chunks of data can then be received and processed on-the-fly by the client.

服务端初体验

我们使用 php fpm + nginx,来完成服务端的 http 流输出。PHP 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (ob_get_level() == 0) ob_start();

for ($i = 0; $i<10; $i++){
echo "<br> Line to show. At".date('Y-m-d H:i:s');
echo str_pad('',4096)."\n";

ob_flush();
flush();
sleep(2);
}

echo "Done.";

ob_end_flush();

我们看到其中有 str_pad('',4096),为填充输出缓冲区,进而保障每次 flush 都有数据能输出到客户端。因为我们的服务经过 Nginx,而 Nginx 的 proxy_buffer_size [3] 默认是 4k,所以我们需要填充完缓冲区后,才能保证每次的 Server 端输出可以直接打到客户端。(当然了,你也可以关闭 Nginx 的缓冲区设置)

其输出为

1
2
3
4
5
6
7
8
9
10
Line to show. At2021-04-11 01:54:52
Line to show. At2021-04-11 01:54:54
Line to show. At2021-04-11 01:54:56
Line to show. At2021-04-11 01:54:58
Line to show. At2021-04-11 01:55:00
Line to show. At2021-04-11 01:55:02
Line to show. At2021-04-11 01:55:04
Line to show. At2021-04-11 01:55:06
Line to show. At2021-04-11 01:55:08
Line to show. At2021-04-11 01:55:10 Done.

对于客户端,我们也有简单的代码验证,关注浏览器开发者工具即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function watchResource(url, callback) {
const xhr = new XMLHttpRequest();

xhr.open("GET", url, true);

xhr.onreadystatechange = () => {
if (xhr.readyState >= 3 && xhr.status === 200) {
callback(xhr.responseText);
}
};

xhr.send();

return xhr;
}

watchResource("/streaming.php", function (res) {
console.log(res);
});

在这种情况下,每次后端有新的数据推送过来时,都会触发 xhr.onreadystatechange 事件,进而进入回调函数。

关于 xhr.onreadystatechange 的 readState 状态值含义如下[1]:

1
2
3
4
5
0		The request is not initialized.
1 The request has been set up.
2 The request has been sent.
3 The request is in process.
4 The request is completed.

应用场景

HTTP Streaming VS WebSocket

两者的模式和使用场景不同,最明显的区别

  • HTTP Streaming 是 Server Push,数据流向是单向的,由服务器推送给客户端。
  • WebSocket 是双向通信,客户端可以和服务端进行交互通信。

以容器云为例的应用场景

以容器云平台为例,我们需要在平台上对容器进行简单的管理,其管理包括:

  • 场景1:持续获取容器的运行日志,即容器终端持续输出的日志(view container logs)
  • 场景2:远程登录至容器终端(exec container terminal)

场景1情况下,我们需要持续获取终端的日志输出,并且数据的流向是单向的:即服务端有新的日志输出就推送至客户端

在这种情况下,我们可以采用 HTTP Streaming 的方式来完成需求,其前端的代码简化如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 监听 url 的输出,每次触发后进入 callback 回调
function watchResource(url, callback) {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)

xhr.onreadystatechange = () => {
if (xhr.readyState >= 3 && xhr.status === 200) {
callback(xhr.responseText)
}
}
xhr.send()
return xhr
}

// 持续获取日志用于显示
this.logText = ''
watchResource('/logs', res => {
this.logText += res
})

场景2情况下,数据是双向交互的,我们选择使用 Websocket

此场景下,用户输入 command 后,服务端响应后返回至前端,是典型的双向通信场景,使用 websocket 来完成需求。

注意点

在实际操作过程中,还是有些注意点需要提及。

  1. 服务端的数据如果经过 Nginx 转发,需要注意 proxy_buffer 配置。不然会出现服务端服务有持续输出,经过 Nginx 后不一定有输出的情况。
  2. 客户端在处理完流数据后,记得在合适的析构时期断开连接。如在 unmount 组件时进行 xhr.abort()

参考资料