什么是流式输出?

一 名词理解

1 流式php

流式(Stream)亦称响应式,是一种基于异步数据流研发框架,是一种概念和编程模型,并不是一种技术架构,目前在各技术栈都有响应式的技术框架,前端的React.js、RxJs,服务端以RxJava、Reactor,Android端的RXJava。由此而来的便是响应式编程。html

2 反应式/响应式编程前端

反应式编程/响应式编程(Reactive Programming)是一种基于事件模型编程范式,众所周知异步编程模式中一般有两种得到上一个任务执行结果的方式,一个就是主动轮训,咱们把它称为Proactive方式。另外一个就是被动接收反馈,咱们称为Reactive。简单来讲,在Reactive方式中,上一个任务的结果的反馈就是一个事件,这个事件的到来将会触发下一个任务的执行。react

这也就是Reactive的内涵。咱们把处理和发出事件的主体称为Reactor,它能够接收事件并处理,也能够在处理完事件后,发出下一个事件给其余Reactor。web

下面是一个Reactive模型的示意图:spring

固然一种新的编码模式,它的RunTime会减小上下文切流从而提高性能,减小内存消耗,与之相反带来的是代码的可维护性下降。衡量优劣须要根据场景带来的收益来衡量。编程

3 流式输出json

流式输出就比较神奇,源自于团队内部在一次性能大赛结束后的总结中产生,是基于流式的理论基础在页面渲染以及渲染的HTML在网络传输中的具体应用而诞生,也有人也简单的称之为流式渲染。即:将页面拆分红独立的几部分模块,每一个模块有单独的数据源和单独的页面模板,在server端流式的操做每一个模块进行业务逻辑处理和页面模板的渲染,而后流式的将渲染出来的HTML输出到网络中,接着分块的HTML数据在网络中传输,接着流式的分块的HTML在浏览器逐个渲染展现。具体流程以下:api

针对HTML能够如上所述进行流式输出,衍生出针对json数据的流式输出,其实也是一模一样,无非少了一层渲染的逻辑,数据流式输出流程跟上图相似,再也不赘述。这里能够把客户端的请求当作响应式的一个事件,因此总结就是客户端主动发出请求,服务端流式返回数据,即流式输出。跨域

4 端到端响应式

基于流式输出,咱们再深刻一点,能够发现其实不仅是用户端和web server之间的数据能够在网络上进行流式输出,微服务的各个server之间的数据其实也能够在网络上进行流式输出,以下图所示:

数据能够在网络之间的流式传输,再进一步来看,数据在整条请求响应链路上的流式传输会是什么样子,见下图所示:

综上所述咱们定义:端到端响应式=流式输出+响应式编程。

二 流式输出理论基础

是什么基础技术理论,支撑咱们可以像上述流程那样对数据进行流式输出和接收,下面有几个核心的技术点:

1 HTTP分块传输协议

分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,容许HTTP由网页服务器发送给客户端应用( 一般是网页浏览器)的数据能够分红多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。

若是须要使用分块传输编码的响应格式,咱们须要在HTTP响应中设置响应头Transfer-Encoding: chunked。它的具体传输格式是这样的(注意HTTP响应中换行符是\r\n):

HTTP/1.1 200 OK\r\n
\r\n
Transfer-Encoding: chunked\r\n
...\r\n
\r\n
<chunked 1 length>\r\n
<chunked 1 content>\r\n
<chunked 2 length>\r\n
<chunked 2 content>\r\n
...\r\n
0\r\n
\r\n
\r\n

具体流程见流式输出名词理解部分,分块传输编码例子:

func handleChunkedHttpResp(conn net.Conn) {
    buffer := make([]byte, 1024)
    n, err := conn.Read(buffer)
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(n, string(buffer))

    conn.Write([]byte("HTTP/1.1 200 OK\r\n"))
    conn.Write([]byte("Transfer-Encoding: chunked\r\n"))
    conn.Write([]byte("\r\n"))

    conn.Write([]byte("6\r\n"))
    conn.Write([]byte("hello,\r\n"))

    conn.Write([]byte("8\r\n"))
    conn.Write([]byte("chunked!\r\n"))

    conn.Write([]byte("0\r\n"))
    conn.Write([]byte("\r\n"))
}

这里须要注意的是HTTP分块传输对同步HTML输出比较适合(对于浏览器来说),由于在不少web页面涉及SEO,SEO的TDK元素必须同步输出,因此这种方式比较适合,针对于JSON数据的流式输出经过SSE来实现,具体以下。

2 HTTP SSE协议

sse(Server Send Events)是HTTP的标准协议,是服务端向客户端发送事件流式的方式。在客户端中为一些事件类型绑定监听函数,从而作业务逻辑处理。这里要注意的是SEE是单向的,只能服务器向客户端发送事件流,具体流程以下:

SSE协议中约束了下面几个字段类型

1)event

事件类型。若是指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的字段值,你可使用addEventListener()方法在当前EventSource对象上监放任意类型的命名事件,若是该条消息没有event字段,则会触发onmessage属性上的事件处理函数。

2)data

消息的数据字段。若是该条消息包含多个data字段,则客户端会用换行符把它们链接成一个字符串来做为字段值。

3)id

事件ID,会成为当前EventSource对象的内部属性"最后一个事件ID"的属性值。

4)retry

一个整数值,指定了从新链接的时间(单位为毫秒),若是该字段值不是整数,则会被忽略。

客户端代码示例

// 客户端初始化事件源
const evtSource = new EventSource("//api.example.com/ssedemo.php", { withCredentials: true } );

// 对 message 事件添加一个处理函数开始监遵从服务器发出的消息
evtSource.onmessage = function(event) {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.innerHTML = "message: " + event.data;
  eventList.appendChild(newElement);
}

服务器代码示例

date_default_timezone_set("America/New_York");
header("Cache-Control: no-cache");
header("Content-Type: text/event-stream");
$counter = rand(1, 10);
while (true) {
  // Every second, send a "ping" event.
  echo "event: ping\n";
  $curDate = date(DATE_ISO8601);
  echo 'data: {"time": "' . $curDate . '"}';
  echo "\n\n";
  // Send a simple message at random intervals.
  $counter--;
  if (!$counter) {
    echo 'data: This is a message at time ' . $curDate . "\n\n";
    $counter = rand(1, 10);
  }
  ob_end_flush();
  flush();
  sleep(1);
}

效果示例

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}
event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

这里须要注意下,在未经过http2使用SSE时,SSE会收到最大链接数限制,此时默认的最大链接数只有6,即同一时间只能创建6个SSE链接,不过这里的限制是对同域名的,跨域的域名能够再创建6个SSE链接。经过HTTP2使用SSE时默认的最大链接数是100。

目前SSE已集成到spring5,Springboot2的webflux其实就是经过SSE的方式进行数据的流式输出。

3 WebSocket

Websocket就比较老生常谈了,这里主要介绍下它与SSE的区别:

  • Websocket是区别于HTTP的另一种协议,是全双工通讯,协议相对来讲比较中,对代码侵入度比较高。
  • SSE是标准的HTTP协议,是半双工通讯,支持断线重连和自定义事件和数据类型,相对轻便灵活。

4 RSocket

在微服务架构中,不一样服务之间经过应用协议进行数据传输。典型的传输方式包括基于 HTTP 协议的 REST 或 SOAP API 和基于 TCP 字节流的 RPC 等。可是对于HTTP只支持请求响应模式,若是客户端须要获取最新的推送消息,就必须使用轮询,这无疑形成了大量的资源浪费。再者若是某个请求的响应时间过长,会阻塞以后的其余请求的处理;虽然服务器发送事件(Server-Sent Events,SSE)能够用来推送消息,不过 SSE 是一个简单的文本协议,仅提供有限的功能;而WebSocket 能够进行双向数据传输,不过它没有提供应用层协议支持,Rsocket很好的解决了已有协议面临的各类问题。

Rsocket是一个面向反应式应用程序的新型应用网络协议,它工做在网络七层模型中 5/6 层的协议,是 TCP/IP 之上的应用层协议,RSocket 可使用不一样的底层传输层,包括 TCP、WebSocket 和 Aeron。TCP 适用于分布式系统的各个组件之间交互,WebSocket 适用于浏览器和服务器之间的交互,Aeron 是基于 UDP 协议的传输方式,这就保证了 RSocket 能够适应于不一样的场景,见上图。而后RSocket 使用二进制格式,保证了传输的高效,节省带宽。并且,经过基于反应式流控保证了消息传输中的双方不会由于请求的压力过大而崩溃。更多详细资料请移步RSocket[1]。雷卷也开源了alibaba-rsocket-broker[2],感兴趣能够去深刻了解请教。

Rsocket提供了四种不一样的交互模式知足全部场景:

RSocket 提供了不一样语言的实现,包括Java、Kotlin、JavaScript、Go、.NET和C++ 等,以下为仅供学习了解的简单Java实现:

import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.TcpServerTransport;
import io.rsocket.util.DefaultPayload;
import reactor.core.publisher.Mono;

public class RequestResponseExample {

  public static void main(String[] args) {
    RSocketFactory.receive()
        .acceptor(((setup, sendingSocket) -> Mono.just(
            new AbstractRSocket() {
              @Override
              public Mono<Payload> requestResponse(Payload payload) {
                return Mono.just(DefaultPayload.create("ECHO >> " + payload.getDataUtf8()));
              }
            }
        )))
        .transport(TcpServerTransport.create("localhost", 7000)) //指定传输层实现
        .start() //启动服务器
        .subscribe();

    RSocket socket = RSocketFactory.connect()
        .transport(TcpClientTransport.create("localhost", 7000)) //指定传输层实现
        .start() //启动客户端
        .block();

    socket.requestResponse(DefaultPayload.create("hello"))
        .map(Payload::getDataUtf8)
        .doOnNext(System.out::println)
        .block();

    socket.dispose();
  }
}

5 响应式编程框架

若是要在全链路实现响应式,那响应式编程框架是支撑这个技术的核心技术,这对于开发者来讲是一种编程模式的变革,经过使用异步数据流进行编程对于原流程化的编程模式来讲变化还很大。

简单示例以下:

@Override
public Single<Integer> remaining() {
    return Flowable.fromIterable(LotteryEnum.EFFECTIVE_LOTTERY_TYPE_LIST)
        .flatMap(lotteryType -> tairMCReactive.get(generateLotteryKey(lotteryType)))
        .filter(Result::isSuccess)
        .filter(result -> !ResultCode.DATANOTEXSITS.equals(result.getRc()))
        .map(result -> (Integer) result.getValue().getValue())
        .reduce((acc, lotteryRemaining) -> acc + lotteryRemaining)
        .toSingle(0);
}

总的来讲经过HTTP分块传输协议和HTTP SSE协议以及RSocket咱们能够实现流式输出,经过流式输出和响应式编程端到端的响应式才得以实现。

三 流式输出应用场景

性能、体验和数据是咱们平常工做中抓的最紧的三件事情。对于性能来讲也一直是咱们追求极致和永无止境的核心点,流式输出也是在解决性能体验这个问题而诞生,那是否是全部的场景都适合流式输出呢?固然不是,咱们来康康哪些场景适合?

以上为Resource Timing API规范提供的请求生命周期包含的主要阶段,经过上述来看下一下几个场景对于请求生命周期的影响。

1 页面流式输出场景

对于动态页面来讲(相对于静态页面)主要由页面样式、页面交互的JS以及页面的动态数据构成,除了上述请求生命周期的各阶段耗时,还有页面渲染耗时阶段。浏览器拿到HTML会先进行DOM树构建、预加载扫描器、CSSOM树构建,Javascript编译执行,在过程当中CSS文件的加载和JS文件的加载阻塞页面渲染过程。若是咱们将页面按照如下方式进行拆分进行流式输出将会在性能上有很大的收益。

单接口动态页面

对于某些场景好比SEO,页面须要同步渲染输出,此时页面一般是单接口动态页面,就能够将页面拆分红body以上部分和body如下的部分,例如:

<!-- 模块1 -->
<html>
  <head>
  <meta  />
    <link  />  
  <style></style>  
  <script src=""></script>
  </head>
  <body>

<!-- 模块2 -->
        <div>xxx</div>
        <div>yyy</div>  
        <div>zzz</div> 
    </body>
</html>

当模块1到达页面模块2未到达时,模块1渲染后在等待模块2到来的同时能够进行CSS和JS的加载,在几个方面进行了性能提高:

  • 到达浏览器的首字节时间(TTFB)
  • 数据包到达浏览器下载HTML的时间
  • CSS和JS的加载及执行时间
  • 拆成模块以后网络传输的时间会有必定的下降

单接口多楼层页面

<!-- 模块1 -->
<html>
  <head>
  <meta  />
    <link  />  
  <style></style>  
  <script src=""></script>
  </head>
  <body>

<!-- 模块2 -->
        <div>xxx1</div>
        <div>yyy1</div>  
        <div>zzz1</div> 

<!-- 模块3 -->
        <div>xxx2</div>
        <div>yyy2</div>  
        <div>zzz2</div>

<!-- 模块4 -->
        <div>xxx3</div>
        <div>yyy3</div>  
        <div>zzz3</div>
    </body>
</html>

不少场景是一个页面展示多个楼层、譬如首页的四大金刚以及各类导购楼层,detail的信息楼层等等,甚至后面楼层依赖前面楼层的数据,相似这种状况能够将页面楼层拆分红多个模块进行输出,在上述几个方面进行了性能提高以外还有额外的性能提高:楼层之间数据相互依赖的数据处理时间。

多接口多楼层页面

通常状况下大部分页面都是由同步SSR渲染和异步CSR渲染进行,这时会涉及到JS异步加载异步楼层,若是同步渲染部分按照单接口多楼层进行拆分会在上述基础上提早加载运行异步楼层的渲染。

总的来讲基于HTTP分块传输协议的流式输出几乎覆盖全部页面场景,供全部页面提高性能体验。

2 数据流式输出场景

单接口大数据

对于APP或者单页面系统更多的是经过异步加载数据的方式进行页面渲染,单个接口会形成单个接口的RT时间较长,以及数据包体太大致使在网络中拆包粘包的损耗较大。若是经过多个异步接口会因网络带宽受限而致使数据请求的延时较高以及网络IO的带来的CPU占有率较高,所以能够经过业务场景进行分析将单接口拆分红多个相互独立或者有必定耦合关系的业务模块,将这些模块的数据进行流式输出,以此带来如下性能体验上的提高。

  • 到达浏览器的首字节时间(TTFB)
  • 数据包到达端侧下载数据的时间
  • 数据在网络传输的时间

多相互依赖接口

可是在大部分场景中咱们遇到的业务场景是相互耦合关联的,比方说榜单模块数据依赖它上面的新品模块的数据进行业务逻辑处理,这种状况在服务器侧处理完新品模块数据后对数据进行输出,再接着处理榜单模块数据进行输出,这里接节省了相互依赖等待的时间。

固然平常的业务场景会相对复杂的多,可是经过流式输出都会页面性能和体验会有很大的提高和助力。

四 小结

  • 流式输出的前世为流式渲染,此生为端到端的响应式,这些虽然带来了性能体验上的提高,但对研发模式变革的接受程度和运维成本的增长须要加以权衡。
  • 简单介绍了几种流式输出的技术方案,适合不一样的业务场景。
  • 提出了流式输出适合的几种场景,以及对页面和数据进行拆分的方法。

原文连接

本文为阿里云原创内容,未经容许不得转载。

相关文章
相关标签/搜索