RSocket With Spring Boot + Vue.js アプリケーション

こんにちは、プラットフォーム事業部 DXユニットの高橋(@yukey1031)です。

先日開催されたOPTiM TECH NIGHT|Webアプリケーション/APIサーバフレームワークを語るLT大会で 「WebFluxでリアクティブなWebAPIのデモ」といった話をさせて頂きましたが、続けてこの記事ではSpring Boot2.2でサポートされたRSocketについてサンプルを交えつつ触れてみたいと思います。

optim.connpass.com

マイクロサービスな現場でのAPI開発

以前の記事でも触れているようにオプティムが提供するAI・IoTプラットフォーム Cloud IoT OSは、マイクロサービスアーキテクチャで構成されており、周辺エコシステムの開発においてもマイクロサービスが前提となります。

マイクロサービスの連携には一般的にWebAPIが利用され、RESTful API、GraphQL、gRPC等のテクノロジーが用途に応じて採用されます。

最近では通信効率が高く、双方向通信、StreamingをサポートするgRPCの採用が増えつつありますが、ブラウザクライアントによるフロントエンド/バックエンドサーバー間通信では、以下のようなブラウザ側の制限やキャッシュ機構等の理由から、今でもほとんどのWeb APIとしてRESTful APIが採用されています。

と記載されていますが、やはりブラウザとの通信がサポートされていて、通信上のオーバーヘッドが少なく、Duplex(双方向通信)が可能な通信プロトコルを採用したいと考える訳です。 ということで、RSocketに着目します。

RScoketとは

rsocket.io

  • マイクロサービスでの使用を目的としたバイナリのアプリケーションプロトコル。
  • TCP、WebSocket、 Aeron*1と HTTP/2 streamsをトランスポートに使う。
  • OSI参照モデルの5/6レイヤ、またはTCP/IP階層モデルのアプリケーションレイヤ。
  • Reactive Streamsをサポート。
  • 4つのInteraction Model を提供。
    • request/response (stream of 1)
    • request/stream (finite stream of many)
    • fire-and-forget (no response)
    • channel (bi-directional streams)
  • セッションの再開をサポート。
  • 多言語対応(Java、JavaScript、C++、Kotlin..)
  • Reactive Foundationで開発。

reactive.foundation

Spring のサポート

  • Spring Framework 5.2、Spring Boot 2.2から正式サポート。
  • spring-messagingモジュール
    • io.rsocket.RSocketを介したデータおよびメタデータのEncode/Decodeの提供。
    • @MessageMappingを使ったハンドラーメソッドの提供。
  • spring-webモジュール
    • RSocketアプリケーションが必要とする可能性が高いCBOR, Protobuf等のEncode/DecodeやRouteマッチング用のプラグインを提供。
    • WebFluxサーバーでRSocket over WebSocketを公開するオプションを含むRSocketサーバーの立ち上げをサポート。
    • RSocketRequester.BuilderおよびRSocketStrategiesのクライアントサポートと自動構成を提供。
  • Spring Security 5.2は、RSocketサポートを提供。
  • Spring Integration 5.2は、RSocketクライアントおよびサーバーのゲートウェイを提供。
  • Spring Cloud Gatewayは、RSocket接続をサポート。

JavaScriptのサポート

  • 今回利用するモジュール

www.npmjs.com

www.npmjs.com

RSocket With Spring Boot + Vue.js アプリケーション

概要

  • Webクライアント(Vue.js)からRSocketでリクエストを行うとリ1秒おき且つリアクティブにPayloadが送信される。
  • Interaction Model は、request/streamを採用。
  • 抽象的な構成は以下の通り。

https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20200121/20200121183636.png

Requirement

  • 実行環境
    • JDK 1.8
    • node v13.6.0
    • yarn 1.19.1
    • Vue CLI 4.1.2

RSocket サーバーの実装

  • Spring Initializerから プロジェクトを作成します。dependenciesには、WebFlux、RSocket、Lombok を指定します。
$ curl https://start.spring.io/starter.tgz \
    -d type=maven-project \
    -d baseDir=rsocket-server \
    -d dependencies=webflux,rsocket,lombok | tar -xzvf -
  • ベースディレクトリに移動します。
$ cd rsocket-server
  • @Controller アノテーションと@MessageMapping アノテーションでRSocketのエンドポイントを設定します。
  • routing名は"message"としています。
  • 1秒DelayさせてFluxで返却する実装を追加します。
$ vi src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;

+ import lombok.AllArgsConstructor;
+ import lombok.Data;
+ import lombok.NoArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+ import org.springframework.messaging.handler.annotation.MessageMapping;
+ import org.springframework.stereotype.Controller;
+ import reactor.core.publisher.Flux;
+ 
+ import java.time.Duration;
+ import java.util.stream.Stream;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

+ @Controller
+ class MessageController {
+   @MessageMapping("message")
+   Flux<Message> messageStream(Message request) {
+       return Flux.fromStream(Stream.generate(() -> new Message(request.getMessage(), request.getDatetime())))
+               .delayElements(Duration.ofSeconds(1));
+   }
+ }
+ 
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Data
+ class Message {
+   private String message;
+   private String datetime;
+ }
}
  • application.yml に RSocket のプロトコル(WebSocket)とパス(/rsocket)の設定を追加します。
  • 使用ポート番号(デフォルト8080)等の指定も可能です。8.2. RSocket server Auto-configuration
$ mv src/main/resources/application.properties src/main/resources/application.yml
$ vi src/main/resources/application.yml
spring:
  rsocket:
    server:
      transport: websocket
      mapping-path: /rsocket
  • RSocket サーバーを起動します。
$ ./mvnw spring-boot:run &
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.3.RELEASE)

2020-01-21 20:07:03.794  INFO 45642 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on 
2020-01-21 20:07:03.797  INFO 45642 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-01-21 20:07:04.524  INFO 45642 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2020-01-21 20:07:04.529  INFO 45642 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.99 seconds (JVM running for 1.251)

RSocket クライアント(Web)の実装

  • 続いてRSocket クライアントの実装をvue-cliから進めていきます。
## $ yarn global add @vue/cli
$ vue create rsocket-front

Vue CLI v4.1.2
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? Yes
? Save preset as:
? Pick the package manager to use when installing dependencies: Yarn
...
🎉  Successfully created project rsocket-front.
👉  Get started with the following commands:

 $ cd rsocket-front
  • RSocket Clientの実装に必要なモジュールを追加します。
$ yarn add rsocket-core
$ yarn add rsocket-websocket-client
$ yarn add @types/rsocket-core
$ yarn add @types/rsocket-websocket-client
  • 画面からRSocketでのリクエストを行い、レスポンスを表示するための実装を行います。尚、通常はAtomic Design ベースのコンポーネント設計で実装が行われていますが、今回はサンプルとなりますので最小限の実装で実現する対応を行っています。
  • metadataに String.fromCharCode('message'.length) + 'message' としてroutingを指定しています。本指定と合わせて、metadataMimeTypeとserializerの設定がポイントです。
$ vi src/components/Rsocket.vue
+ <template>
+   <div class="rsocket">
+     <button v-on:click="requestStream()">RSocket</button>
+       <li v-for="(item, index) in items" :key="index">
+         {{ item }}
+       </li>
+   </div>
+ </template>
+ 
+ <script lang="ts">
+ import { Component, Prop, Vue } from 'vue-property-decorator';
+ import { RSocketClient, JsonSerializer, IdentitySerializer } from 'rsocket-core';
+ import RSocketWebSocketClient from 'rsocket-websocket-client';
+ import { ReactiveSocket } from 'rsocket-types';
+ 
+ @Component
+ export default class Rsocket extends Vue {
+ 
+   private socket?: ReactiveSocket<any, any>;
+   private items: string[];
+ 
+   constructor() {
+     super();
+     this.items = [];
+     this.connect();
+   }
+ 
+   private async connect() {
+     const transport = new RSocketWebSocketClient({
+       url: 'ws://localhost:8080/rsocket',
+     });
+     const client = new RSocketClient({
+        serializers: {
+          data: JsonSerializer,
+          metadata: IdentitySerializer,
+        },
+       setup: {
+         lifetime: 180000,
+         keepAlive: 60000,
+         dataMimeType: 'application/json',
+         metadataMimeType: 'message/x.rsocket.routing.v0',
+       },
+       transport,
+     });
+     this.socket = await client.connect();
+   }
+ 
+   private requestStream() {
+       if (this.socket) {
+         const message = { message: 'RSocket!', datetime: new Date() };
+         this.socket
+           .requestStream({
+             data: message,
+             metadata: String.fromCharCode('message'.length) + 'message',
+           })
+           .subscribe({
+             onError: (error) => {
+               console.error(error);
+             },
+             onNext: (value) => {
+               this.items.push(value.data);
+             },
+             onSubscribe: (sub) => {
+               sub.request(3);
+             },
+           });
+       }
+   }
+ }
+ </script>+ 

$ vi src/App.vue
-    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
+    <Rsocket />

-    import HelloWorld from './components/HelloWorld.vue';
+    import Rsocket from './components/Rsocket.vue';

-        HelloWorld,
+        Rsocket,
  • RSocket クライアント(Web)を起動します。
$ yarn run serve --port 8082
yarn run v1.19.1
・・・
  Note that the development build is not optimized.
  To create a production build, run yarn build.

動作確認

  • ブラウザの「RSocket」を数回クリックしてレスポンスを確認します。
  • リアクティブなレスポンスやバイナリ通信を確認することが出来ます。

https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20200121/20200121195402.gif

おわりに

ブラウザとの通信がサポートされていて、通信上のオーバーヘッドが少なく、Duplex(双方向通信)が可能な通信プロトコルであるRSocketを試してみました。 現時点でRSocketの要件を必要とするユースケースが見えているので、採用を検討していきたいと考えています。

オプティムの技術スタックにはJavaも含まれています。マイクロサービスでリアクティブな実装に興味のあるフロントエンドエンジニア/バックエンドエンジニアの方々は、是非こちらをご覧ください。

www.optim.co.jp