【読書感想ブログ】13歳から分かる!プロフェッショナルの条件

はじめに

この本↓のわかりやすい版の本と間違って購入した。なので、期待した内容ではなかったが学びはあったので読んでよかったと思う。

www.amazon.co.jp

この本は「成果」を挙げることで人生をより豊かにするにはどうすればいいか?ということに対しての一つの道しるべを与えてくれる。そんな本になっています。

どういう人が読むべきか

「人一倍頑張っているのに思うような結果が出ない」 「会社では高く評価されているのに充実感が得られない」 といった人に読んでもらいたい本です

どういう本か

この本の「はじめに」には以下のように書いてあります

本書は、ドラッカーの数ある著作の中でも、成果を上げ人生を豊かにするための方法がまとめられた『プロフェッショナルの条件』をベースにし、ほかの著書の要素もおりまぜながら、13歳から分かるようにまとめた入門書です。

上記の通り、難しい内容をかみ砕いで説明してくれているので、大変読みやすい本でした。あるレストランを任された店長がなかなか売り上げが上がらず困っているところにある老紳士があらわれ「プロフェッショナルの条件」を教えるというストーリーを交えて、条件の一つ一つを丁寧にかつ分かりやすく説明してくれています。

プロフェッショナルの条件には以下の項目があります。

  1. <貢献> を考える
  2. <強み> を生かす
  3. <時間> をコントロールする
  4. 一番重要なことに <集中> する
  5. 正しく <意思決定> し、実行する

1. <貢献> を考える

組織が社会に対してどのように貢献できるか、自分が組織にどのように貢献できるかを考える必要があるとのこと。自分よがりに仕事をしていてもうまくいかないし、組織が求めていないことに力を入れても空回りするだけなので、ちゃんと貢献しているかを頭に入れて仕事をしないと結果が出ないよ。ということなのかなと思いました。そして、個人が貢献するにはどうしたらいいかの指針として、組織が考えていることは基本「直接的な成果をだす」「組織の価値を高める」「人材を育てる」の3つなので、それに沿った貢献を考えることがいいのではないか?とも書いてありました。確かに会社ではよく、売り上げアップや部下教育が叫ばれている気がします。また、貢献を考えることで人間関係も円滑に行くことがあるとのことです。たしかに、組織に反発した行動はあまりよくないというイメージがあるので、いい意味で組織の歯車として仕事をするということはよいことなのかもしれませんね。

2. <強み> を生かす

大きな成果は弱みからは生まれません。強味を見つけ活かすことで初めて、成果を上げることができます。 自分の強みだけではなく、人の強みを生かすことも大切です。特別な才能を持った人を集めるのではなく、普通の人がそれぞれに強みを活かせるようにしましょう

特に日本人は欠点を補う傾向にあるような気がしていますので、強みを伸ばすということはなかなか苦手なところがあるかもしれませんが、今後は自分の強みが何かを探って伸ばしていけたらと思いました。自分の強みが見つからないという人のためにこの本では「フィードバック分析」というのを紹介していました。

以下の4ステップを繰り返すのがフィードバック分析というものらしいです。

STEP1. 目標を決める

STEP2. 実行する

STEP3. 目標と結果を比較する

STEP4. 次の目標を決める

※ STEP3で「目標よりもうまくやれた」「一生懸命取り組めた」という点が自分の強みに当てはまるポイントだということらしいです。

3. <時間> をコントロールする

ムダな時間を排除してうまく使うことで成果を最大化しよう。ということらしいです。基本的に割り込みが多ければ多いほど能率が下がるので、まとまった時間を取って作業をすることで効率化を行いましょうという内容。

時間だけが唯一人類に平等に与えられた資源。というワードはすごいインパクトがあった。

4. 一番重要なことに <集中> する

タスクは優先順位をつけて一番優先順位の高いものから手を付けていきましょうという内容。また、何をやらないのか。ということもリストにすると、逆説的に一番やらなければならないことが見えてきていいよ。ということも書いてありました。

このあたりは仕事をしているとよく言われることなのですんなり理解できましたが、プライベートに対しても同じようにできているかというとなかなか難しいなぁと思っています。そのあたりにも少し触れられていて

いますぐやるべき「一番重要なこと」を決めるには勇気が必要です。未来、チャンス、独自性、新しい道。これらを意識して、勇気を出して判断しましょう。

僕に今必要なものは「勇気」そんな気持ちになりました。

5. 正しく <意思決定> し、実行する

何かを意思決定するときは、5つのステップを踏むことで意思決定の制度上げることができます

5つのステップとは以下の項目になります

STEP1. 問題を分類する

STEP2. 必要条件をはっきりさせる

STEP3. 「なにが正しいのか」を決める

STEP4. 決定したことを行動に起こす

STEP5. フィードバック分析する

おそらくこのステップのうちで一番重要なのは STEP1 の問題を分類するじゃないかなと思います。この本では4つのカテゴリに分類しています。

①一般的な問題

②自分にとっては例外的だが、世の中にはよくある一般的な問題

③本当に例外的な問題

④いまは例外的でも、将来一般的になるかもしれない問題

①、②は解決策がすでに存在していて、本当に取り組まなければならない問題は③、④になります。③は一度きりの例外的なものですが、④についてはその問題を解決することでそれが仕事になる可能性を含んでいることを示唆していて、④が一番取り組むべき問題ということになります。また、②については例外的な問題であると勘違いしてしまう可能性があり、慎重に分類する必要があることも注意が必要です。

また、STEP5でフィードバック分析することで再度STEP1に立ち返り、意思決定のループを作り出し常に意思決定し続けることでより良い意思決定を続けていくということが重要になってくるのではないかとも思いました。

まとめ

「13歳から分かる!プロフェッショナルの条件」読んだ感想を書きました。はじめは勘違いから購入した本でしたが、読んでみると今の私にも刺さる内容だったので読んでよかったと思いました。仕事だけではなく、人生においても同じような考え方でプロセスを回すことでより良い人生が歩めるようになるのかもしれないなとも思ったので、できるだけ実践していこうと思います。

【読書感想ブログ】 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