Protocol Buffers動かしてみる

## 初めに

   こんにちは。CYBIRDエンジニア Advent Calendar(http://qiita.com/advent-calendar/2017/cybird)
   15日目の@keitarou です。14日目は@sakamoto_kojiさんの「[Actions on Google(Dialogflow)での構築知見を少し](https://qiita.com/sakamoto_koji/items/8a9bb8a3f2063b4b5f34)」でした。

# Protocol Buffers

「Protocol Buffers」が何かっていうところは、他のネットの情報が豊富なので軽くですが、
単に言うと、バイナリ形式のメッセージシリアライズフォーマット。
主に業務などで利用しているのは、「JSON」はほぼ全てですが、

- データサイズをできるだけ小さくしたい！
- エンコード・デコードをできるだけ高速に省メモリで！
- gRPCとか、RPCのフレームワークを使ってマイクロサービスしたい！

など、そのへんの話をされた時に困らないように、簡単なデモを動かしての予習です。


その他、バイナリ形式の同様技術としては

- Flatbuffers
- Apache Thrift
- MessagePack
- BSON

などもある模様。


# やってみること

- 1. ビットコインの取引履歴をprotocol-buffer返すtcpサーバーを立てる
- 2. (1)で作ったサーバーへtcpリクエストを送り、protocol-bufferをパースして画面へ出力するクライアントを作る
- 3. 手っ取り早く終わらせるために、クライアント・サーバーともにNode.jsで作ることとする

※今回はNode.jsを例で使っていますが、Java, Golang, C#, PHPとかいろんな言語で利用することも勿論できます。
※ビットコインの取引履歴はCoincheckのPublic APIを利用させて頂きます。
https://coincheck.com/ja/documents/exchange/api#public
図で書くとこういった感じです。
<img width="261" alt="スクリーンショット 2017-12-14 20.03.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/19855/412a89d3-509f-79b5-f44c-571a95a44f28.png">


# protocコマンドのインストール

あとに記述しますが、.protoファイルというインタフェース定義言語(IDL)をビルドする必要があります。
ビルドに必要なコマンドをインストールします

```
brew install protobuf
```

# .protoファイルの作成


```
https://coincheck.com/api/trades
[
  {
    "id": 60989919,
    "amount": "0.004",
    "rate": 1912722,
    "order_type": "sell",
    "created_at": "2017-12-14T10:14:09.000Z"
  },
  .
  .
  .
  .
  .
  .
]
```

こんな感じのJSON APIを扱えたら良いのでこんな感じで


```messages.proto
syntax = "proto3";

message Trades
{
  enum OrderType
  {
    UNKNOWN = 0;
    BUY     = 1;
    SELL    = 2;
  }

  message Trade
  {
    int32     id        = 1;
    string    amount    = 2;
    int32     rate      = 3;
    OrderType orderType = 4;
    string    createdAt = 5;
  }

  repeated Trade trades = 1;
}
```

シンタックスについては公式のドキュメントがわかりやすいです。
日本語ではありませんが、それほど難しくはなさそうです。
https://developers.google.com/protocol-buffers/docs/proto3

もっといいのがあるかもしれませんが、jsonから.protoに変換するツールもあったりします。
https://www.site24x7.com/tools/json-to-protobuf.html


# .prpotoファイルのビルド

```
protoc -o ./messages.desc messages.proto
```

出力されたmessage.descをプログラミングのランタイムで利用することになります。

# npmライブラリのインストール

```
# Protocol Buffersのエンコード/デコードができるように
npm install --save node-protobuf

# CoincheckのAPIを簡単に使うために
npm install --save superagent
```

# サーバーの実装

```server.js
const fs       = require('fs');
const net      = require('net');
const request  = require('superagent');
const protobuf = require('node-protobuf');
const messages = new protobuf(fs.readFileSync('./messages.desc'));
const TARGET_APIURL = 'https://coincheck.com/api/trades';

const server = net.createServer((conn) =>
  {
    conn.on('data', () =>
    {
      request
        .get(TARGET_APIURL)
        .end((err, res) =>
        {
          if(!err)
          {
            // シリアライズ用に、snake,Camelの変換および、enumとして扱いたい値の変換など
            let fmtData = {
              trades: res.body.map((d) => {
                return {
                  id        : d.id,
                  amount    : d.amount,
                  rate      : d.rate,
                  orderType : d.order_type.toUpperCase(),
                  createdAt : d.created_at
                }
            })};
            let buf = messages.serialize(fmtData, 'Trades');
            conn.write(buf);
          }
          else
          {
            console.error(err);
            conn.write('E');
          }
        });
    });
    conn.on('close', () =>
    {
      console.log('close connection');
    });
  }
).listen(4000);
```

★サーバー起動方法

```
node server.js
```

# クライアントの実装

↑で作ったサーバーを1000ms間隔で叩いて出力するだけです。

```client.js
const fs       = require('fs');
const net      = require('net');
const protobuf = require('node-protobuf');
const messages = new protobuf(fs.readFileSync('./messages.desc'));
const client   = new net.Socket();

client.on('data', (data) =>
{
  let buf = new Buffer(data);
  let json = messages.parse(buf, 'Trades');
  json.trades.map(d => { console.log(JSON.stringify(d)); });
});
client.on('close', () =>
{
  console.log('connection close');
});

client.connect('4000', 'localhost', () =>
{
  setInterval(() =>
  {
    client.write('ping');
  }, 1000);
});
```

★実行方法

```
node client.js
```

# 実行結果

```
$ node client.js

{"id":60995449,"amount":"0.00452097","rate":1923368,"orderType":"BUY","createdAt":"2017-12-14T10:32:36.000Z"}
{"id":60995448,"amount":"0.00547903","rate":1923192,"orderType":"BUY","createdAt":"2017-12-14T10:32:36.000Z"}
{"id":60995447,"amount":"0.005","rate":1923032,"orderType":"BUY","createdAt":"2017-12-14T10:32:36.000Z"}
{"id":60995446,"amount":"0.005","rate":1923000,"orderType":"SELL","createdAt":"2017-12-14T10:32:35.000Z"}
{"id":60995445,"amount":"0.005","rate":1923006,"orderType":"BUY","createdAt":"2017-12-14T10:32:35.000Z"}
{"id":60995444,"amount":"0.00155723","rate":1923000,"orderType":"SELL","createdAt":"2017-12-14T10:32:35.000Z"}
{"id":60995443,"amount":"0.01","rate":1923005,"orderType":"BUY","createdAt":"2017-12-14T10:32:35.000Z"}
{"id":60995442,"amount":"0.005","rate":1923000,"orderType":"SELL","createdAt":"2017-12-14T10:32:35.000Z"}
{"id":60995441,"amount":"0.005","rate":1923005,"orderType":"BUY","createdAt":"2017-12-14T10:32:34.000Z"}
{"id":60995440,"amount":"0.005","rate":1923000,"orderType":"SELL","createdAt":"2017-12-14T10:32:34.000Z"}
{"id":60995439,"amount":"0.10463106","rate":1923000,"orderType":"SELL","createdAt":"2017-12-14T10:32:34.000Z"}
{"id":60995438,"amount":"23.56338218","rate":1923000,"orderType":"BUY","createdAt":"2017-12-14T10:32:34.000Z"}
```

それっぽくデータをデコードして出力することが出来ていそうです。


おまけに、telnetで実行するとどんなかんじかですが、こんな感じです。
文字列として扱っている値はわかりますが、数値型や列挙型のものは流石に人間で解釈するのは非常に困難です。

```
$ telnet localhost 4000

Trying ::1...
Connected to localhost.
Escape character is '^]'.


0.03503u *2017-12-14T10:34:21.000Z
0.016u *2017-12-14T10:34:21.000Z
0.3u *2017-12-14T10:34:21.000Z
0.006u *2017-12-14T10:34:20.000Z
0.01u *2017-12-14T10:34:20.000Z
0.01u *2017-12-14T10:34:20.000Z
0.0251u *2017-12-14T10:34:20.000Z
1
0.15255587 *2017-12-14T10:34:20.000Z
0.01Ҩu *2017-12-14T10:34:20.000Z
0.3u *2017-12-14T10:34:20.000Z
0.006u *2017-12-14T10:34:20.000Z
0.026u *2017-12-14T10:34:20.000Z
0.005ϧu *2017-12-14T10:34:20.000Z
1
0.00460936u *2017-12-14T10:34:20.000Z
0.01 *2017-12-14T10:34:20.000Z
0.01 *2017-12-14T10:34:19.000Z
0.025u *2017-12-14T10:34:17.000Z
0.011u *2017-12-14T10:34:15.000Z
0.005u *2017-12-14T10:34:15.000Z
0.01ߡu *2017-12-14T10:34:15.000Z
0.01 *2017-12-14T10:34:15.000Z
0.01ݢu *2017-12-14T10:34:14.000Z
0.005u *2017-12-14T10:34:14.000Z
1
0.01273603ӡu *2017-12-14T10:34:13.000Z
0.046סu *2017-12-14T10:34:13.000Z
0.002եu *2017-12-14T10:34:13.000Z
0.006٥u *2017-12-14T10:34:13.000Z
0.01٣u *2017-12-14T10:34:13.000Z
1
0.07904541٣u *2017-12-14T10:34:13.000Z
0.0083u *2017-12-14T10:34:13.000Z
1
0.35365459ԥu *2017-12-14T10:34:12.000Z
0.15 *2017-12-14T10:34:12.000Z
0.01ڣu *2017-12-14T10:34:12.000Z
0.006٣u *2017-12-14T10:34:12.000Z
0.03119âu *2017-12-14T10:34:12.000Z
0.4327¢u *2017-12-14T10:34:12.000Z
0.01ݡu *2017-12-14T10:34:12.000Z
1
0.00645541͡u *2017-12-14T10:34:12.000Z
0.01881âu *2017-12-14T10:34:11.000Z
0.01999u *2017-12-14T10:34:11.000Z
0.0042 *2017-12-14T10:34:11.000Z
0.004 *2017-12-14T10:34:11.000Z
0.006u *2017-12-14T10:34:11.000Z
0.02u *2017-12-14T10:34:10.000Z
0.003 *2017-12-14T10:34:10.000Z
0.0054u *2017-12-14T10:34:10.000Z
0.03 *2017-12-14T10:34:07.000Z
0.02u *2017-12-14T10:34:07.000Z
0.0046ȟu *2017-12-14T10:34:07.000Z
0.006 *2017-12-14T10:34:07.000Z
0.011u *2017-12-14T10:34:07.000Z
```

#　最後に

時間があればJSONとの性能比較とかもしたかったんですが、申し訳ないですがこれにて断念でございます。
特に、UnityやCocos2dxなどのゲーム系の通信周りのチューニングのためにJSONなどのテキストベースのシリアライズ方式から、バイナリ形式にしてみるのもよいのではないでしょうか。
それでは、これにて失礼致します。

   CYBIRD エンジニア Advent Calendar(http://qiita.com/advent-calendar/2017/cybird) 明日は、@cy-tatsuya-sakaiさんの「Unity uGUI ボタンのイベントを一か所で受け取る」らしいです！楽しみですね！