基于HTTP流式传输的长时响应体验提升

广告位招租
扫码页面底部二维码联系

在我们应用开发中偶尔遇到某个请求需要后端【原创内容,转载请注明出处】转载请注明出处:www.tangshuang.net进行大量计算的情况,这种情况下,按照传统未经授权,禁止复制转载。未经授权,禁止复制转载。的前后端协同方式,前端需要等待后端慢慢计【版权所有】唐霜 www.tangshuang.net【原创不易,请尊重版权】算,会放一个loading效果,而长时间【版权所有,侵权必究】【本文受版权保护】的loading对用户的体验并不友好,而本文作者:唐霜,转载请注明出处。【原创不易,请尊重版权】如果后端采用异步方式,在接收到前端请求后【原创内容,转载请注明出处】原创内容,盗版必究。立即返回,过一段时间完成计算后再让前端请未经授权,禁止复制转载。【版权所有】唐霜 www.tangshuang.net求一次,又会让界面上的数据在这段等待时间本文作者:唐霜,转载请注明出处。【原创不易,请尊重版权】中处于老的不正确的数据情况,因此,我们需【版权所有】唐霜 www.tangshuang.net未经授权,禁止复制转载。要找到一种既可以避免异步发送数据让用户误【原创不易,请尊重版权】未经授权,禁止复制转载。认为结果错误,又可以避免长时响应让用户等原创内容,盗版必究。【关注微信公众号:wwwtangshuangnet】待焦虑的方法,利用流式传输,可以将结果分著作权归作者所有,禁止商业用途转载。未经授权,禁止复制转载。片返回,从而让界面实时发生变化,又可以减【版权所有,侵权必究】【未经授权禁止转载】少前后端多次交互带来的编码困难。

转载请注明出处:www.tangshuang.net【本文首发于唐霜的博客】未经授权,禁止复制转载。

HTTP流式传输【访问 www.tangshuang.net 获取更多精彩内容】

本文版权归作者所有,未经授权不得转载。【版权所有】唐霜 www.tangshuang.net著作权归作者所有,禁止商业用途转载。【转载请注明来源】

这里的流式传输是指借鉴流媒体技术,在数据【原创内容,转载请注明出处】著作权归作者所有,禁止商业用途转载。传输中实现持续可用的不间断的传输效果。流原创内容,盗版必究。本文版权归作者所有,未经授权不得转载。式传输可以依赖http, rtmp, r【原创不易,请尊重版权】著作权归作者所有,禁止商业用途转载。tcp, udp…等等网络协原创内容,盗版必究。著作权归作者所有,禁止商业用途转载。议,在本文的场景下,我们主要探讨的是HT著作权归作者所有,禁止商业用途转载。著作权归作者所有,禁止商业用途转载。TP流式传输。

【版权所有】唐霜 www.tangshuang.net【原创不易,请尊重版权】【作者:唐霜】【原创内容,转载请注明出处】本文版权归作者所有,未经授权不得转载。

我们都知道,HTTP是基于TCP的无状态【版权所有】唐霜 www.tangshuang.net【作者:唐霜】的一次性使用的连接协议,在我们日常的开发【本文首发于唐霜的博客】本文版权归作者所有,未经授权不得转载。过程中,从客户端发起数据请求到服务端把数转载请注明出处:www.tangshuang.net【原创不易,请尊重版权】据一次性吐给客户端,就完成了这一次连接,【作者:唐霜】【访问 www.tangshuang.net 获取更多精彩内容】随后它就关闭了。我们姑且不讨论TCP反复本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】的开启和关闭带来的性能损耗。我们要探讨的本文版权归作者所有,未经授权不得转载。本文版权归作者所有,未经授权不得转载。是,在HTTP1.1中默认开启的Keep【转载请注明来源】【关注微信公众号:wwwtangshuangnet】-Alive模式,当客户端和服务端都支持【原创内容,转载请注明出处】本文作者:唐霜,转载请注明出处。该模式时,一个TCP连接会维持打开,直到【版权所有】唐霜 www.tangshuang.net著作权归作者所有,禁止商业用途转载。客户端不在回答服务端的ACK。而开启Ke【原创不易,请尊重版权】著作权归作者所有,禁止商业用途转载。ep-Alive之后,一次HTTP连接就原创内容,盗版必究。本文作者:唐霜,转载请注明出处。可以维持较长时间的连接状态,配合Tran【版权所有】唐霜 www.tangshuang.net【本文首发于唐霜的博客】sfer-Encoding:chunke【版权所有,侵权必究】【原创不易,请尊重版权】d报文, 客户端和服务端基于底层的Soc本文作者:唐霜,转载请注明出处。本文作者:唐霜,转载请注明出处。ket,实现持续的服务端将数据发送给客户【转载请注明来源】著作权归作者所有,禁止商业用途转载。端。

本文版权归作者所有,未经授权不得转载。【原创内容,转载请注明出处】未经授权,禁止复制转载。【版权所有】唐霜 www.tangshuang.net
Connection: keep-alive
Transfer-Encoding: chunked

另一种方式是客户端通过Range报文,主著作权归作者所有,禁止商业用途转载。【版权所有,侵权必究】动要求服务端返回的数据范围:

著作权归作者所有,禁止商业用途转载。转载请注明出处:www.tangshuang.net【访问 www.tangshuang.net 获取更多精彩内容】【本文首发于唐霜的博客】
Connection: keep-alive
Range: bytes=0-100

此时,服务端会报:【原创内容,转载请注明出处】

本文版权归作者所有,未经授权不得转载。【关注微信公众号:wwwtangshuangnet】【作者:唐霜】未经授权,禁止复制转载。
Accept-Ranges: bytes
Connection: keep-alive
Content-Range: bytes 0-100/5243
Content-Length: 101

此时的Content-Length只返回著作权归作者所有,禁止商业用途转载。【作者:唐霜】当前片段的长度。

【转载请注明来源】本文作者:唐霜,转载请注明出处。【原创内容,转载请注明出处】【转载请注明来源】【本文首发于唐霜的博客】

Nodejs实现流式传输著作权归作者所有,禁止商业用途转载。

转载请注明出处:www.tangshuang.net著作权归作者所有,禁止商业用途转载。【转载请注明来源】原创内容,盗版必究。本文作者:唐霜,转载请注明出处。

由于Nodejs内部实现了Stream,未经授权,禁止复制转载。【未经授权禁止转载】且很多实现的基础都是Stream例如ht未经授权,禁止复制转载。【原创不易,请尊重版权】tp, file等。我们用nodejs可本文版权归作者所有,未经授权不得转载。原创内容,盗版必究。以轻松实现流式传输:

【未经授权禁止转载】著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。【原创内容,转载请注明出处】【关注微信公众号:wwwtangshuangnet】
const http = require("http");

http
  .createServer(async function (req, res) {
    res.writeHead(200, {
      "Content-Type": "text/plain;charset=utf-8",
      "Transfer-Encoding": "chunked",
      "Access-Control-Allow-Origin": "*",
    });
    for (let index = 0; index < chunks.length; index++) {
      setTimeout(() => {
        const content = chunks[index];
        res.write(JSON.stringify(content));
      }, index * 1000);
    }
    setTimeout(() => {
      res.end();
    }, chunks.length * 1000);
  })
  .listen(3000, () => {
    console.log("app starting at port 3000");
  });

这里的核心点就在于res.write,在【转载请注明来源】转载请注明出处:www.tangshuang.nethttp模块中,res本身就是一个基于流【未经授权禁止转载】【原创内容,转载请注明出处】实现的响应对象,res.write则是向本文作者:唐霜,转载请注明出处。【版权所有】唐霜 www.tangshuang.net流中写入内容(相当于append)。

转载请注明出处:www.tangshuang.net转载请注明出处:www.tangshuang.net【转载请注明来源】转载请注明出处:www.tangshuang.net【访问 www.tangshuang.net 获取更多精彩内容】

浏览器端实现流式接收未经授权,禁止复制转载。

【原创内容,转载请注明出处】【版权所有】唐霜 www.tangshuang.net【关注微信公众号:wwwtangshuangnet】【访问 www.tangshuang.net 获取更多精彩内容】【版权所有】唐霜 www.tangshuang.net

在大部分浏览器内部也实现了流,我们可以通原创内容,盗版必究。【本文受版权保护】Streams API【版权所有,侵权必究】了解当前浏览器已经提供的各种借口。而在h【版权所有】唐霜 www.tangshuang.net【未经授权禁止转载】ttp请求场景中,全局的fetch函数为【访问 www.tangshuang.net 获取更多精彩内容】【原创不易,请尊重版权】我们提供了非常便捷的接入方法。

原创内容,盗版必究。【转载请注明来源】【原创不易,请尊重版权】【作者:唐霜】
const res = await fetch('xxx');
for await (let chunk of res.body) {
  console.log(chunk);
}

fetch返回的响应对象中.body就是本文作者:唐霜,转载请注明出处。原创内容,盗版必究。一个流,在for await语法的加持下【转载请注明来源】转载请注明出处:www.tangshuang.net,我们都不需要做过多的处理,就可以用ch【本文首发于唐霜的博客】【未经授权禁止转载】unk来更新界面上显示的数据。不过可惜的【原创不易,请尊重版权】著作权归作者所有,禁止商业用途转载。是,目前for await只对firef【访问 www.tangshuang.net 获取更多精彩内容】【版权所有】唐霜 www.tangshuang.netox加持,因此我们还是必须按照一个Rea未经授权,禁止复制转载。【原创内容,转载请注明出处】dableStream的使用方式来从re本文版权归作者所有,未经授权不得转载。【转载请注明来源】s.body中读取数据:

【关注微信公众号:wwwtangshuangnet】【访问 www.tangshuang.net 获取更多精彩内容】【关注微信公众号:wwwtangshuangnet】未经授权,禁止复制转载。【访问 www.tangshuang.net 获取更多精彩内容】
const utf8Decoder = new TextDecoder("utf-8");

const res = await fetch('http://localhost:3000');

const reader = res.body.getReader();
const processor = async () => {
    const { done, value } = await reader.read();
    clearInterval(timer);
    if (done) {
        return;
    }
    const chunk = utf8Decoder.decode(value, { stream: true });
    const item = JSON.parse(chunk);
    console.log(item);
    await processor();
}

await processor();

上面标红的reader.read()返回【版权所有】唐霜 www.tangshuang.net【本文首发于唐霜的博客】结果和generator的逻辑一致,只是【访问 www.tangshuang.net 获取更多精彩内容】著作权归作者所有,禁止商业用途转载。不知道为什么chrome没有实现next【访问 www.tangshuang.net 获取更多精彩内容】本文作者:唐霜,转载请注明出处。接口。

本文版权归作者所有,未经授权不得转载。本文版权归作者所有,未经授权不得转载。本文作者:唐霜,转载请注明出处。【未经授权禁止转载】未经授权,禁止复制转载。

效果对比转载请注明出处:www.tangshuang.net

本文版权归作者所有,未经授权不得转载。著作权归作者所有,禁止商业用途转载。本文作者:唐霜,转载请注明出处。转载请注明出处:www.tangshuang.net

接下来,我们用没有经过处理的实现,和经过未经授权,禁止复制转载。【转载请注明来源】处理的实现来做一个感性的对比。

原创内容,盗版必究。【作者:唐霜】未经授权,禁止复制转载。【访问 www.tangshuang.net 获取更多精彩内容】

首先我们来看下传统方式的效果:【访问 www.tangshuang.net 获取更多精彩内容】

【原创内容,转载请注明出处】未经授权,禁止复制转载。原创内容,盗版必究。

【访问 www.tangshuang.net 获取更多精彩内容】【本文首发于唐霜的博客】本文版权归作者所有,未经授权不得转载。【原创内容,转载请注明出处】

可以看到,我们用一个计时器来作为load【未经授权禁止转载】未经授权,禁止复制转载。ing效果,当时间进入10s之后,所有数【版权所有】唐霜 www.tangshuang.net【原创内容,转载请注明出处】据回来了,于是我们一次性将全部数据渲染到【本文首发于唐霜的博客】著作权归作者所有,禁止商业用途转载。界面上。

【版权所有,侵权必究】【作者:唐霜】【访问 www.tangshuang.net 获取更多精彩内容】

服务端代码如下:原创内容,盗版必究。

【作者:唐霜】【转载请注明来源】【原创内容,转载请注明出处】著作权归作者所有,禁止商业用途转载。
const http = require("http");

const ids = new Array(200).fill(0).map((_, i) => i);

const getData = (id) => new Promise((resolve) => {
    const cost = id % 2 * 100;
    setTimeout(() => resolve({ id, cost }), cost);
});

http
  .createServer(async (req, res) => {
    res.writeHead(200, {
      "Access-Control-Allow-Origin": "*",
    });

    const startTime = Date.now();
    const results = [];
    const run = async (i = 0) => {
        const id = ids[i];
        if (i >= ids.length) {
            return;
        }
        const data = await getData(id);
        results.push(data);
        await run(i + 1);
    };
    await run();
    const endTime = Date.now();

    res.end(JSON.stringify(results));
    console.log('Cost:', endTime - startTime);
  })
  .listen(3000);

客户端代码如下:【原创不易,请尊重版权】

【本文受版权保护】原创内容,盗版必究。未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。
<!DOCTYPE html>

<div id="root"></div>

<script>
    const root = document.querySelector('#root');

    let count = 0;
    const timer = setInterval(() => {
        count ++;
        root.innerHTML = count;
    }, 1000);

    const startTime = Date.now();
    fetch('http://localhost:3000').then(res => res.json()).then((data) => {
        console.log(data);
        const endTime = Date.now();
        const cost = endTime - startTime;
        console.log(cost);
        clearInterval(timer);
        data.forEach((item) => {
            const el = document.createElement('div');
            el.innerHTML = `id: ${item.id}, cost: ${item.cost}`;
            root.appendChild(el);
        });
    });
</script>

当然,这里面还有一些优化空间,比如在服务【未经授权禁止转载】【关注微信公众号:wwwtangshuangnet】端用Promise.all来一次性执行全原创内容,盗版必究。【本文受版权保护】部任务。但是,无论如何优化,底层思维都是【访问 www.tangshuang.net 获取更多精彩内容】原创内容,盗版必究。一次性拿到全部数据之后再渲染,因此,lo未经授权,禁止复制转载。【版权所有,侵权必究】ading过程中,是没有数据展示的。

本文作者:唐霜,转载请注明出处。【本文受版权保护】著作权归作者所有,禁止商业用途转载。【版权所有,侵权必究】原创内容,盗版必究。

接下来看下基于流的效果:本文作者:唐霜,转载请注明出处。

【访问 www.tangshuang.net 获取更多精彩内容】【本文受版权保护】本文版权归作者所有,未经授权不得转载。

转载请注明出处:www.tangshuang.net著作权归作者所有,禁止商业用途转载。【本文首发于唐霜的博客】

可以看到,页面一打开,数据就一条一条的逐【版权所有】唐霜 www.tangshuang.net本文版权归作者所有,未经授权不得转载。步被渲染,虽然全部的数据回来也需要10s未经授权,禁止复制转载。【本文受版权保护】左右,但是,在这过程中,我们可以看到界面【未经授权禁止转载】原创内容,盗版必究。上一部分数据已经被渲染出来。

【未经授权禁止转载】转载请注明出处:www.tangshuang.net【版权所有】唐霜 www.tangshuang.net本文版权归作者所有,未经授权不得转载。【访问 www.tangshuang.net 获取更多精彩内容】

服务端代码如下:本文作者:唐霜,转载请注明出处。

【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】著作权归作者所有,禁止商业用途转载。
const http = require("http");

const ids = new Array(200).fill(0).map((_, i) => i);

const getData = (id) => new Promise((resolve) => {
    const cost = id % 2 * 100;
    setTimeout(() => resolve({ id, cost }), cost);
});

http
  .createServer(async (req, res) => {
    res.writeHead(200, {
      "Transfer-Encoding": "chunked",
      "Access-Control-Allow-Origin": "*",
      'Content-Type': 'text/plain',
    });

    const startTime = Date.now();
    const run = async (i = 0) => {
        const id = ids[i];
        if (i >= ids.length) {
            return;
        }
        const data = await getData(id);
        res.write(JSON.stringify(data));
        await run(i + 1);
    };
    await run();
    const endTime = Date.now();

    res.end();
    console.log('Cost:', endTime - startTime);
  })
  .listen(3000);

客户端代码如下:原创内容,盗版必究。

【原创内容,转载请注明出处】著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。本文作者:唐霜,转载请注明出处。【原创内容,转载请注明出处】
<!DOCTYPE html>

<div id="root"></div>

<script>
    const utf8Decoder = new TextDecoder("utf-8");

    async function init() {
        const root = document.querySelector('#root');

        let count = 0;
        const timer = setInterval(() => {
            count ++;
            root.innerHTML = count;
        }, 1000);

        const startTime = Date.now();
        const res = await fetch('http://localhost:3000');

        const reader = res.body.getReader();
        const processor = async () => {
            const { done, value } = await reader.read();
            clearInterval(timer);
            if (done) {
                return;
            }
            const chunk = utf8Decoder.decode(value, { stream: true });
            const item = JSON.parse(chunk);
            const el = document.createElement('div');
            el.innerHTML = `id: ${item.id}, cost: ${item.cost}`;
            root.appendChild(el);
            await processor();
        }

        await processor();

        const endTime = Date.now();
        const cost = endTime - startTime;
        console.log(cost);
    }
    init();
</script>

可以发现,总体代码的结构是一致的,只是在著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。传输和获取数据的地方不同,随之渲染的过程【本文首发于唐霜的博客】【版权所有】唐霜 www.tangshuang.net也不同。这也说明,在现有的系统中,实现这本文作者:唐霜,转载请注明出处。原创内容,盗版必究。种传输方式的迁移,是可行的,不会对原有项转载请注明出处:www.tangshuang.net原创内容,盗版必究。目的整体架构带来大的变化。

原创内容,盗版必究。本文版权归作者所有,未经授权不得转载。著作权归作者所有,禁止商业用途转载。

其他场景【原创不易,请尊重版权】

【访问 www.tangshuang.net 获取更多精彩内容】【版权所有】唐霜 www.tangshuang.net本文版权归作者所有,未经授权不得转载。【转载请注明来源】本文作者:唐霜,转载请注明出处。

本文设想的场景是,一个列表中,每一条数据转载请注明出处:www.tangshuang.net本文版权归作者所有,未经授权不得转载。后端都需要花一定的时间,整个列表的总时间【访问 www.tangshuang.net 获取更多精彩内容】原创内容,盗版必究。就比较长。针对这一场景,我们采用流式传输【原创不易,请尊重版权】【未经授权禁止转载】的方法,可以让列表可以逐条渲染或更新,从【本文首发于唐霜的博客】【关注微信公众号:wwwtangshuangnet】而可以让用户在较快的时间里,获得前面的数转载请注明出处:www.tangshuang.net【原创不易,请尊重版权】据。而这种流式传输,现在已经在前端被广泛【本文首发于唐霜的博客】【版权所有】唐霜 www.tangshuang.net使用,甚至被某些框架作为其架构的底层选型【原创内容,转载请注明出处】未经授权,禁止复制转载。。我个人也想到了一些场景,供你参考:

【本文首发于唐霜的博客】【版权所有】唐霜 www.tangshuang.net【本文受版权保护】本文作者:唐霜,转载请注明出处。
  • 长列表【访问 www.tangshuang.net 获取更多精彩内容】
  • 【原创不易,请尊重版权】著作权归作者所有,禁止商业用途转载。【本文受版权保护】【原创内容,转载请注明出处】
  • 数据表格实时更新,例如股票市场行情【本文受版权保护】
  • 【版权所有】唐霜 www.tangshuang.net【版权所有,侵权必究】【转载请注明来源】
  • 较长的文章【作者:唐霜】
  • 【访问 www.tangshuang.net 获取更多精彩内容】本文版权归作者所有,未经授权不得转载。【版权所有】唐霜 www.tangshuang.net本文作者:唐霜,转载请注明出处。
  • 将网页分为多个chunk,每一个chun【作者:唐霜】【原创内容,转载请注明出处】k对应页面中的一块,首屏chunk放在最未经授权,禁止复制转载。原创内容,盗版必究。前面,这样可以更快让用户看到界面
  • 著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。【原创不易,请尊重版权】转载请注明出处:www.tangshuang.net【转载请注明来源】
  • 打字机效果,例如实时翻译字幕、ChatG原创内容,盗版必究。著作权归作者所有,禁止商业用途转载。PT的回复
  • 【访问 www.tangshuang.net 获取更多精彩内容】【版权所有,侵权必究】原创内容,盗版必究。未经授权,禁止复制转载。【本文首发于唐霜的博客】
  • 用户提交后需要大量计算,可以先返回一个c【作者:唐霜】本文作者:唐霜,转载请注明出处。hunk,让前端提示用户已经成功,等计算未经授权,禁止复制转载。本文版权归作者所有,未经授权不得转载。完再返回真正的chunk,更新界面数据
  • 原创内容,盗版必究。转载请注明出处:www.tangshuang.net转载请注明出处:www.tangshuang.net【版权所有】唐霜 www.tangshuang.net
  • 古老的聊天室,在服务端,当收到别人发送的【版权所有】唐霜 www.tangshuang.net未经授权,禁止复制转载。消息时,通过一个chunk发送给自己的浏【访问 www.tangshuang.net 获取更多精彩内容】【未经授权禁止转载】览器,这样我们就不需要自己架设socke本文作者:唐霜,转载请注明出处。本文作者:唐霜,转载请注明出处。t
  • 【版权所有,侵权必究】未经授权,禁止复制转载。本文版权归作者所有,未经授权不得转载。【本文首发于唐霜的博客】【未经授权禁止转载】
  • 由粗糙逐渐细腻的渲染,例如先发送较少的模著作权归作者所有,禁止商业用途转载。原创内容,盗版必究。型数据,形成一个轮廓,然后在逐渐发送更多【访问 www.tangshuang.net 获取更多精彩内容】【访问 www.tangshuang.net 获取更多精彩内容】数据,将模型的颜色、细节等进行填充
  • 【原创不易,请尊重版权】【本文受版权保护】本文版权归作者所有,未经授权不得转载。【访问 www.tangshuang.net 获取更多精彩内容】
  • 分段式操作的场景,例如文件下载,用户点击【本文首发于唐霜的博客】【原创内容,转载请注明出处】下载按钮后,服务端要进行压缩打包等,需要转载请注明出处:www.tangshuang.net未经授权,禁止复制转载。一段时间,在打包过程中,还会发现其中某个本文作者:唐霜,转载请注明出处。【原创内容,转载请注明出处】文件存在问题,要将问题反馈给前端,完成打原创内容,盗版必究。转载请注明出处:www.tangshuang.net包之后才返回给前端打包好的文件
  • 【关注微信公众号:wwwtangshuangnet】【本文受版权保护】原创内容,盗版必究。本文作者:唐霜,转载请注明出处。原创内容,盗版必究。
  • 随机渲染,例如不同的用户处在地图的不同点【访问 www.tangshuang.net 获取更多精彩内容】【关注微信公众号:wwwtangshuangnet】,我们优先返回该点的地图信息,然后再逐渐【本文首发于唐霜的博客】本文版权归作者所有,未经授权不得转载。往外扩散
  • 未经授权,禁止复制转载。未经授权,禁止复制转载。【本文受版权保护】本文作者:唐霜,转载请注明出处。

总之,流式传输的特性决定了我们可以在较长【转载请注明来源】【原创内容,转载请注明出处】的时间里,持续的接收数据,实现界面的同步本文作者:唐霜,转载请注明出处。【本文受版权保护】

原创内容,盗版必究。【本文首发于唐霜的博客】【原创内容,转载请注明出处】

2023-06-17 10447

为价值买单,打赏一杯咖啡

本文价值104.47RMB