NEMのWebSocket通信を使ってみた
nem-kotlin という NEM の Kotlinライブラリを先日リリースしました。早速次のバージョンの機能追加を検討していたところ NEM で WebSocket が使えるという情報を耳にしたので、試してみました。
結果、使えました!知りませんでした!これは便利じゃないか!というわけで、NEM の WebSocket 通信について調べたことなどを記載しておきます。
ちなみに、この記事は NEM-sdk のコードを参照はしてはいますが、NEM-sdk の WebSocket 通信の使い方を書いた記事では無いです。NEM-sdk の WebSocket 通信の使い方に関しては、こちらに記事がありますのでご参照ください。
要点
NEM の WebSocket の通信は STOMP Over WebSocket です。
WebSocket ってなに?
技術的な細かい部分はおいておいて、あくまでイメージです。
NEM の API 含め、Web API と呼ばれる Web 上のインターフェイスは、HTTP/1.1 の仕様上に成り立っているものが多いです。
HTTP/1.1 の通信は基本的に 1 リクエストに対して 1 レスポンス。
何かしらデータが欲しい場合は、クライアントトリガでデータを要求する必要があります。
サーバ側が管理している状態が変化したかどうかを知るためには、クライアントが都度問い合わせなければ行けません。ブラウザの更新ボタンを連打するイメージです。
非常に無駄な通信が発生します。これは、サーバトリガでの通信を発生させることが出来ないことに起因しているので、そこを解消し、双方向・多重に通信できるようにしたプロトコルが WebSocket です。
WebSocket では、一度コネクションが確立とすると、プロトコル上はサーバ・クライアントのどちらからでも通信を発生させることができるので、無駄な通信を発生させなくて済むようになります。
雑ですが、こんな感じです。
補足ですが、WebSocket の通信を開始する際は、クライアントから “Upgrade : websocket” というヘッダを入れた HTTP のリクエストを行います。サーバはそれに対し、”101 Switching Protocols” というレスポンスを返し、以降 WebSocket での通信となります。これから WebSocket を使うよ−という合図みたいな感じですね。
NEM で WebSocket が使えると何が嬉しいのか
例えば、XEM が着金したことをアプリなどでいち早くユーザに通知したい場合。
クライアントからは着金したことがわからないので、NIS に問い合わせに行きますね。
ただ、トランザクション承認のタイミングは不明なので、HTTP/1.1 しか使えない場合だと、トランザクションが承認されるまでひたすら NIS への問い合わせを行う必要があります。
これが、WebSocket の場合だと上記のようにサーバから通知が送られてくることになるので、コネクションだけ開いて待機しておけば良いということになります。
便利ですね!
STOMP Over WebSocket ってなに?
要点にも書きましたが NEM の WebSocket 通信のプロトコル仕様は、STOMP Over WebSocket のようです。
WebSocket は上記の通り双方向・多重通信をするためのプロトコルですが、そこで送受信するデータの内容については自由です。
自由ですが、ある程度取り決めをしておいた方がサーバもクライアントも実装し易い。
というわけで、WebSocket 上でやりとりするデータを簡単なテキスト形式で仕様化したものが STOMP Over WebSocket です。
STOMP の 詳細な仕様はこちらをご確認ください。
STOMP 自体は WebSocket に限らず、クライアントーサーバ間でのテキストデータのやりとりを仕様化したもので、それを WebSocket 上で使いましょうというのが STOMP Over WebSocket ということになるかと思います。
STOMP では、一度の送信・受信で扱うテキスト一式をフレームという単位で管理しています。フレームは以下のような形式のテキストです。
1 2 3 4 5 6 |
コマンド名 ヘッダ1: ヘッダ値1 ヘッダ2: ヘッダ値2 ボディ(終端文字) |
コマンドは、よく使うものだと
- CONNECT(クライアント → サーバ)
接続開始時に送る - SUBSCRIBE(クライアント → サーバ)
サーバにあるパスの監視をお願いする - SEND(クライアント → サーバ)
クライアントからサーバへ情報を通知 - MESSAGE(サーバ → クライアント)
サーバからクライアントへ情報を通知
などがあります。
NEM でも、上記のような STOMP のフレームのやり取りを行って情報の送受信をしています。
NEM の WebSocket 通信フロー
さて、NEM の通信は STOMP Over WebSocket というところまで説明しました。
では実際にどういったデータのやり取りが行われるか。調べてみました。
ここでは、例として モザイク残高に変化があった場合にユーザに通知するというユースケースを考えます。
監視したい(変化があったことを伝えてほしい)ことをサーバに伝えるのは SUBSCRIBE フレームなので、SUBSCRIBE で監視対象のアドレスを通知します。下記のようなフレームを送信します。
1 2 3 4 5 |
SUBSCRIBE id:0 destination:/account/mosaic/owned/監視対象のアドレス |
これで待機していると、サーバから MESSAGE フレームが通知されます。
1 2 3 4 5 6 7 8 |
MESSAGE subscription:0 destination:/account/mosaic/owned/監視対象のアドレス message-id:xxxxxx content-length:68 {"quantity":109,"mosaicId":{"namespaceId":"ttech","name":"ryuta"}} |
STOMP のボディ部分でデータモデルが通知される感じですね。今回の場合は Mosaic データモデルが通知されるようです。
これでやりたいことはできるのですが、この方法の場合、変化があるまでは情報が通知されません。
つまり、初期状態がわからない状態です。
これを解決する手段として、下記のような SEND フレームをクライアントからサーバに送ります。SEND フレームを送ると、変化がない状態でも一度 MESSAGE フレームを送るという仕様のようですね。
1 2 3 4 5 6 |
SEND destination:/w/api/account/mosaic/owned content-length:54 {'account':アドレス} |
これで、最初にモザイク残高を取得しつつ、残高に変化があったら再度ユーザに通知する、という流れになります。まとめると、下記のような流れとなるかと。(NISとかブロックチェーンとかまとめてサーバと書いてます)
NEM でできる WebSocket 通信の種類
ドキュメントが見当たらなかったので、NEM-sdk のここらへんのソースから情報をひっぱりだしてきました。
一応、全種類 Kotlin で実装して動作確認済み。
取得できる情報 | データモデル | SUBSCRIBEパス | SENDパス | SENDボディ |
---|---|---|---|---|
アカウント情報 | AccountMetaDataPair | /account/アドレス | /w/api/account/get/ | {‘account’:’アドレス’} |
直近トランザクション | TransactionMetaDataPairの配列 | /recenttransactions/アドレス | /w/api/account/transfers/all | {‘account’:’アドレス’} |
未承認トランザクション | TransactionMetaDataPair | /unconfirmed/アドレス | ||
承認済みトランザクション | TransactionMetaDataPair | /transactions/アドレス | ||
保有モザイク定義 | MosaicDefinitionの亜種? | /account/mosaic/owned/definition/アドレス | /w/api/account/mosaic/owned/definition | {‘account’:’アドレス’} |
保有モザイク量 | Mosaic | /account/mosaic/owned/アドレス | /w/api/account/mosaic/owned | {‘account’:’アドレス’} |
保有ネームスペース | Namespace | /account/namespace/owned/アドレス | /w/api/account/namespace/owned | {‘account’:’アドレス’} |
ブロックの高さ | BlockHeight | /blocks/new | ||
最新のブロックの情報 | Block | /blocks |
SUBSCRIBEパスは、SUBSCRIBEフレームのdestination に設定する値です。
SENDパスは、上記のように MESSAGEの送信を誘発する SENDフレームが有る場合に、destination に設定する値。SENDボディも同様。
取得できるデータのデータモデルは、NEM NIS API Document の 9章 にある名称です。
保有しているモザイクは、モザイクの種別ごとに MESSAGE が通知されます。HTTP の方の /account/mosaic/owned は配列で一括で通知されるため、データモデルの違いに注意が必要です。
所有しているモザイクには XEM も含まれていますが、XEM の量が変化した時は保有モザイク量の MESSAGE 通知は来ませんでした。XEM をモザイクとして添付した場合も駄目。フィルタしてるんですかね。
あと、通知されるデータモデルが、ちょっとよくわからないのがちらほら。
- 未承認トランザクションの取得は UnconfirmedTransactionMetaDataPair が通知されるのかと思いきや、JSONの形式を見ると TransactionMetaDataPair。
当然、 height 値が変な値になっている。(0x1FFFFFFFFFFFFF. Int でマッピングしてるとオーバーフローするので注意。) -
保有しているモザイク定義は、 {‘mosaicDefinition’: MosaicDefinition } という感じで、MosaicDefinition が一段ラップされた型が通知される。
MosaicDefinitionMetaDataPair とも違う。
謎です。
使ってみる
ここから実践です。おおよそ必要な実装が見えてきたかと思いますが、必要なものとしては
- WebSocket で通信ができる通信クライアント
- STOMP を解釈・作成できるパーサ
ですね。STOMP Over WebSocket に対応した通信クライアントがあればそれだけで事足ります。
相変わらず Kotlin で実装しました。
WebSocketが使える通信クライアントとしては、Android の通信クライアントのデファクトスタンダート OkHttp が使えます。
STOMP は良さげなライブラリが見つからなかったので、自前で簡単なものを実装しました。
今回は、サンプルではなく nem-kotlin の開発ブランチの方にソースを入れています。下記辺りが該当箇所です。
上記コードを簡単化したコードで説明します。
コネクションの確立
OkHttp で WebSocket を明示してサーバとのコネクションを開始します。
ちなみに、最初につなぐ URL はこちらのコードを参考にさせていただき、”http://ホスト:ポート/w/messages/websocket” としました。とりあえずこれでいけたので良しとしてます。
1 2 3 4 5 6 7 8 9 10 11 |
val okRequest = Request.Builder().get().url(url).build() val client = OkHttpClient() val socket = client.newWebSocket(okRequest, object : okhttp3.WebSocketListener() { override fun onOpen(webSocket: WebSocket?, response: Response?) { ... } override fun onMessage(webSocket: WebSocket?, text: String?) { ... } override fun onMessage(webSocket: WebSocket?, bytes: ByteString?) { ... } override fun onClosing(webSocket: WebSocket?, code: Int, reason: String?) { ... } override fun onClosed(webSocket: WebSocket?, code: Int, reason: String?) { ... } override fun onFailure(webSocket: WebSocket?, t: Throwable?, response: Response?) { ... } }) |
上記見ればわかると思いますが、コネクションを確立した時、何か受信したとき、エラーが発生したときなどでコールバックを指定できます。
また、
1 2 |
socket.send(text) |
でコネクション確立後にクライアントからサーバへデータを送信することが出来ます。
これを使い、コネクション確立後に CONNECT フレームを送信します。
1 2 3 4 |
val url = URL(hostUrl) val connect = StompFrame(StompFrame.Command.Connect, mapOf("accept-version" to "1.0,1.2", "host" to url.host)) socket.send(connect.toString()) |
保有モザイク量監視の登録
CONNECTEDフレームを受信後に、SUBSCRIBE フレームを送信します。
1 2 3 |
val subscribe = StompFrame(StompFrame.Command.Subscribe, mapOf("id" to id.toString(), "destination" to "/account/mosaic/owned/$address")) socket.send(subscribe.toString()) |
また、SEND フレームを送信して今の状態も取得するようにしておきます。
1 2 3 |
val send = StompFrame(StompFrame.Command.Send, mapOf("destination" to "/w/api/account/account/mosaic/owned"), "{'account':'$address'}") socket.send(send.toString()) |
実際に nem-kotlin で作成したコードは、コネクション確立してから Subscribe させるのも面倒だったので、フレーム送信のリクエストをキューイングする実装にしてあります。
メッセージ受信
受信したテキストを、StompFrame として解析して Body を抽出して Gson で データモデルへマッピングします。
1 2 3 4 |
val frame = StompFrame.parse(receivedText) val mosaic = Gson().fromJson(frame.body, Mosaic::class.java) return mosaic |
動かしてみる
動かしてみます。フレームの動きがわかりやすいように、送受信時にログを仕込んで動かしてみました。
1 2 3 4 5 6 7 8 9 10 |
|2017/11/24 18:19:38.654|I|RxNemWebSocketClient|Opening connection to http://23.228.67.85:7778 |2017/11/24 18:19:38.683|I|RxNemWebSocketClient|Queue frame: command=SUBSCRIBE, headers={id:0,destination:/account/mosaic/owned/アドレス} body= |2017/11/24 18:19:38.683|I|RxNemWebSocketClient|Queue frame: command=SEND, headers={destination:/w/api/account/mosaic/owned,content-length:54} body={'account':'アドレス'} |2017/11/24 18:19:38.973|I|RxNemWebSocketClient|Sending frame: command=CONNECT, headers={accept-version:1.0,1.2,2.0,host:23.228.67.85} body= |2017/11/24 18:19:39.149|I|RxNemWebSocketClient|Received frame: command=CONNECTED, headers={version:1.2,heart-beat:0,0} body= |2017/11/24 18:19:39.149|I|RxNemWebSocketClient|Sending queued frame: command=SUBSCRIBE, headers={id:0,destination:/account/mosaic/owned/アドレス} body= |2017/11/24 18:19:39.149|I|RxNemWebSocketClient|Sending queued frame: command=SEND, headers={destination:/w/api/account/mosaic/owned,content-length:54} body={'account':'アドレス'} |2017/11/24 18:19:39.494|I|RxNemWebSocketClient|Received frame: command=MESSAGE, headers={destination:/account/mosaic/owned/アドレス,subscription:0,message-id:610828d9-2029667,content-length:68} body={"quantity":4952988,"mosaicId":{"namespaceId":"nem","name":"xem"}} |2017/11/24 12:19:39.534|I|RxNemWebSocketClient|Received frame: command=MESSAGE, headers={destination:/account/mosaic/owned/アドレス,subscription:0,message-id:610828d9-2029668,content-length:68} body={"quantity":109,"mosaicId":{"namespaceId":"ttech","name":"ryuta"}} |
CONNECT -> SUBSCRIBE -> SEND 後に、MESSAGE で 2回、XEM と ryuta モザイクの情報が飛んできました。
この状態で、NanoWallet からこのアドレスに ryuta モザイクを送信してみます。
1 2 3 |
|2017/11/24 18:24:25.328|I|RxNemWebSocketClient|Received frame: command=MESSAGE, headers={destination:/account/mosaic/owned/アドレス,subscription:0,message-id:610828d9-2029702,content-length:68} body={"quantity":4952988,"mosaicId":{"namespaceId":"nem","name":"xem"}} |2017/11/24 18:24:26.747|I|RxNemWebSocketClient|Received frame: command=MESSAGE, headers={destination:/account/mosaic/owned/アドレス,subscription:0,message-id:610828d9-2029703,content-length:68} body={"quantity":110,"mosaicId":{"namespaceId":"ttech","name":"ryuta"}} |
MESSAGE が来ました! 1ryuta が追加されて 110 ryuta になってます!
クライアントから何も送らずに、サーバから情報が到着しました!
ちなみに、このログを見たときすごいテンション上がりましたが、ログだけ書いても何にも伝わりませんね。すみません。
さいごに
こんな機能があったとは!WebSocket のおかげで、よけいなポーリングしなくて済みそうですね。
nem-kotlin の方は、すでに開発ブランチにソースを入れたので、次期バージョンで搭載予定です。
nem-kotlin 0.2.0 でリリース済みです。
また、今回実装した nem-kotlin の WebSocket 通信機能を使って、リアルタイムに出金を通知する Skype の Bot を作ってみました。こちらもよかったら使ってみてください。
全然関係ありませんが、今回のシーケンス図、Powered by mermaid.js です。
最後まで読んでいただきありがとうございます。 このブログを「いいな」と感じていただけましたら、Twiter にてフォローいただけるとうれしいです。ブログ更新情報などもお届けします。
Follow @ryuta461
この記事をシェアする