【読書】Webを支える技術 を読んだ

はじめに

私は IT系といえばそうなのかなという感じの職場にいますが、Web系というより組み込み系に近い職場です。ただ、最近では組み込み分野でも IoT の煽りを受けてWebサーバーが組み込まれた機器が増えてきています。そういう意味では組み込み系もWeb系といえるかもしれませんね。そういう理由もあってWebの基本技術を知識として身に着けておくべくこの本を読もうと思いました。なので、この本を読めばある程度Webの何たるかがわかるかなと思っていました。結果としてそれはいい意味で裏切られたわけですが、最後には読んでよかったなと思えた本でした。

Webサービスのリソース設計についての本

この本のメインテーマは、表紙にある通り「実践的なWebサービスの設計指針」についての本になるのかなと思います。なので、第5部の「Webサービスの設計」の部分が作者の一番書きたかったことなのかなと思いました。そして、第5部が一番熱を入れて読めた気がします。第4部までは第5部を説明するための用語を解説しているような、そういった印象を受けました。ただ、第4部までも重要なことがたくさんあるので、読まなくていいというわけではないですが、メインどころだけ読みたい!という人は第5部から読んでみてもいいかもしれません。

個人的によかったところ

第16章

第16章の「トランザクション」や「排他制御」のパートは私が今まで疑問に思っていた部分に答えてくれたよいパートだったと思います。おそらく今後も何度も見返すだろうなぁと思います。HTTPではないですが、似たような機構を持つプロトコルを使ったことがあり、確かにこうやってトランザクション管理してたなぁ。あれはそういう意味だったのか。と今更ながらに関心しました。

URIの設計指針

第5章の「URI設計指針」には以下のように記載があります

上4つはなるほどなぁ~と読んでいたのですが「URIはそのリソースを表現する名詞である」はびっくりしました。だって、Ruby On Rails とか show とか使ってなかった⁉って思ったからです。でもちゃんとそれについても触れていて Ruby On Rails は 2.0 移行から メソッド名をURIに含めなくなったそう。(そうだったのか。。。)

最近、どや顔でRESTに設計するならリソース名は動詞にすべき。みたいなことを言ってしまっていたので、読んでいて恥ずかしい気持ちになった。これからは「リソース名は名刺にすべき」とどや顔で言うようにします。

まとめ

ネット上の評価を見てると「内容が古い」という評価がいくつかありました。初版が2010年なので確かに情報は古いかなと思っいましたが書いている内容は今でも十分通用する内容なのかなと思います。Webの歴史やベースを知りたい人やWebサービスの設計ってどうやるんだろうって思っている人には一読の価値はあるのかなと思いますので皆さんも是非読んでみてください。

読んだ本

www.amazon.co.jp

※ このリンクを張り付けるときに気づいたけど、購入が2020年の2月になってた。2年越しにようやく読み切ったのか。遅読にもほどがある。。。

所有権? String と 文字列スライス

文字リテラル Hello から String を作成し、一部を文字列スライスとして切り出した後、String スコープ外で文字列スライスを使う。というシナリオを考えたが、以下のようにコンパイルエラーが発生する。文字列スライスが参照している大元の String がスコープを抜ける際に Drop してしまうためだ。

fn main() {
    let sub: &str;
    {
        let s = String::from("Hello");
        sub = &s[0..2];
//             ^ borrowed value does not live long enough
    }

    println!("{}", sub);
//                 --- borrow later used here
}

文字列スライスを clone() すれば行けるか?と思ったが、文字列スライスは clone() メソッドを実装していないようだ。同様に copy() メソッドも実装されていなかった。

fn main() {
    let sub: &str;
    {
        let s = String::from("Hello");
        sub = &s[0..2].clone();
//                     ^^^^^ method not found in `str`
    }

    println!("{}", sub);
}

一度文字列スライスから String を作成し、それを as_str() で再度文字列スライス化しても、新たに生成した String はスコープを抜ける際に Drop() されてしまうので、同じだった。

fn main() {
    let sub: &str;
    {
        let s = String::from("Hello");
        sub = String::from(&s[0..2]).as_str();
//            ^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
//            |
//            creates a temporary which is freed while still in use
    }

    println!("{}", sub);
//                 --- borrow later used here
}

文字列スライスを取得するのをあきらめて、文字列スライスを String にしたものを取得するようにすると、コンパイルが通る。

fn main() {
    let sub: String;
    {
        let s = String::from("Hello");
        sub = String::from(&s[0..2]);
    }

    println!("{}", sub);
}

to_string() のほうが String::from() より簡潔に書けてよさそう。

fn main() {
    let sub: String;
    {
        let s = String::from("Hello");
        sub = s[0..2].to_string();
    }

    println!("{}", sub);
}

というか、今回の例ではこれでもいいのか。簡潔に書きすぎて、いったい何がしたいんだ。。。というコードになるけど。。。

fn main() {
    let sub: String;
    {
        sub = "Hello"[0..2].to_string();
    }

    println!("{}", sub);
}

参考資料

qiita.com

npm? webpack?

今更、npm/webpack ってなんだ!?ってなったのでちょろっと調べてみた。数年前に一回React触ったけどまるっと忘れてしまったので復習。

参考文献

https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

https://qiita.com/koedamon/items/3e64612d22f3473f36a4

NPMとは

npm は Node Package Manager の略。javascriptパッケージをweb上の管理リポジトリから依存関係を解決しつつイイカンジに取ってくるのが主な役割と思われる。apt っぽい位置づけかな?でも apt よりも役割が多いイメージがある。Node.js のサブシステムで、node入れたら一緒についてくるヤツ。

package.json

npm init -y とかするとできる。jsパッケージ管理の起点となるファイル。ファイルの中身で最低限知っておかないとダメそうなやつをピックアップして以下に書いてみる。

dependencies & devDependencies

dependencies には production 環境、devDependencies には test や development 環境の依存パッケージの一覧が記述されるらしい。書き方等はおいおい調べようかな。

scripts

ここで記述したコマンドは npm run <command> という形で呼び出すことができる。便利。例えば、以下のような記述になっていたとして

  "scripts": {
    "start": "node index.js"
  },

npm run start とすると node index.js が実行される。この例はあまりよくないけど、何しかタイプ数が減って便利。とかそういう感じだとおもう。

また、いくつかのキーワードは特別扱いらしく、start とか test とかは npm startnpm test で実行可能らしい。

あと、npm build とかの前後でコマンドを実行したい場合は prebuildpostbuild のように pre / post をつけておくとイイカンジにやってくれるみたい。

WebPackとは?

必要なjsファイルをイイカンジにくっつけて一つのjsファイルにすることでモジュールが使えない古いブラウザでも動くようにしよう!というモチベーションの元作成されたjsバンドラーとのこと。

npm install webpack webpack-cli --save-dev でインストールする。

npx webpack をすると、./src/index.js を起点にjsファイルをまとめて ./dist/main.js に出力する。

npx コマンドは ./node_modules/.bin/ にパスを通すことなく webpack が使えるようになる魔法のコマンド!便利!

まとめ

ここまで調べて、僕に必要なコマンドは npx webpack だったということになりそうです。

esbuild だと npx esbuild ./src/index.js --bundle --minify --outfile=./dist/main.js という感じになりそう。

Postgresql の設定 (Rails 編)

Rails はすでにインストールされているものとします。

Postgresql の インストール

www.digitalocean.com

上記を参考にインストール。今回は Debian 公式パッケージをインストール

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install postgresql postgresql-contrib libpq-dev

ユーザーの作成

bamchoh というユーザーをパスワード付きで作成

$ sudo -u postgres createuser -s bamchoh -P
新しいロールのためのパスワード: *****
もう一度入力してください:*****

Rails のデフォルトで作成される database.yml は production モードしか usernamepassword を要求しない。なので、development / test モードで使用する分には作成は必要ないかもしれない。production モードでデフォルトで指定されている username は アプリの名前がそのまま使われている模様。 testapp というアプリを作成すると username には testapp が指定される。パスワードは環境変数から取得するように設定され、testapp の場合は TESTAPP_DATABASE_PASSWORD という環境変数から取得するように設定されるので、productionモードで使用する場合は適宜設定するようにする。

テスト用Rails アプリの作成

適当なフォルダを作成。今回は testapp というフォルダを作成。

bundle exec rails new . -d postgresql の途中でファイルを上書きするかどうか聞かれるが、すべて Y で上書きする。

$ mkdir testapp
$ cd testapp
$ bundle init
$ echo 'gem "rails"' >> Gemfile
$ bundle config --local path vendor/bundle
$ bundle install
$ bundle exec rails new . -d postgresql
$ bundle exec rails db:create
$ bundle exec rails g scaffold Tweet title:string content:text
$ bundle exec rails db:migrat
$ bundle exec rails s -b 0.0.0.0

http://localhost:3000/tweets/ でアクセスできたら成功。

Raspberry PI 4 で RS485 通信 (双方向)

まとめ

Raspberry Pi でサポートしている UART は RTSトグルをサポートしていないので RTS は自力でトグルする必要がある

使用した機器

今回は Raspberry Pi 4 と TTL-RS485 コンバーターを使用しました。

使用したTTL-RS485 コンバーター www.amazon.co.jp

UART2とUART4を有効にする。

www.ingenious.jp

こちらを参考に UART2 と UART4 を有効にする。

Linuxシリアルコンソールを無効化してから /root/config.txt の末尾に以下を追加した

dtoverlay=disable-bt
dtoverlay=uart2,ctsrts
dtoverlay=uart4,ctsrts

結線

Raspberry Pi 4 と TTL-RS485 コンバーター を以下のように結線します。

※ ICと記載されている部分がコンバーターになります

f:id:bamch0h:20211214011221p:plain

TTL-RS485 コンバーター

RS485は差動信号通信なので、2線ありますが、送信と受信が混線できません。なので送信モードと受信モードを切り替えるためには DEとREに切り替え信号を送る必要があります。

・送信イネーブル (DE) Hにすると送信回路がオンになります。

・受信イネーブル (RE) Lにすると受信回路がオンになります。

なので、送信するときは DE / RE の両方を HIGH にして、受信するときは DE / RE 両方を LOW にします。

PL011 は RTS トグルをサポートしていない

f:id:bamch0h:20211214004426p:plain

引用ドキュメント:https://developer.arm.com/documentation/ddi0183/f/

上記の通り、Raspberry Pi の UART チップの PL011の RTS ハードウェアフローコントロールでは受信バッファが規定値に達しないと RTS が動作してくれないので、送信する直前に RTS を LOWにして、送信し終わったら HIGH にするような仕組みはハードウェアレベルでは存在していない。なので自分自身で信号を制御する必要があります。

TIOCMGET / TIOCMSET

参考文献:Manpage of TTY_IOCTL

TIOCMGET / TIOCMSET を使うとシリアルの通信ラインを制御できます。RTS以外にもいろいろな情報が int でとれるので、TIOCM_RTS でビットマスクをして RTSのライン状態を取得、設定します。

RTS を OFF に設定する場合は以下のようにします。制御信号はOFFの時にHIGH、ONの時に電圧がLOWに設定されます。なので、RTSをOFFすると、TTL-RS485コンバーターのDE/REにはHIGHになり、送信モードになります。

int RTS_flag = 0;
ioctl(fd, TIOCMGET, &RTS_flag); // 一度取得
RTS_flag &= ~TIOCM_RTS; // 
ioctl(fd, TIOCMSET, &RTS_flag);

送信後待ち処理

RTS を OFF した後、write() した後 RTS を ON することでデータの送信処理は完了しますが、write() は FIFO にデータを積んだ後すぐ戻ってくるため、送信 FIFO が空になるまで待つ必要があります。待ち時間は以下の計算式で計算できます。

(データ長 + Stop ビット + パリティビット + Start ビット) * 送信データバイト数 / ボーレート = 待ち時間 (sec)

データ長 8 bit, Stop ビット 1 bit, 偶数パリティあり, 2400 bps で送信データが8バイトの場合の計算は以下の通りです。

(8 + 1 + 1 + 1) * 8 / 2400 = 11 * 8 / 2400 = 0.3666... (sec) = 366 (ms) = 366666 (μs)

これを usleep等の引数に渡して待ち処理を入れます。

送信側プログラム例

上記を踏まえて、送信プログラムを組むと以下のようになります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <linux/serial.h>
#include <sys/ioctl.h>

void my_write(int fd) {
    unsigned char buf[8] = {
        0x01,
        0x03,
        0x00,
        0x00,
        0x00,
        0x02,
        0xC4,
        0x0B
    };

    unsigned int size = sizeof(buf);

    int RTS_flag = 0;
    ioctl(fd, TIOCMGET, &RTS_flag);

    RTS_flag &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);

    write(fd, buf, size);

    int duration = size * 11 * 1000000 / 2400;
    usleep(duration);

    RTS_flag |= TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);
}

int main(int argc, char *argv[])
{
    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open error\n");
        return -1;
    }

    struct termios tio = {0};
    tio.c_cflag |= CREAD;
    tio.c_cflag |= CLOCAL;
    tio.c_cflag &= ~CRTSCTS;
    tio.c_cflag |= B2400;
    tio.c_cflag |= PARENB;
    tio.c_cflag |= 0;
    tio.c_cflag |= CS8;

    // non canonical mode setting
    tio.c_lflag = 0;
    tio.c_cc[VTIME] = 0;
    tio.c_cc[VMIN] = 1;

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &tio);

    my_write(fd);

    unsigned char buf[1];
    unsigned char rxbuf[4086] = {0};
    int len = 0;
    int total = 0;
    while(1) {
        len = read(fd, buf, 1);
        if (len <= 0) {
            fprintf(stderr, "read error!!\n");
            break;
        }

        rxbuf[total] = buf[0];
        total++;

        if(total == 9) {
            for(int i = 0; i < total; i++) {
                printf("%02X:", rxbuf[i]);
            }
            printf("\n");

            memset(rxbuf, 0, total);
            total = 0;

            my_write(fd);
        }
    }

    close(fd);
    return 0;
}

受信側プログラム例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <linux/serial.h>
#include <sys/ioctl.h>

void my_write(int fd) {
    unsigned char buf[9] = {
        0x01,
        0x03,
        0x04,
        0x00,
        0xE2,
        0x01,
        0xF2,
        0xDA,
        0x10
    };

    unsigned int size = sizeof(buf);

    int RTS_flag = 0;
    ioctl(fd, TIOCMGET, &RTS_flag);

    RTS_flag &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);

    write(fd, buf, size);

    int duration = size * 11 * 1000000 / 2400;
    usleep(duration);

    RTS_flag |= TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);
}

int main(int argc, char *argv[])
{
    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open error\n");
        return -1;
    }

    struct termios tio = {0};
    tio.c_cflag |= CREAD;
    tio.c_cflag |= CLOCAL;
    tio.c_cflag &= ~CRTSCTS;
    tio.c_cflag |= B2400;
    tio.c_cflag |= PARENB;
    tio.c_cflag |= 0;
    tio.c_cflag |= CS8;

    // non canonical mode setting
    tio.c_lflag = 0;
    tio.c_cc[VTIME] = 0;
    tio.c_cc[VMIN] = 1;

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &tio);

    char buf[255] = {0};
    unsigned char rxbuf[4086] = {0};
    int total = 0;
    int len;
    printf("recv...\n");
    while(1) {
        len = read(fd, buf, 1);
        if (len <= 0) {
            fprintf(stderr, "read error!!\n");
            break;
        }

        rxbuf[total] = buf[0];
        total++;

        if(total == 8) {
            for(int i = 0; i < total; i++) {
                printf("%02X:", rxbuf[i]);
            }
            printf("\n");

            memset(rxbuf, 0, total);
            total = 0;

            my_write(fd);
        }
    }

    close(fd);
    return 0;
}

実行例

受信側を起動してから別ターミナルで送信側を実行します。引数にはシリアルデバイスを指定します。

$ ./recv /dev/ttyAMA2
$ ./sender /dev/ttyAMA1

まとめ

Raspberry pi 4 と TTL-RS485 コンバーターを使用して RS485 通信 (双方向)をやってみました。Raspberry pi 4 では RTSトグル処理をユーザーレベルで制御しなければならないため使い勝手があまりよくないと感じました。RS485はマイコン等を使用したほうがRaspberry pi 4 のコードはシンプルになるかもしれませんね。

2021年度版、TSharkを使ってリアルタイムパケットモニタリング

2013年に以下の記事を書いた。この時はJSONではなく、XMLフォーマットを用いてリアルタイムパケットモニタリングを実現したが、最近では jsonが隆盛してきているため、jsonフォーマットでのリアルタイムパケットモニタリングを行いたい。また、Rubyのバージョンも 3.0 になり、Thread を使うより Ractor を使うほうがナウというもの。

qiita.com

JSON版パケットモニタリングコードサンプル

require 'open3'
require 'json'

def main
  cmd = [
    %q(C:\Program Files\Wireshark\tshark.exe),
    '-i', '5',
    '-Y', 'tcp',
    '-T', 'json'
  ]

  r1 = Ractor.new(cmd) do |command|
    _, stdout, stderr, _ = Open3.popen3(*command)
    puts stderr.gets
    json_str = ''
    while line = stdout.gets
      json_str += line
      next unless line =~ /^  },$/

      json_str = json_str&.chomp&.delete_suffix(',')&.delete_prefix('[')&.lstrip
      json_data = JSON.parse(json_str)

      process_json(json_data)

      json_str = ''
    end
  end

  r2 = Ractor.new do
    gets
  end

  Ractor.select(r1, r2)
end

def process_json(json_data)
  payload = json_data['_source']['layers']['tcp']['tcp.payload']
  p payload&.split(':')&.size.to_i
end

main

コード解説

main() メソッドが メイン部分、tsharkからJSONデータを受け取り、パースし、process_json() メソッド に jsonデータを渡すを繰り返す。 process_json() メソッドが jsonデータを加工して処理する部分

コマンド生成

5行目~10行目は tshark.exe のコマンドを作成している部分。

cmd = [
  %q(C:\Program Files\Wireshark\tshark.exe),
  '-i', '5',
  '-Y', 'tcp',
  '-T', 'json'
]

-iイーサネットインターフェースの番号を指定するオプション。数字を指定するが、この数字はどのように選べばいいか?というと、tshark.exe -D とするとインターフェースの一覧が表示されるので、その頭の番号を指定するとよい。私の環境では 5番目のインターフェースがインターネットにつながるイーサネットカードであったのでそれを選択している。

-Y は モニタリングしたパケットをフィルタするためのオプション。-f もフィルタするためのオプションであるが、こちらはキャプチャする前にフィルタするためのオプション(キャプチャフィルタ)で -Y はキャプチャした後表示をフィルタするためのオプション(ディスプレイフィルタ)になる。Wiresharkを使っていると馴染みがあるのはディスプレイフィルタとなると思うので、今回は -Y を使用している。 (キャプチャフィルタの構文がわからないわけじゃないんだからね!!)

-T は 出力フォーマットのオプションで、 json を指定すると JSONで出力される。

Ractor

Ractor の技術は今回の tshark のモニタリングの本筋からは逸れるので軽く説明する。(というかちゃんと理解できてない)

Ruby にはもともと Thread という並列処理機構があったが、GILの関係でCPUのコア数をフルに活用できていなかった。その制約を取っ払う目的で作られたのが Ractor だと思っている。Ruby で真の並列処理を行いたいのであれば Ractor を使おう。というのが最近のトレンドな気がする。

以下に Ractor のサンプルコードを示す。

a = "hello"
r1 = Ractor.new(a) { |a_|
  sleep 5
  a_
}

r2 = Ractor.new {
  gets
}

_, msg = Ractor.select(r1, r2)
puts msg

上記の例では、2つの Ractor を生成し、 Ractor.select でどちらかが終了するのを待ち受けている。 Ractor の最後の結果が msg に入る。 5秒何も入力しなかったら "hello" が表示され、5秒以内に何か入力するとその文字列が出力される。 Ractor.new(a) { |a_| ~ } のように new に変数を与えるとそれを Ractor 内部で使用することができる。基本的に Ractor のスコープを超えて変数は参照できないので、引数で渡す必要がある。(そうせずに値を渡す方法もあるようだが、ここでは割愛。)

JSON パース

13行目で tshark を実行し、14行目で tsharkから標準エラー出力に出力されるモニタ情報を一行だけ取得し表示してる。一応これで、tsharkが正常に起動していることを確認できる(かな?)

tsharkから出力されるJSONテキストの形式は 配列がルートとなり、その中にパケット情報がオブジェクトとしてパックされている。以下、パケット例

[
  {
    "_index": "packets-2021-11-04",
    "_type": "doc",
    "_score": null,
    "_source": {
      "layers": {
        "frame": {
          "frame.interface_id": "0",
          "frame.interface_id_tree": {
            "frame.interface_description": "イーサネット"
          },
          "frame.encap_type": "1",
          ...
        },
        "eth": {
          ...
          },
        },
        "ip": {
          "ip.version": "4",
          "ip.hdr_len": "20",
          ...
        },
        "tcp": {
          "tcp.srcport": "55291",
          "tcp.dstport": "443",
          "tcp.port": "55291",
          "tcp.port": "443",
          ...
        },
        "tls": {
          "tls.record": {
            "tls.record.content_type": "23",
            ...
          }
        }
      }
    }
  },
  {
    "_index": "packets-2021-11-04",
    "_type": "doc",
    "_score": null,
    "_source": {
      "layers": {
        "frame": {
        ...
        }
      }
    }
  },
  {
    ...
  }
  ...

抽象化すると [ { パケット1 }, { パケット2 }, { パケット3 }, ... { パケットn } ] という構造になる。tsharkが起動されモニタリングが開始されると [ から出力され、パケット1, パケット2, .. と出力されていき、tsharkがモニタリングを終了するときに ] が出力され JSONデータとして完成する。リアルタイムでモニタリングするには配列ノードは無視して、中の各パケットオブジェクトを逐次取得して処理しなければならない。戦略としては オブジェクトのお尻の } がくるまで gets で取得して頭の無駄な文字をトリミングしてJSONオブジェクトとする方法を考える。それが 16行目 ~ 20行目の部分になる。

    while line = stdout.gets
      json_str += line
      next unless line =~ /^  },$/

      json_str = json_str&.chomp&.delete_suffix(',')&.delete_prefix('[')&.lstrip

16行目の while line = stdout.gets で1行ずつ取得する。

17行目で取得した1行を json_str に結合する json_str が最終的に JSON文字列となり JSON.parse() に渡される

18行目で取得した1行がオブジェクトのお尻かどうかをチェックする。今のところ tshark はパケットオブジェクトのお尻として }, を1行で出力してくれるようなので、これを基準にお尻かどうか判断している。

20行目で取得した json_str を成形する。

  • まず、chomp で無駄な末尾改行を取り除く
  • 次に、delete_suffix(',') で無駄なカンマを取り除く
  • 続いて、delete_prefix('[') で頭の [ を取り除く
  • 最後に、頭の空白を取り除く

以下、成形の具体例となる。

もし、tsharkが起動して一発目のパケットであれば以下のような構造になっているはずである。

[\n
␣␣{\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣},\n

なので、①末尾の改行を取り除き

[\n
␣␣{\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣},

次に、②末尾のカンマを取り除き

[\n
␣␣{\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣}

③ 頭の [ を取り除き

\n
␣␣{\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣}

④ 頭の空白・改行を取り除くことで、正しいJSONデータが取得できる。

{\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣␣␣~なにかデータ~\n
␣␣}

2回目以降のパケットでは③の [ を取り除く部分が不要だがRubyでは無駄に処理が動くだけで入力文字列がそのまま出力されるので問題なく動作する。

JSONデータを処理

21行目で成形したJSONデータをパースした後、process_json() メソッドにそのデータを渡し処理する。

def process_json(json_data)
  payload = json_data['_source']['layers']['tcp']['tcp.payload']
  p payload&.split(':')&.size.to_i
end

今回は tcpペイロードを取得し、そのサイズを出力するようにした。JSONデータのどのフィールドにどのデータがあるかは 一度 JSONデータを出力してみないとわからないので、そのあたりは筋力が必要になるだろう。

JSONデータの調べ方

パケットにどのようなフィールドがあるかを調べるのに、tsharkを動かして、コマンドプロンプトでいちいちJSONを見ていたのでは埒が明かないときもあるだろう。そういう時は Wiresahrkに頼ろう。 Wiresharkの [ファイル(F)] → [エキスパートパケット解析] → [JSONとして] を選択すると取得したパケットをJSONで出力することが可能だ。出力方法には様々なオプションがあるので、ある程度フィルタリングした後出力し、好きなエディタでフィールドをチェックするといいだろう。

f:id:bamch0h:20211104231435p:plain
Wiresharkで取得したパケットをJSONで出力

参考資料

techlife.cookpad.com

www.wireshark.org

microsoft / vs-streamjsonrpc の Formatter を MessagePack にして使う

github.com

Microsoft が提供している jsonrpc ライブラリ vs-streamjsonrpcJson 以外にも MessagePack でシリアライズ・デシリアライズできるように作成されています。

以前の記事 microsoft/vs-streamjsonrpc のバージョンが 2 になっていたので使ってみた。 - bamch0h’s diary を MessagePackで通信できるようにしてみたコードが以下。

JsonRpc.Attach を使う代わりに、JsonRpc(IJsonRpcMessageHandler) を使うのがミソ。ハンドラは LengthHeaderMessageHandler を使います。HeaderDelimitedMessageHandler が内部的にはデフォルトで使われているようですが、 Formatter が IJsonRpcMessageTextFormatter を実装していないとダメなようなので使えません。なので、 LengthHeaderMessageHandler を使います。

また、メソッドの引数や戻り値にクラスを指定する場合は各クラスにMessagePackの属性をつけておく必要があります。自身で定義している場合は属性付けれますが、サードパーティーが提供してくれているクラスを指定する場合はどうするんでしょうね。ラップクラスを自身で定義する必要がありそうです。

using System;
using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;
using StreamJsonRpc;
using MessagePack;

namespace ConsoleApp5
{
    public class Target
    {
        public int Add(int a, int b)
        {
            return a + b;
        }

        public Parameter Test(Parameter p)
        {
            p.Param1 += "-aaa";
            p.Param2.Param2 += "-bbb";
            return p;
        }
    }

    [MessagePackObject]
    public class Parameter
    {
        [Key(0)]
        public string Param1 { get; set; }

        [Key(1)]
        public SubParameter Param2 { get; set; }
    }

    [MessagePackObject]
    public class SubParameter
    {
        [Key(0)]
        public string Param2 { get; set; }
    }

    public class Client
    {
        public async Task Start(string pipename)
        {
            var stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous);
            stream.Connect();
            var handler = new LengthHeaderMessageHandler(stream, stream, new MessagePackFormatter());
            using (var rpc = Utils.NewRpc(handler))
            {
                var ret = await rpc.InvokeAsync<int>("Add", new object[] { 1, 2 });
                Console.WriteLine(ret);

                var ret2 = await rpc.InvokeAsync<Parameter>("Test", new Parameter() { Param1 = "1", Param2 = new SubParameter() { Param2 = "2" } });
                Console.WriteLine(ret2.Param1);
                Console.WriteLine(ret2.Param2.Param2);
            }
        }
    }

    public class Server
    {
        public void Start(string pipename)
        {
            var stream = new NamedPipeServerStream(pipename, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            stream.WaitForConnection();
            var handler = new LengthHeaderMessageHandler(stream, stream, new MessagePackFormatter());
            using (var rpc = Utils.NewRpc(handler, new Target()))
            {
                rpc.Completion.Wait();
            }
        }
    }

    public class Utils
    {
        public static JsonRpc NewRpc(IJsonRpcMessageHandler handler, object target = null)
        {
            var rpc = new JsonRpc(handler, target);
            try
            {
                rpc.StartListening();

                return rpc;
            }
            catch
            {
                rpc.Dispose();
                throw;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string pipename = "winiotestpipe";

            var ts = Task.Run(() => new Client().Start(pipename));
            var tc = Task.Run(() => new Server().Start(pipename));

            Task.WhenAll(tc, ts).Wait();

            Console.ReadLine();
        }
    }
}