WebSocketメッセージングサーバ"Madoi"をASL2で公開しました

チャットや共有ホワイトボードなどの多人数参加型のウェブアプリを作る際に必要となる、メッセージ送受信や入退室管理、オブジェクト状態管理などの機能を実装したWebSocketサーバ "Madoi" を、オープンソースライセンス(ASL2)で公開しました。この記事では、起動方法から簡単な利用方法までを駆け足で解説します。

## まとめ
* サーバ側開発無しで、チャットや共有ホワイトボードなどの多人数参加型アプリが開発できる、オープンソースのWebSocketサーバを公開した
    * https://github.com/kcg-edu-future-lab/madoi
* サーバの実装言語はJava。Dockerで一発起動できる
* クライアントライブラリはJavaScript/TypeScript用
* シンプルな記述でブラウザ間のメッセージ送受信、関数実行の共有、オブジェクトの共有が可能
* Issue登録・プルリク待ってます！


## なんで"Madoi"？

親しい人たちが集まり楽しく過ごす場所である"円居"のようなアプリを作るフレームワークという意味と、ネットワークプログラミング難しすぎやろ何やこれという開発者の"惑い"から名付けています。


## リポジトリ

https://github.com/kcg-edu-future-lab/madoi
ソースコードだけでなく、一通りの使い方説明やサンプルコードもこのリポジトリにあります。


## サーバ起動方法

dockerをインストールした上で、リポジトリをcloneして、`docker compose up`で起動できます。(dockerのバージョンが20.10.7未満の場合、環境変数 `DOCKER_BUILDKIT` に `1`を設定してください)

```bash
git clone https://github.com/kcg-edu-future-lab/madoi
cd madoi
docker compose up
```

初回起動時に、Madoiのコア機能である`madoi-core`(Java)、オンメモリサーバである`madoi-volatileserver`(Java)、およびクライアントライブラリ`madoi-client-ts-js`(TypeScript)がビルドされ、volatileserverがポート8080で起動します。クライアントライブラリは、 http://localhost:8080/madoi/js/madoi.js からダウンロードできます。


## 最もシンプルな使い方

以下に、ページが表示されたらMadoiサーバに接続してメッセージを送り、メッセージを受け取ったらページに表示する例を示します。

```HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<!-- ① -->
<script src="http://localhost:8080/madoi/js/madoi.js"></script>
<script>
window.addEventListener("load", ()=>{
  // ②サーバに接続する。接続先は↑のscriptタグのsrc属性から決定される。
  // コンストラクタの引数はルームIDとAuthToken(サーバのdocker-compose.yml内で指定)。
  const m = new madoi.Madoi("intro_1_flej4as", "AUTH_TOKEN");
  // ③メッセージをブロードキャスト。
  m.send("greeting", "hello world");
  // ④メッセージを受け取ったらbodyタグに追加。
  m.addReceiver("greeting", ({detail: {content}})=>{
    document.body.innerHTML += `<div>${content}</div>\n`;
  });
});
</script>
</body>
</html>
```

Madoiを利用するには、まずscriptタグ(①)でJavaScript用ライブラリを読み込みます。
次にインスタンスを作成(②)し、サーバに接続します。この際、AUTH_TOKENには、サーバ側で指定したトークン文字列を指定してください。実際の値は、docker-compose.yml内で定義している環境変数MADOI_AUTH_TOKENSを参照してください。

インスタンス作成後、sendメソッドにメッセージタイプ(最初の引数)と内容(2番目の引数)を渡すと、メッセージが送信できます(③)。サーバへの接続が確立されていなければ、確立された後にメッセージが送信されます。この際、内容はJSON.stringifyメソッドを使用してJSON文字列に変換されます。

メッセージを受信するには、レシーバを登録します(④)。この際、sendメソッドの最初の引数に渡したタイプと、メッセージを受け取るハンドラ関数を指定します。ハンドラはメッセージ受信時のイベント情報(CustomEventクラス)を受け取る関数で、detailプロパティのcontentに、sendメソッドの2番目の引数に指定した内容が格納されています。内容は、JSON.parseメソッドで復元されたオブジェクトです。

このサンプルを複数のブラウザで開くと、開いた数だけ、各ブラウザに"hello world"と表示され、リロードするたびにその数が増えていき、ブラウザ間でメッセージ送受信が行われていることがわかります。
後から参加したブラウザにも同じ数の"hello world"が表示されますが、これはMadoiサーバがメッセージ履歴の管理機能を持っており、ルーム内で送信されたメッセージの履歴を新しく参加したブラウザに送信するためです(デフォルトでは1000件まで保持)。

以下に、2つのブラウザでこのサンプルを開いた時のシーケンスを示します。

```mermaid
sequenceDiagram
  participant b1 as Browser1
  participant m as Madoi
  participant b2 as Browser2
  b1->>b1: ページを開く
  b1->>m: 接続
  b1->>m: send("greeting", "hello world")
  m->>m: 履歴追加
  m->>b1: send("greeting", "hello world")
  b1->>b1: "hello world"を画面に追加
  b2->>b2: ページを開く
  b2->>m: 接続
  b2->>m: send("greeting", "hello world")
  m->>b2: 履歴送信
  b2->>b2: "hello world"を画面に追加
  m->>m: 履歴追加
  m->>b1: send("greeting", "hello world")
  b1->>b1: "hello world"を画面に追加
  m->>b2: send("greeting", "hello world")
  b2->>b2: "hello world"を画面に追加
```


## 関数実行の共有とオブジェクトの共有

Madoiはメッセージ送受信処理だけなく、多人数参加型アプリケーション内の状態同期処理を抽象化したプログラミングモデルをサポートしています。それが関数実行の共有とオブジェクトの共有です。

関数実行の共有は、文字通り、関数の実行を複数のブラウザで共有するものです。この機能を使うと、あるブラウザである関数が実行されると、他のブラウザでも同じ関数が実行されます。コード例を以下に示します。(全体のコードは[こちら](https://github.com/kcg-edu-future-lab/madoi/blob/master/examples/js/intro_2.html))

```JavaScript
window.addEventListener("load", ()=>{
  const m = new madoi.Madoi("intro_2_o4ijfslkdd", "AUTH_TOKEN");

  // ①メッセージの追加処理を実装した関数。
  let chat = function(message){
    document.getElementById("log").innerHTML += `<div>${message}</div>\n`;
  };

  // ②関数をmadoiに登録する。戻り値は、呼び出されたことをサーバにBroadcastする関数。
  // 内部でレシーバも用意されており、サーバからBroadcastが来れば、本来の関数が呼び出される。
  chat = m.registerFunction(chat);

  // フォームのsubmit時に、chat関数を呼び出す。
  document.getElementById("form").addEventListener("submit", e=>{
    e.preventDefault();
    const input = document.getElementById("input");
    chat(input.value);  // ③関数を実行する
    input.value = "";
  });
});
```

まず、実行を共有する関数を定義します(①)。次に、その関数をMadoiの`registerFunction`メソッドに渡して登録します。この際、代替の関数が返されるので、関数を実行したい場所で、これを呼び出します(③)。すると、サーバにメッセージが送信され、サーバからすべてのブラウザにそれが転送され、メッセージを受け取ったブラウザで本来の関数(`registerFunction`で登録した関数)が実行されます。これが関数実行の共有機能の振る舞いです。引数はJSONに変換されて送信され、受信時にオブジェクトへ戻され、本来の関数の引数として渡されます。

オブジェクトの共有では、所定の規則を満たしたオブジェクトをMadoiに登録することで、より洗練された状態管理が行えます。まずはサンプルコードを見てみましょう。(全体のコードは[こちら](https://github.com/kcg-edu-future-lab/madoi/blob/master/examples/js/intro_3.html))

```JavaScript
window.addEventListener("load", ()=>{
  // Madoiクライアントを作成しサーバに接続する。
  // 引数は任意のルームIDとAuthToken。
  const m = new madoi.Madoi("chat_by_object_slkjf2sas", "AUTO_TOKEN");

  // ①Chatクラスのインスタンスを作成する。引数はそれぞれ、
  // 投稿フォームタグのid、チャットを入力inputタグのid、ログを表示するdivタグのidを想定
  const c = new Chat("form", "input", "log");

  // ②Maodiに登録する。chatメソッドを共有するよう指定する。
  // c.chatメソッドは、呼び出しの通知をサーバにBroadcastする代替メソッドで置き換えられる。
  // レシーバも内部で用意され、通知がサーバから届くと本来のchatメソッドが実行される。
  m.register(c, [
    {method: c.chat, share: {maxLog: 1000}},
    {method: c.getState, getState: {maxInterval: 3000}},
    {method: c.setState, setState: {}}
  ]);
});

//チャットのログ管理と画面表示処理を実装するクラス。
class Chat{
  constructor(formId, inputId, logId){
    this.id = 0;
    this.chatLog = [];
    this.logDiv = document.getElementById(logId);
    // フォームがsubmitされると、テキストボックスに入っている内容を
    // 取り出しchatメソッドを呼び出す。
    document.getElementById(formId).addEventListener("submit", e=>{
      e.preventDefault();
      const input = document.getElementById(inputId);
      this.chat(input.value);  // ③メソッドを実行する
      input.value = "";
    });
  }
  // ④共有メソッド。登録時に代替メソッドで置き換えられ、メッセージ受信時に実行される
  chat(message){
    this.addChatLog(`chatlog_${this.id++}`, message);
  }
  // ⑤状態取得のため、Madoiから定期的に呼び出される
  getState(){
    return this.chatLog;
  }
  // ⑥状態設定のため、参加時に一度だけMadoiから呼び出される
  setState(state){
    for(const l of state){
      this.addChatLog(l.id, l.message);
    }
  }
  addChatLog(id, message){
    // チャットログに追加(getStateで返す用)
    this.chatLog.push({id: id, message: message});
    // メッセージを表示するdivを作成して追加
    this.logDiv.innerHTML += `<div id="${id}">${message}</div>\n`;
    // メッセージが100件を超えていたら古いものを削除
    if(this.chatLog.length > 100){
      document.getElementById(this.chatLog[0].id).remove();
      this.chatLog.shift();
    }
  }
}
```

Madoiのオブジェクト共有機能は、何らかの状態を管理するオブジェクトに対して、状態の取得、設定、変更を行うメソッドを適切に扱うことで、ルームに参加しているブラウザ間でその状態を同期させることを目的としています。

今回の例では、チャットのログと表示を管理するChatクラスのインスタンスを共有しています。
まずインスタンスを作成し(①)、それをMadoiの`register`メソッドで登録しています(②)この際、共有すべきメソッドが`chat`(状態変化を起こすメソッドが共有すべきメソッドです)であること、状態取得を行うメソッドが`getState`であること、状態設定を行うメソッドが`setState`であることを、引数で伝えています(TypeScriptを使うと、この情報はデコレータ経由で指定できます。詳しくはMadoiリポジトリの[readme](https://github.com/kcg-edu-future-lab/madoi)を参照)。

これらの指定により、`chat`メソッド(④)の実行(③)はブラウザを跨いで共有されるようになります。また、定期的に`getState`が呼び出されて(⑤)状態が取得され、それがサーバに送信されて保存されます(この際、過去の`chat`メソッド実行履歴も削除されます)。新しく参加してきたブラウザにはこの状態が送信され、`setState`メソッドに渡されます(⑥)。これにより、オブジェクトの状態同期が実現されます。

仕組みとしては少し複雑ですが、一定の規則に沿うだけで、コード量も脳の負担も少なく、オブジェクト同期機能を利用できるはずです。


## おわりに

本記事では、オープンソースメッセージングサーバ Madoiの簡単な使い方を紹介しました。Madoiの[リポジトリ](https://github.com/kcg-edu-future-lab/madoi) に、より詳しい説明やサンプルコードがあるので、詳細はそちらを参照してください。


## おまけ: そもそも何故公開したのか

話は2000年、初期の未踏ソフトウェア創造事業の頃に遡ります。当時多人数参加型3D仮想空間シミュレータのネットワーク部分を担当しており、複雑な通信プログラムと格闘する日々を過ごしていました。その時ふと、「この機能はある程度汎用化できるのでは？」という感覚を得、開発計画をまとめて2002年の未踏ソフトウェアに応募し、見事採択され、通信サーバとクライアントライブラリの開発を行いました。当時飼っていた秋田犬にちなんで、WhiteDogと呼んでいました。今回公開したMadoiも、その設計の多くはこのWhiteDogのものを踏襲しています。

WhiteDogはTCP/IP上でXMLデータをやりとりし、AspectJを使ってクライアントソフトウェアと結合するという、当時の業界トレンド、研究トレンドを色濃く反映した、しかしながら今となってはなかなか使いにくいアーキテクチャをしていました。ただ、自分でも作れたのだから、こういう機能はいずれ誰かが開発しオープンソース化されるだろう、特にデータ通信部分の作り直しは面倒なのでそういうOSSが公開されたら乗り換えよう、くらいに考えていました。

その後多人数参加型のアプリケーションを開発する機会が何度かありましたが、その度に調べてもサクッと使えるフレームワークは公開されておらず、毎度WhiteDogのコードや設計を再利用しつつ改良しつつ、実装していました。そうこうしているうちに、20年以上経ってしまい、未だいい感じのOSSが存在しないことに驚きつつも、もうこれは自分で作って公開するしかないなと腹を括り、公開に踏み切りました。

今回公開するにあたって、内部でやりとりするメッセージの種類やプロトコル、それを処理するサーバ側のコードも大幅に整理しました。そのためまだテストが行き届いていない部分も残っていますが、引き続き開発を続けてブラッシュアップしていきます。こんなOSSが欲しかった、あるいは記事を見て多人数参加型のアプリを作ってみたいと思った方は、ぜひ使ってフィードバックしていただけるとありがたいです。