daikiojm’s diary

ブログを書くときがやってきたどん!

TypeScriptでgRPCのstreaming RPCを使ったチャットのサンプル

はじめに

Node.jsのgRPCサンプルコードを解説されている方がいたのですが、この記事で触れられているのはUnaryなリクエストのみで、Streem通信に関しては触れられていなかったのでChartを例に試してみた。

tomokazu-kozuma.com

Node.jsは現時点(2018/12/23)でのLTSv10.14.2を使っています。 gRPCの通信方式の違いに関しては、公式ドキュメントの RPC life cycleの項目が参考になります。

Protpcol Buffersの定義

gRPC公式サンプルの中に含まれているstreamを使った例 route_guide.proto を参考にchartアプリケーションを想定した簡単なProtpcol Buffersの定義を作成しました。 https://github.com/grpc/grpc/tree/master/examples/protos

// proto version.
syntax = "proto3";

// package name.
package example;

service Chat {
  rpc join(stream Message) returns (stream Message) {}
  rpc send(Message) returns (Message) {}
}

message Message {
  string user = 1;
  string text = 2;
}

上記.protoファイルのmessageと対応するTypeScriptのInterfaceも作成しました。

// .protoファイルの message定義に合せてtypescriptのinterfaceを用意
export interface Message {
  user: string;
  text: string;
}

Server

今回は、Node.jsの grpc パッケージのインターフェースを一通り眺めながら進めて行きたかったので、protoファイルからコードを自動生成を行うことはしていません。protoLoaderに.protoファイルを渡してサービス定義を読み込みます。
今回のように実行時に動的にコード生成を行う方法を dynamic codegen といい、事前にコード生成(静的にコード生成)をおこなう方法を static codegen と呼ぶようです。

import * as grpc from 'grpc';
import { ServerWriteableStream } from 'grpc';
import * as protoLoader from '@grpc/proto-loader';

import { Message } from './types';

const server = new grpc.Server();
const serverAddress = '0.0.0.0:5001';

// .protoファイルを動的に読み込み
const proto = grpc.loadPackageDefinition(
  protoLoader.loadSync('./chat.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  })
);

const users: ServerWriteableStream<Message>[] = [];

function join(call: ServerWriteableStream<Message>): void {
  users.push(call);
  notifyChat({ user: 'Server', text: `new user joined ...` });
}

function send(call: ServerWriteableStream<Message>): void {
  console.log(`send message from ${call.request.user}: ${call.request.text}`);
  notifyChat(call.request);
}

function notifyChat(message: Message): void {
  // すべてのユーザーにメッセージを送信
  users.forEach((user) => {
    user.write(message);
  });
}


// .protoファイルで定義したserviceと上記実装をマッピング
server.addService(proto.example.Chat.service, {
  join: join,
  send: send
});

// Serverのconfig設定 & 起動
server.bind(serverAddress, grpc.ServerCredentials.createInsecure());
server.start();

Client

Client側もServer側と同様に、protoLoaderに.protoファイルを渡してサービス定義を読み込みます。

import * as grpc from 'grpc';
import * as protoLoader from '@grpc/proto-loader';
import * as readline from 'readline';

import { Message } from './types';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const proto = grpc.loadPackageDefinition(
  protoLoader.loadSync('./chat.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  })
);

const remoteServer = '0.0.0.0:5001';

let username = '';

const client = new proto.example.Chat(
  remoteServer,
  grpc.credentials.createInsecure()
)

function startChat(): void {
  let channel = client.join({ user: username });

  channel.on('data', onDataCallback);

  // 標準入力に新しい行が入力されるごとにServerに送信
  rl.addListener('line', (text: string) => {
    client.send({ user: username, text: text }, () => {});
  });
}


function onDataCallback(message: Message): void {
  if (message.user === username) {
    return;
  }

  console.log(`${message.user}: ${message.text}`);
}

rl.question('User name: ', (answer: string) => {
  username = answer;

  startChat();
});

動作確認

今回は、REPLで実行する。

まずはServer側を実行しておく

$ ts-node server.ts

その後、Client側(1)を実行
ユーザー名を入力しEnterを押すと入力待受状態になる

$ ts-node client.ts
User name: daikiojm
Server: new user joined ...

Client(2)も同様に実行

$ ts-node client.ts
User name: test-user
Server: new user joined ...

その後は双方向の通信が確立される

$ ts-node client.ts
User name: test-user
Server: new user joined ...
daikiojm: はらへった
me too

終わりに

gRPCのstreaming RPCはClient - Serverで真意を発揮しそうなので、機会があればgRPC-webでも試してみたい。
従来httpとwsを併用していたような箇所をgRPC1本にまとめつつ型安全を保てるのはかなり実用的な印象がある。
Node.jsでのServer実装の例は少なそうなので、そのうちもう少し大きめのサンプルを作ってみたいかもしれない。

参考

https://tomokazu-kozuma.com/run-grpc-sample-code-with-nodejs/
https://grpc.io/docs/guides/concepts.html
https://github.com/improbable-eng/ts-protoc-gen