【响应式编程】深度理解之手写代码实现reactive stream规范

1,598 阅读8分钟

原创:临虹路365号(微信公众号ID:codegod365),欢迎分享,转载请保留出处。

简介

为了更好更深入的了解响应式编程reactive streams规范,我们尝试自己手动实现reactive streams的四个接口。通过对接口的实现,我们也能更好的去理解reactive streams的核心概念,从而为后续学习project reactor打下基础。

Reactive Streams

根据reactive streams的原文介绍,reactive streams为了对异步非阻塞且有背压的流处理提供标准规范,其主要覆盖范围是JVM与Javascript运行时环境与网络协议。

Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. This encompasses efforts aimed at runtime environments (JVM and JavaScript) as well as network protocols.

上面这段简短的描述中,有两个重点:1. 异步非阻塞且有背压的流处理 2. JVM与Javascript运行时环境。

  • “异步非阻塞且有背压的流处理”说的就是reactive programming反应式编程,也是反应式宣言里对reactive的定义。
  • JVM与Javascript运行时环境,说明支持的力度范围,注意这里的JVM不单单是指java,也包裹所有使用JVM的语言,例如scala、kotlin等。JVM是主要的后端环境,javascript又是主要的前端开发语言,这两者都支持了,也就涵盖了百分之八九十的Web开发了。

Reactive Streams API

反应式编程API主要有三个,Publisher、Subscriber以及Subscription分别对应三个角色,另外一个则是Processor,相当于Publisher和Subscrbier的组合。

  1. Publisher
public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}
  1. Subscriber
public interface Subscriber<T> {
  void onSubscribe(Subscription s);

  void onNext(T t);

  void onError(Throwable t);

  void onComplete();
}
  1. Subscription
public interface Subscription {
    
    public void request(long n);

    public void cancel();
}
  1. Processor
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

一个简易的demo

下面通过以实现上述API接口的方式来展示reactive streams的lifecycle。 在这个demo里,publisher是一个能发[0-size)有限数字的发布者,subscriber则一次性订阅了全部的消息,即请求的大小为Long.MAX_VALUE,也就是纯PUSH模式(关于reactive streams的pull-push模式,请参考背压(Back-Pressure) —— Pull与Push模式的结合)。

为了打印输出结果直观,这里publisher只发送0和1两个数字,即size为2。

public class SimpleOnceRequestDemo {

  public static void main(String[] args) {
    //0.1 创建消息发布者,以消息类型以Integer为例
    System.out.println("Main: 0.1 create a publisher with integer message type");
    Publisher<Integer> publisher = new Publisher<Integer>() {
      private int start = 0;
      //假设这个publisher产生[0-size)的消息
      private int size = 2;

      private boolean isCancelled = false;

      @Override
      public void subscribe(Subscriber<? super Integer> subscriber){
        //1.1 创建订阅元Subscription
        System.out.println("Publisher: 1.1 create a subscription to the Subscriber");
        Subscription subscription = new Subscription() {
          @Override
          public void request(long n) {
            //2.1 接受到消息请求以及请求数量n
            System.out.println("Publisher: 2.1 receive the request with size from Subscriber");
            if (isCancelled) {
              //如果已经收到了取消请求,若再收到request请求,则通知错误
              subscriber.onError(new IllegalStateException("the subscription is already cancelled"));
            }
            //开始发送请求数量的消息
            long after = start + n;
            for (int i = start; i < after; i++) {
              if (i == size) {
                //2.3 到了数据源的尽头,发送complete消息
                System.out.println("Publisher: 2.3 send complete message to the Subscriber");
                subscriber.onComplete();
                return;
              }
              //2.2 发送消息给订阅者
              System.out.println("Publisher: 2.2 send the message for request size to Subscriber");
              subscriber.onNext(i);

            }
            start = (int)after;
          }

          @Override
          public void cancel() {
            //3.1 收到取消的请求,进行取消消息的发送
            isCancelled = true;
          }
        };
        //1.2 传递订阅元(Subscription)给Subscriber
        System.out.println("Publisher: 1.2 send the subscription to the Subscriber");
        subscriber.onSubscribe(subscription);
      }
    };

    //0.2 创建订阅者
    System.out.println("Main: 0.2 create a subscriber");
    Subscriber<Integer> subscriber = new Subscriber<Integer>(){
      private Subscription subscription;
      @Override
      public void onSubscribe(Subscription s) {
        //1.3 收到来自publisher的订阅元subscription
        System.out.println("Subscriber: 1.3 receive the subscription from the Publisher");
        subscription = s;
        //2.0 开始发送消息的请求以及请求数量
        System.out.println("Subscriber: 2.0 send the request with size Long.MAX_VALUE");
        subscription.request(Long.MAX_VALUE);
      }

      @Override
      public void onNext(Integer msg) {
        System.out.println("\tSubscriber: receive the message: " + msg);
      }

      @Override
      public void onError(Throwable t) {
        System.out.println("get error: " + t);
      }

      @Override
      public void onComplete() {
        System.out.println("\tSubscriber: the publisher is finished to send data");
      }
    };
    //0.3 开始订阅
    System.out.println("Main: 0.3 start to subscribe");
    publisher.subscribe(subscriber);
  }

}

运行结果如下:

Main: 0.1 create a publisher with integer message type
Main: 0.2 create a subscriber
Main: 0.3 start to subscribe
Publisher: 1.1 create a subscription to the Subscriber
Publisher: 1.2 send the subscription to the Subscriber
Subscriber: 1.3 receive the subscription from the Publisher
Subscriber: 2.0 send the request with size Long.MAX_VALUE
Publisher: 2.1 receive the request with size from Subscriber
Publisher: 2.2 send the message for request size to Subscriber
	Subscriber: receive the message: 0
Publisher: 2.2 send the message for request size to Subscriber
	Subscriber: receive the message: 1
Publisher: 2.3 send complete message to the Subscriber
	Subscriber: The publisher is finished to send data

从上面的输出结果我们可以看出,一个reactive streams的生命周期主要有三步: 0. 装配周期 1. 订阅周期 2. 运行周期

ReactiveStreams.Runtime.drawio.png

为了方便理解,我们假设有三个人,一个是装配工人Main,一个是消息发布者Publisher,一个是消息接收者Subscriber。 因为通常是异步的消息流,所以可以将Main、Publisher、Subscriber理解成三个不同的线程,其中Main就是主线程负责装配。

  1. 装配周期

在装配周期中,主要由主线程Main来完成业务逻辑的装配工作,将每一个逻辑处理部件组装在一起,即声明式编程的方式,最后通过调用subscribe方法,来触发整个消息流转。

  1. 订阅周期

订阅周期,即订阅者告知发布者自己要来订阅,使得发布者能知道订阅者的存在,有点类似于客户端Client主动connect到服务端Serverclient.connect(server)。在这里的语法则是server.subscribe(client),看起来像是server主动连接client。其实不是,这样的语法主要是装配工作是由Main完成的,从Main角度是无所谓谁主动连接的谁。因为先有发布者,然后才有订阅者,所以语法publisher.subscribe(subscriber)这样的语法也是很顺畅直观的。

在订阅成功之后,会触发订阅者的onSubscribe方法,其类似于onConnect方法,即表示订阅成功,可以进行消息请求处理了。

  1. 运行周期

在完成订阅之后,就开始了消息请求和处理的过程,即运行周期。与通常的push模式不同的是,reactive streams采用的是pull-push模式。所以每次要处理消息前,需要先发一个请求消息给publiser,publisher会按请求的数量发消息给订阅者,绝对不会多发。通过这种pull-push的方式从而实现了背压(back-pressure)的功能,提供了对消息源发送消息的管控能力。

虽然说,Main、Publisher、Subscriber对应的是三个角色、三个不同的执行周期,也对应于三个不同的线程,但是在真正实现的时候,往往没有必要独立创建这三类线程,因为创建的线程越多越影响CPU的性能。所以,在没有明确指定线程的时候,Main、Publisher、Subscriber都是默认当前线程,只有当明确指定时,才会选用独立的线程,这里指定Subscriber线程(即订阅周期)的方法是subscribeOn,指定Publisher线程(即运行周期)的方法是publishOn

另外默认当前线程的顺序是,Pubshlier > Subscriber > Main,即如果没有指定publisher线程,则publisher的线程用的是subscriber的线程。如果Subscriber的线程也没有指定,则subscriber用的是Main线程,此时,如果publisher也没有指定,则publisher用的也是Main线程。

进一步理解 Subscription

Publisher和Subscriber的概念都很好理解,就如同观察者模式中的Observable和Observer,Publisher是消息发送者,Subscriber是消息接收者,Publisher可以直接发消息给Subscriber,那么Subscription是个什么东西呢?

从语义上讲,Subscription是联系Publisher和Subscriber两者之间的桥梁,是由Publisher发布给Subscriber的通信凭证,也是Subscriber用于控制Publisher流量的控制器。(Subscription翻译过来的中文是订阅,订阅在中文里更像是一个动词,为了方便理解,我把Subscription叫做订阅元,类似于token,通信用的令牌)

那么为什么需要在Publisher和Subscriber之间加一个Subscription呢?主要是考虑到Publisher与Subscriber是一个一对多的关系。如果没有Subscription,就很难记录每一个Subscriber订阅到了哪个消息位置了,Subscription就是起这个作用,是Publisher用来表示与记录每一个Subscriber对应状态的工具。

为了更形象的理解,我们用网络通信来举例,这里Publisher相当于Server,Subscriber则相当于Client。那么,Subscription就相当于是channel或者connection,充当了Server与Client之间的连接对象。

  • Server可以通过该channel或connection来发送数据给client,即Publisher通过Subscription拿到Subscriber,并调用Subscriber#onNext方法。
  • Client可以通过该channel或者connection来发送控制信息给Server,即Subscriber通过Subscription关联到Publisher,并调用Subscription#request或者cancel方法来进行流量控制。

虽然Reactive Streams关注的是程序本身或者说是单机内部,它是一种编程方式,不是种架构工具,但其概念更接近于网络通信或者消息队列,所以我们在使用响应式编程时,可以无限的把Publisher和Subscriber当做是远程通信的Server与Client去理解,这样会更有助于我们形成响应式编程的思维,因为其本质就是异步的非阻塞的,与nio网络通信的方式特别接近。甚至可以说,响应式编程是一种更贴近于物理运行方式的一种编程思维。

总结

本文主要对Reactive Streams涉及到的三个主要API的概念进行了介绍,通过一个手动实现接口的demo来更深入的辅助Reactive Streams思想的理解。尤其重点对Subscription作用进行了细致的介绍。


原创不易,需要一点正反馈,点赞+收藏+关注,三连走一波~ ❤

如果这篇文章对您有所帮助,或者有所启发的话,请关注公众号【临虹路365号】(微信公众号ID:codegod365),您的支持是我们坚持写作最大的动力。