2020/04/30

PlantUMLでクラス図作成

背景


システムの詳細設計でクラス図を作成する必要があり、Gitで差分管理が可能なクラス図作成ツールを調査した。

記事の目的


PlantUMLでクラス図を作成する

クラス図


クラス図について記載する。

クラス図とは

クラス図は、システムの静的な構造・関係性を表現する為の図である。つまり、クラスはオブジェクトの型を表現している。

クラス図の描き方

  • オブジェクトの種類
  • @startuml
    class Class
    interface Interface
    abstract Abstract
    enum Enum
    @enduml
    
    オブジェクトの種類には、クラス(Class)、インタフェース(Interface)、抽象クラス(Abstract)、列挙型(Enum)がある。
    • クラス
    • オブジェクトを生成するための設計図あるいはひな形に相当するもの
    • インタフェース
    • クラスに含まれるメソッドの具体的な処理内容を記述せず、変数とメソッドの型のみを定義したもの
    • 抽象クラス
    • 具体的な処理内容を記述せず、メソッド名や引数などの定義だけを宣言したもの
    • 列挙型
    • 複数の変数をまとめたもの
  • 可視性
  • @startuml
    skinparam classAttributeIconSize 0
    class ClassA{
     - field1: str
     # field2: int
     ~ method1(int a): void
     + method2(): int
    }
    @enduml
    
    可視性は、「属性」や「操作」に対してアクセスが可能となる範囲を表現するものである。
    • +: すべてのクラスからアクセスが可能
    • ー: 自クラスからのみアクセスが可能
    • #: 自クラスおよび継承されているクラスからのみアクセスが可能
    • ~: 同一パッケージ内のクラスからのみアクセスが可能
  • 関係を示す線
  • @startuml
    A --> B: 関連 
    C ..> D: 依存
    E o-- F: 集約
    G *-- H: 合成
    I <|-- J: 汎化
    interface K
    K <|.. L: 実現
    @enduml
    オブジェクト間の関係には、関連、依存、集約、合成、汎化、実現があり、線で表現される。
    • -->:  関連 (Association)
    • 関連は、複数のクラス間における関係を表現したものである。
    • ..>:  依存 (Dependency)
    • 依存は、クラス間で関係性はあるが、あまり強くない関係性の場合に使用する。
    • o--:  集約 (Aggregation)
    • 集約は、関連のあるクラス同士が、「全体」と「部分」の関係にある場合に使用する。
    • *--:  合成 (Composition)
    • 合成は、「全体」インスタンスが「部分」インスタンスの生成や削除を担っている場合に使用する。
    • <|--: 汎化 (Extension)
    • 汎化は、クラスAの型を継承し、具体化したクラスBがあるときのAとBの関係性を指す。
    • <|..: 実現 (Realization)
    • 実現は、インタフェースとサブクラスの関係を指す。
  • 多重度と関連のラベル
  • @startuml
    BookShelf "1" o-- "*" Book : contains
    @enduml
    多重度は、関連のあるクラス間において、クラスAから見たクラスBの存在しうる数を表したものである。
    • "*": 任意
    • "n": 値の通り(n)
    • "0..n" または "*": 0以上
    • "1..*": 1以上
    • "m..n": mからnまで

まとめ


  • PlantUMLでクラス図を作成する方法を調査、記載した

参考文献



変更履歴


  1. 2020/04/30: 新規作成

2020/04/18

GitHubでAuthentication failedが出る場合の対処法

背景


仕事でGitHubAzure DevOpsを使用する際、git pullやgit clone時にAuthentication failedが出たため、対処方法を調査した。

記事の目的


GitHubAzure DevOpsにおける原因不明のAuthentication failedを解決する

Authentication failed


現象

GitHubAzure DevOpsでgit pullもしくはgit cloneする際、下記のようなエラーが出る場合がある
$ git clone https://github.com/xxx/yyy.git
...
fatal: Authentication failed for 'https://github.com/xxx/yyy.git'

原因

原因として、下記が考えられる。
  • 2FA(2段階認証)に切り替えた
  • トークンが設定されていないため、認証できない。
  • GitHubのレポジトリやAzureDevOpsでアクティブディレクトリを切り替えた
  • ローカルのキャッシュに前の認証情報が残ってしまっているため、認証できない。

対処方法

  • 2FA(2段階認証)に切り替えた場合
    1. 以下のページを参考にし、トークンを設定する
    2. git clone/pull時に、ユーザーIDとトークンを入力してログインする
  • GitHubのレポジトリやAzureDevOpsでアクティブディレクトリを切り替えた場合
    • Windowsの場合
      1. C:\ユーザー\ユーザー名\AppData\Local\GitCredentialManager\tenant.cashを削除
      2. コントロールパネル→ユーザーアカウント→資格情報マネージャー→Windows資格情報→git:https://github.com/xxx/yyy.gitを削除
    • Macの場合
      1. キーチェーンアクセスアプリケーションを開く
      2. github.com の「internet password」エントリを削除
    • Ubuntuの場合
      1. 認証情報を削除
      2. $ git config --global credential.helper erase
      3. 認証情報を再保存
      4. $ $ git config --global credential.helper store

まとめ


  • git cloneやgit pull時のAuthentication failedエラーを解決する方法について調査、記載した


参考文献




変更履歴


  1. 2020/04/18: 新規作成

2020/04/16

Node.jsの導入方法

背景


仕事で、サーバーアプリを開発する必要があり、Node.jsを採用した。

記事の目的


LinuxでNode.jsを導入する

Node.jsの導入


ここでは、Node.jsの導入方法と使用方法について記載する。

node.jsとは


Node.jsは、バイナリにコンパイルされた多くの「コア・モジュール」とともに提供される。それはネットワークの非同期ラッパーであるnetモジュールの他、パスやファイルシステム、バッファ、タイマー、より一般的なストリームなどの基本的なモジュールを含む。サードパーティー製のモジュールを使用することも可能である。それはプリコンパイルされた ".node" アドオン、または、プレーンなJavaScriptファイルのどちらの形式でもよい。JavaScriptモジュールはCommonJSモジュール仕様に従って実装され、モジュールが実装する関数や変数へのアクセスにはexports変数が使われる。
サードパーティーのモジュールはNode.jsを拡張または抽象レベルを提供することで、ウェブアプリケーションで使われる様々なミドルウェア実装することができる。たとえばポピュラーなフレームワークとしてconnectおよびExpress.jsがある。モジュールは単なるファイルとしてインストールすることもできるが、通常はnpmを使ってインストールされる。それは依存性の扱いも含めてモジュールの構築、インストール、更新を助けてくれる。さらに、モジュールはNodeのデフォルトであるモジュール用ディレクトリにインストールしなくても、相対的なパス名を要求することで見つけられる。Node.js wikiに利用可能なサードパーティー製のモジュール一覧がある。

利点

  • Node.jsはシングルスレッドで動作するため、接続数が増えてもスレッドがメモリを食い潰さない
  • ノンブロッキングI/O採用の為、待ち時間が少なく大量の処理を行うことができる
  • Javascriptで実装できるため、開発が容易である


導入方法

node.jsの導入手順は、下記の通りである。
  • Ubuntuの場合
    1. node.js、npm(node.jsのパッケージ管理モジュール;Pythonのpip相当)をインストールする
    2. $ sudo apt install -y nodejs npm
    3. npmでn(node.jsのバージョン管理モジュール;Pythonのpyenv相当)をインストールする
    4. $ sudo npm install n -g
    5. nで最新のnodeをインストールする(apt installで入れたnodeはバージョンが古いため)
    6. $ sudo n stable
    7. apt install で入れたnodejs、npmを削除する
    8. $ sudo apt purge -y nodejs npm
      $ sudo apt autoremove
    9. nodeのバージョン確認
    10. $ node -v
  • Windowsの場合
    • 直接インストール
      1. 公式ホームページから、使用するバージョンのインストーラをダウンロードする
      2. ダウンロードしたインストーラを使用し、インストールする
    • nvm-windowsを使用(バージョン管理)
      1. GitHubから、nvm-setup.zip をダウンロードする
      2. 解凍し、nvm-setup.exe(インストーラ)を実行する
      3. コマンドプロンプト(Win+R→「cmd」)で、動作確認を行う
      4. $ nvm version
      5. nvm-windowsでNode.jsをインストールする
      6. $ nvm list available
        $ nvm install 6.9.1
        # 32ビットの場合
        $ nvm install 6.9.1 32
      7. nvm-windowsでNode.jsをアンインストールする
      8. $ nvm uninstall 6.9.1
      9. nvm-windowsでNode.jsを切り替える
      10. $ nvm use 6.9.1

まとめ


  • Node.jsの導入方法について調査、記載した

参考文献




変更履歴


  1. 2020/04/16: 新規作成

2020/04/14

Dockerのコンテナとホスト間でメモリ共有

背景


仕事で、複数のエッジコンピュータにデプロイを行うにあたり、Dockerイメージを利用する必要があった。
複数コンテナ間でプロセス間通信を行うにあたり、メモリ共有を行う必要があった。

記事の目的


Dockerでメモリ共有を行う

メモリ共有コマンドについて


ここでは、Dockerでメモリ共有する方法について記載する。

ホストとコンテナ間で共有

コンテナとホスト間でメモリ共有する手順は、下記の通りである。
  1. --ipcオプション付きでdocker run を実行
  2. $ sudo docker run --ipc="host" nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 
動作確認
  1. /run/shm(メモリ)にデータを保存
  2. $ mkdir /run/shm/test
  3. オプションなしでdocker run を実行
  4. $ sudo docker run nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 ls /run/shm
    
  5. --ipcオプション付きでdocker run を実行
  6. $ sudo docker run --ipc="host" nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 ls /run/shm
    

コンテナ間で共有

コンテナ間でメモリ共有する手順は、下記の通りである。
  1. --ipcオプション付きでdocker run を実行
  2. $ sudo docker run --ipc="container:<名前|id>" nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 


まとめ


  • Dockerでメモリ共有する方法について調査、記載した


参考文献




変更履歴


  1. 2020/04/14: 新規作成

2020/04/11

DockerでGPUを使用する

背景


仕事で、複数のエッジコンピュータにデプロイを行うにあたり、Dockerイメージを利用する必要があった


記事の目的


DockerでGPUを使用する


DockerでGPU


ここでは、DockerでGPUを使用する方法について記載する。


DockerでGPUを使用する手順

DockerでGPUを使用する手順は、下記の通りである。
  1. CUDAイメージをpull
  2. $ sudo docker pull nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04
  3. GPUオプション(--gpus all)付きで実行
  4. $ sudo docker run --gpus all nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 nvidia-smi
    +-----------------------------------------------------------------------------+
    | NVIDIA-SMI 430.26       Driver Version: 430.26       CUDA Version: 10.2     |
    |-------------------------------+----------------------+----------------------+
    | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
    | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
    |===============================+======================+======================|
    |   0  GeForce GTX 108...  Off  | 00000000:17:00.0 Off |                  N/A |
    | 23%   30C    P8     9W / 250W |      2MiB /  8192MiB |      0%      Default |
    +-------------------------------+----------------------+----------------------+
    


備考

  • エラーが出る場合
  • 実行時に下記のようなエラーが出る場合がある。
    $ sudo docker run --gpus all nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 nvidia-smi
    docker: Error response from daemon: linux runtime spec devices: could not select device driver "" with capabilities: [[gpu]].
    ERROR[0000] error waiting for container: context canceled
    対処法
    1. 対策スクリプトの作成
    2. $ nano ./nvidia-container-runtime-script.sh
      nvidia-container-runtime-script.sh
      curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \
      sudo apt-key add -
        distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
      curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \
        sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list
      sudo apt-get update
      sudo apt-get install nvidia-container-runtime
    3. 対策スクリプトの実行
    4. $ chmod 777 ./nvidia-container-runtime-script.sh
      $ sudo ../nvidia-container-runtime-script.sh
    5. dockerサービスの再起動
    6. $ sudo systemctl restart docker
    7. 動作テスト
    8. $ sudo docker run --gpus all nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 nvidia-smi


まとめ


  • DockerでGPUを使用する方法について調査、記載した


参考文献




変更履歴


  1. 2020/04/11: 新規作成

2020/04/08

Dockerfileの作成

背景


仕事で、複数のエッジコンピュータにデプロイを行うにあたり、Dockerイメージを利用する必要があった

記事の目的


DockerfileでDockerコンテナを自動ビルドする

Dockerfileの導入


ここでは、Dockerfileの使用方法について記載する。

Dockerfile作成手順

Dockerfileの作成手順は、下記の通りである。
  1. Dockerfileを作成
  2. $ vim Dockerfile
    Dockerfile
    FROM nvidia/cuda
    MAINTAINER emptySet
    # ENV: 環境変数の設定
    ENV http_proxy http://your.proxy.com:8080
    ENV https_proxy http://your.proxy.com:8080
    # RUN: buildするときに実行されるもの
    RUN apt update
    # ADD: ホスト側のファイル(tar)をコンテナに移動し、展開する
    ADD sample.tar.gz /home/
    # COPY: ホスト側のファイルをコンテナにコピーする
    COPY sample.sh /home/
    # ENTRYPOINT: コンテナ実行時に最初に実行される
    ENTRYPOINT: /bin/bash
    # CMD: runするときに実行されるもの
    CMD ["echo", "now running..."]]
    
  3. bildを実行(カレントディレクトリのDockerfileを使用)
  4. $ sudo docker build -t emptySet/sampleImage .
    
  5. 動作確認
  6. $ sudo docker run -i -t emptySet/sampleImage
    ...
    now running...
    ...

備考

  • Dockerfikeのコマンド一覧
    • FROM
    • ベースイメージを指定する(必須)
    • MAINTAINER
    • イメージの作成者を記述できる
    • RUN
    • コマンドを実行する
    • CMD
    • docker run時に実行されるコマンド
    • EXPOSE
    • 外部に公開するポートを指定する
    • ENV
    • 環境変数を設定する
    • ADD
    • 指定したファイル・ディレクトリをコンテナ内にコピーする URLの指定が可能。tarファイルは自動解凍する。
    • COPY
    • 指定したファイル・ディレクトリをコンテナ内にコピーする URLの指定不可。自動解凍もしない。
    • ENTRYPOINT
    • docker run時に最初に実行されるコマンド
    • VOLUME
    • 外部からマウント可能なボリュームを指定する データの永続化が可能となる
    • USER
    • コンテナで使用されるユーザ名/UIDを設定する
    • WORKDIR
    • 作業ディレクトリを変更する
    • ONBUILD
    • 次のビルドの最後に実行されるコマンドを設定する

まとめ


  • Dockerfileの作成方法について調査、記載した

参考文献



変更履歴


  1. 2020/04/08: 新規作成

2020/04/06

Dockerの導入方法

背景


仕事で、複数のエッジコンピュータにデプロイを行うにあたり、Dockerイメージを利用する必要があった

記事の目的


UbuntuにDockerを導入する

Dockerの導入


ここでは、Dockerの導入方法と使用方法について記載する。

Dockerとは


Dockerはコンテナ仮想化を用いてアプリケーションを開発・配置・実行するためのオープンソースソフトウェアあるいはオープンプラットフォームである。 Dockerはコンテナ仮想化を用いたOSレベルの仮想化(英語版)によりアプリケーションを開発・実行環境から隔離し、アプリケーションの素早い提供を可能にする。かつその環境自体をアプリケーションと同じようにコード(イメージ)として管理可能にする。Dockerを開発・テスト・デプロイに用いることで「コードを書く」と「コードが製品として実行される」間の時間的ギャップを大きく短縮できる。

利点

  • コンテナに再現性があり、どの環境下でも同じ動作をする
  • コンテナイメージはリポジトリ管理できるため、ロールバックが容易
  • イメージ内で環境が出来上がっているため、環境構築が容易になる


導入方法

Dockerの導入手順は、下記の通りである。ここを参照
  1. Dockerをインストールする(Ubuntuの場合)
  2. $ sudo apt update
    $ sudo apt install \
        apt-transport-https \
        ca-certificates \
        curl \
        gnupg-agent \
        software-properties-common
    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    $ sudo apt-key fingerprint 0EBFCD88
    $ sudo add-apt-repository \
       "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
       $(lsb_release -cs) \
       stable"
    $ sudo apt update
    $ sudo apt install docker-ce docker-ce-cli containerd.io
    
  3. プロキシ設定(オプション)
  4. $ sudo mkdir -p /etc/systemd/system/docker.service.d/
    $ sudo nano /etc/systemd/system/docker.service.d/10_docker_proxy.conf
    
    10_docker_proxy.conf
    [service]
    Environment=HTTP_PROXY=http://your.proxy.com:8080
    Environment=HTTPS_PROXY=http://your.proxy.com:8080
    
    $ sudo systemctl daemon-reload
    $ sudo systemctl restart docker
    
  5. 動作確認
  6. $ sudo docker run hello-world
    ...
    Hello from docker
    ...

使用方法

Dockerの使用手順は、下記の通りである。
  1. DockerイメージをDocker Hubで探す
  2. 以下のコマンドでも検索可能(nvidiaを検索)
    $ sudo docker search nvidia | more
  3. Dockerイメージをpullする
  4. $ sudo docker pull nvidia/cuda
  5. Dockerイメージを確認する
  6. $ sudo docker images
  7. Dockerイメージの詳細を確認する
  8. $ sudo docker inspect nvidia/cuda
  9. Dockerイメージを起動する
  10. $ sudo docker run nvidia/cuda:latest echo "Hello World"
    $ sudo docker run nvidia/cuda:latest -d echo "Hello World" #バックグラウンド実行
    $ sudo docker attach --sig-proxy=false id(コンテナID) #フォアグラウンド実行に変更
  11. 実行中のコンテナを確認する
  12. $ sudo docker ps
  13. 実行中のコンテナ一時中断、再開
  14. $ sudo docker kill id(コンテナID) #中断
    $ sudo docker start id(コンテナID) #再開
  15. 実行中のコンテナを削除する
  16. $ sudo docker rm id(コンテナID)
  17. コンテナの中で作業する
  18. $ sudo docker run -i -t nvidia/cuda /bin/bash
  19. コンテナからイメージを作成する
  20. $ sudo docker run -i -t nvidia/cuda /bin/bash #コンテナに入る
    $ root@7432974# 作業をする
    ...
    $ root@7432974# exit
    $ sudo docker ps -a #先ほどのコンテナを探す
    $ sudo docker commit id(コンテナID) emptySet/sample
  21. Dockerイメージを削除する
  22. $ sudo docker rmi nvidia/cuda

アンインストール方法

Dockerのアンインストール手順は、下記の通りである。
  1. docker docker-engine containerd.ioを削除する
  2. $ sudo apt remove docker docker-engine containerd.io
  3. イメージ・コンテナを削除する
  4. # Docker CEを削除
    $ sudo apt-get purge docker-ce docker-ce-cli
    # dockerイメージやコンテナも削除
    sudo rm -rf /var/lib/docker
    # 不要なパッケージの削除
    sudo apt autoremove
    

備考


  • Dockerコマンド一覧
  • Dockerコマンド
    # コンテナ作成
    run image - コンテナを作成する(起動状態で)
    create image - コンテナを作成する(停止状態で)
    
    # コンテナ一覧
    ps - コンテナの一覧を表示する
    stats - コンテナのリソース使用状況一覧を表示する
    
    # コンテナ操作(1)
    rm container - コンテナを削除する
    start container - コンテナを開始する
    stop container - コンテナを停止する
    kill container - コンテナを強制停止する
    restart container - コンテナを再起動する
    pause container - コンテナ上のプロセスを一時停止する
    unpause container - コンテナ上のプロセスを再開始する
    
    # コンテナ操作(2)
    exec container - コンテナ内でプロセスを起動する
    attach container - コンテナに標準入出力をアタッチする
    
    # コンテナ操作(3)
    cp srcfile dstfile - コンテナに(から)ファイルをコピーする
    rename container newname - コンテナ名を変更する
    update container - コンテナの設定(CPU数等)を変更する
    
    # コンテナ詳細
    logs container - コンテナのログを表示する
    port container - コンテナのポートマッピングを表示する
    top container - コンテナ内のプロセスの一覧を表示する
    
    # Dockerレジストリ関連
    pull name - レジストリからイメージをダウンロードする
    push name - レジストリにイメージをアップロードする
    search term - Dockerレジストリからイメージを検索する
    login - Dockerレジストリにログインする
    logout - Dockerレジストリからログアウトする
    
    # イメージ管理
    images - イメージの一覧を表示する
    rmi images - イメージを削除する
    history image - イメージのヒストリを表示する
    commit container - コンテナからイメージを作成する
    tag image NEWimage - イメージにタグをつける
    build dockerfile - イメージをビルドする
    trust - イメージに署名する
    
    # ボリューム管理
    volume - Dockerボリュームを管理する
    
    # ネットワーク管理
    network - Dockerネットワークを管理する
    
    # インポート/エクスポート/セーブ/ロード
    export container - コンテナをファイルにエクスポートする
    import file - エクスポートファイルをイメージとしてインポートする
    save image - イメージをファイルにセーブする
    load file - セーブファイルをイメージとしてロードする
    
    # Docker Swarm(クラスタリング)関連
    swarm - Swarmを管理する
    node - Swarmノードを管理する
    stack - Swarmスタックを管理する
    secret - Swarmシークレットを管理する
    service - Swarmサービスを管理する
    
    # その他
    version - バージョンを表示する
    help - ヘルプを表示する
    info - Dockerに関するシステム情報を表示する
    inspect - 様々なDockerオブジェクトの詳細情報を表示する
    diff container - コンテナ生成後の更新ファイルを表示する
    wait container - コンテナの停止を待ち合わせる
    events - Dockerエンジンのイベントを監視・表示する
    image - イメージ管理系コマンドを実行する
    container - コンテナ管理系コマンドを実行する
    builder - イメージビルド系コマンドを実行する
    system - システム管理系コマンドを実行する
    config - コンフィグを管理する
    context - ビルド時のコンテキストを管理する
    engine - Dockerエンジンを管理する
    plugin - プラグインを管理する
    オプション(OPTIONS)
    --config string
    -c, --context string
    -D, --debug
    -H, --host list
    -l, --log-level string
    -v, --version
    --tls
    --tlscacert string
    --tlscert string
    --tlskey string
    --tlsverify
  • コンテナの一括削除コマンド
  • $ sudo docker rm $(sudo docker ps -aq)
  • イメージの一括削除コマンド
  • $ sudo docker rmi $(sudo docker images -aq)


まとめ


  • Dockerの導入方法について調査、記載した


参考文献




変更履歴


  1. 2020/04/08: 新規作成
  2. 2020/05/04: アンインストール方法追記
  3. 2020/05/13: Dockerイメージの一括削除方法追記

2020/04/05

Typescriptでsocketio

背景


仕事でTypescriptを使用する機会が出てきたので、使い方を調査した

記事の目的


TypescriptでSocketIO通信を実装する

Typescript


ここでは、Typescriptについて記載する。

Typescriptとは

Typescript

Typescriptはマイクロソフトによって開発され、メンテナンスされているフリーでオープンソースのプログラミング言語である。TypeScriptはJavaScriptに対して、省略も可能な静的型付けとクラスベースオブジェクト指向を加えた厳密なスーパーセットとなっている。C#のリードアーキテクトであり、DelphiとTurbo Pascalの開発者でもあるアンダース・ヘルスバーグがTypeScriptの開発に関わっている。TypeScriptはクライアントサイド、あるいはサーバサイド (Node.js) で実行されるJavaScriptアプリケーションの開発に利用できる。

利点

  • 型チェックが強力で柔軟であるため、型の間違いによるバグを抑制できる
  • VSCodeなどで入力補完が利用でき、JSDocでコメントを作成しておくとドキュメントが不要

テンプレートのパッケージ構成

Typescriptでnodeモジュールを作成するための最小構成
$ tree
.
├── dist                   #トランスパイル済みJavascriptファイルの格納先
├── node_modules           # npm installでインストールした依存関係のあるnodeモジュールの格納先
├── package-lock.json      # npm installでインストールしたnodeモジュール情報(自動で生成される)
├── package.json           # このモジュールの情報
├── param                  # パラメータファイルの格納先(オプショナル)
│   └── log-config.json   # log4js(ロガーモジュール)の設定ファイル
├── src                    # ソースファイルの格納場所
│   └── app.ts            # mainのTypescriptファイル
├── test                   # テストコードの格納場所
│   └── app.test.ts       # app.tsのテストコード
├── tsconfig.json          # Typescriptの設定ファイル
└── tslint.json            # ts-lint(Typescriptのlintツール)の設定ファイル

package.json

テンプレートのpackage.jsonファイルを記載する。
{
  "name": "sample_app",(mainのファイル名)
  "version": "1.0.0",(バージョン)
  "description": "This is sample code",(モジュールの説明)
  "main": "dist/app.js",(mainファイル)
  "directories": {(ディレクトリ構成)
    "src": "src",
    "dist": "dist",
    "param": "param",
    "test": "test"
  },
  "scripts": {(スクリプトの設定)
    "start": "npm run build && npm run watch", (ビルドして実行)
    "build": "npm install && npm run build-ts && npm run tslint",(ビルド)
    "serve": "nodemon dist/src/index.js",(サービスとして実行)
    "watch": "npm install concurrently && concurrently -k -p \"[{name}]\" -n \"Typescript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run serve\"",(ビルドしつつウォッチ)
    "test": "npm run build && jest --forceExit --detectOpenHandles",(テストコード実行)
    "test-ci": "jest --coverage --forceExit --runInBand",(テストコード実行し、コードカバレッジを表示)
    "build-ts": "tsc",(Typescriptのビルド)
    "watch-ts": "tsc -w",(Typescriptのウォッチ)
    "tslint": "tslint -c tslint.json -p tsconfig.json"(ts-lintを実行)
  },
  "author": "EmptySet",(作成者)
  "license": "MIT",(ライセンス)
  "devDependencies": {(依存モジュール、バージョン。npm install -Dでインストールされる)
    "@babel/core": "*",
    "@babel/preset-env": "*",
    "@babel/preset-react": "*",
    "@babel/preset-typescript": "*",
    "@types/jest": "*",
    "@types/uuid": "^7.0.2",
    "async-mutex": "^0.1.4",
    "babel-jest": "*",
    "jest": "^25.2.6",
    "log4js": "^6.1.2",
    "socket.io": "^2.3.0",
    "socket.io-client": "^2.3.0",
    "ts-jest": "^25.3.0",
    "tslint": "6.1.0",
    "typescript": "3.8.3"
  },
  "dependencies": {(依存モジュール、バージョン。npm installでインストールされる)
    "@types/express": "^4.17.4",
    "@types/jest": "^25.1.5",
    "@types/node": "^13.9.5",
    "@types/socket.io": "^2.1.4",
    "@types/socket.io-client": "^1.4.32",
    "express": "^4.17.1",
    "jest-cli": "^25.2.6",
    "log4js": "6.1.2",
    "tsc": "^1.20150623.0",
    "uuid": "^7.0.2"
  },
  "jest": {(jest(テストツール)の設定)
    "transform": {
      "^.+\\.ts$": "ts-jest"
    },
    "testMatch": [
      "**/test/**/*.test.ts"
    ],
    "moduleFileExtensions": [
      "ts",
      "js"
    ],
    "globals": {
      "ts-jest": {
        "tsConfig": "tsconfig.json"
      }
    }
  }
}

tsconfig.json

tsconfig.jsonファイルを記載する。
{
    "compilerOptions": {
        "module": "commonjs",(モジュールコードのバージョン)
        "target": "es2017",(ECMAScriptのバージョン)
        "noImplicitAny": true,(Any型を許容しない)
        "moduleResolution": "node",(構造の単位)
        "sourceMap": true,(tsファイルとjsファイル間のマップファイルを作成)
        "outDir": "./dist",(ビルド後のファイルの格納先)
        "baseUrl": ".",(ソースファイルのベースのパス)
        "rootDir":"./src",(ルートディレクトリ)
        "paths": {(パスの定義)
            "*": [
                "node_modules/*",
                "src/*",
                "test/*"
            ]
        }
    },
    "include": [(includeするファイル)
        "src/**/*"
    ]
}

tslint.json

tslint.jsonファイルを記載する。
{
    "rules": {
        "class-name": true,
        "comment-format": [
            true,
            "check-space"
        ],
        "indent": [
            true,
            "spaces"
        ],
        "one-line": [
            true,
            "check-open-brace",
            "check-whitespace"
        ],
        "no-var-keyword": true,
        "quotemark": [
            true,
            "double",
            "avoid-escape"
        ],
        "semicolon": [
            true,
            "always",
            "ignore-bound-class-methods"
        ],
        "whitespace":[
            true,
            "check-branch",
            "check-decl",
            "check-operator",
            "check-module",
            "check-separator",
            "check-type"
        ],
        "typedef-whitespace": [
            true,
            {
                "call-signature": "nospace",
                "index-signature": "nospace",
                "parameter": "nospace",
                "property-declaration": "nospace",
                "variable-declaration": "nospace"
            },
            {
                "call-signature": "onespace",
                "index-signature": "onespace",
                "parameter": "onespace",
                "property-declaration": "onespace",
                "variable-declaration": "onespace"
            }
        ],
        "no-internal-module": true,
        "no-trailing-whitespace": true,
        "no-null-keyword": true,
        "prefer-const": true,
        "jsdoc-format": true
    }
}

/src/app.ts

app.tsを記載する。
/**
 * SocketIO sample
 * @summary This is socketIO sample code (get rtt)
 * @author EmptySet
 * @version 1.0
 * @todo Something
 */

// ロガーモジュール
import * as Logger from "log4js";
Logger.configure("./param/log-config.json");
// SocketIOのモジュール
import * as socketio_client from "socket.io-client";
import * as socketio from "socket.io";
// UUID作成モジュール
import * as UUID from "uuid";
// Expressモジュール(サーバアプリ)
import * as express from "express";
// HTTPサーバモジュール
import * as http from "http";

// データの型を設定
/**
 * Header in data
 */
interface Header {
    "type": string;
    "uuid": string;
    "version": number;
    "time_stamp": string;
}
/**
 * Body in data
 */
interface Body {
    "time_stamp": Array<number>;
}

/**
 * Data format
 */
export interface Data {
    "header": Header;
    "body": Body;
}

/**
 * SocketIOクライアントクラス
 */
class Client {
    // ロガー
    protected logger: Logger.Logger;
    // SocketIOクライアント
    protected io: SocketIOClient.Socket;
    // SocketIOのパラメータ
    protected url: string;
    protected port: number;
    protected token: string;
    protected namespace: string;
    // 接続状態フラグ(true: 接続, false: 未接続)
    protected is_connection: boolean;

    // コンストラクタ
    /**
     * Set parameters of socketIO
     * @param url Server URL (e.g. azure.com)
     * @param port Port number (e.g. 80)
     * @param token Token in query
     * @param namespace Socket namespace
     */
    constructor(url: string, port: number, token: string, namespace: string) {
        // ロガー名を設定
        this.set_logger("client");
        // クライアントのパラメータを設定
        this.url   = url;
        this.port  = port;
        this.token = token;
        this.namespace = namespace;
        // 接続状態を初期化
        this.is_connection = false;
    }

    /**
     * Check connection status
     * @param class_name Please set class name
     */
    set_logger(class_name: string) {
        this.logger = Logger.getLogger(class_name);
    }

    /**
     * Check connection status
     * @return Connect(true) or not(false)
     */
    protected check_connection(): boolean {
        return this.is_connection;
    }

    /**
     * Set connection status
     * @param flg Connect(true) or not(false)
     */
    protected set_connection(flg: boolean) {
        this.is_connection = flg;
    }

    /**
     * Connect to server
     */
    protected connect() {
        this.logger.info("Try connect to server");
        // 接続重複時に、前の接続を切断
        if (this.check_connection()) {
            this.logger.warn("Connection is duplicated. Disconnect from old connection.");
            this.disconnect();
        }
        // サーバーに接続
        const uri: string = `http://${this.url}:${this.port}/${this.namespace}`;
        try {
            this.io = socketio_client.connect(uri, {
                transports: ["websocket"],
                forceNew: true,
                reconnection: true,
                query: {
                    token: this.token
                }
            });
        } catch (e) {
            this.logger.error(`Cannot connect to server ${e}`);
        }
    }

    /**
     * Disconnect from server
     */
    protected disconnect() {
        this.logger.info("Disconnect from server");
        // サーバーから切断
        try {
            this.io.disconnect();
        } catch (e) {
            this.logger.error(`Cannot disconnect from server. ${e}`);
        } finally {
             this.set_connection(false);
        }
    }

    /**
     * Run client module.
     * Connect to server and set events
     */
    async run () {
        this.connect();

        this.io.on("connect", (socket: socketio.Socket) => {
            this.set_connection(true);
            this.logger.info(`Connected to server. id: ${this.io.id}`);
            // 接続したソケットに対して、コールバックイベントを設定
            this.set_event(socket);
        });
        // 切断イベントを設定
        this.io.on("disconnect", (socket: socketio.Socket) => {
            this.set_connection(false);
            this.logger.info(`Disconnected from server. id: ${this.io.id}`);
        });
    }
    /**
     * Set callback events
     * @param socket socket instance
     */
    async set_event(socket: socketio.Socket) {}
}
/**
 * SocketIO server class
 */
export class Server {
    // ロガー名を設定
    protected logger: Logger.Logger;
    // SocketIOサーバー関連
    protected server: http.Server;
    protected io: SocketIO.Server;
    protected nsp_1: SocketIO.Namespace;
    protected nsp_2: SocketIO.Namespace;
    // SocketIOサーバーのパラメータ
    protected url: string;
    protected port: number;
    protected token: string;
    protected namespace: Array<string>;
    // 接続数
    protected number_of_connection: number;

    // コンストラクタ
    /**
     * Set parameters of socketIO
     * @param url Server URL (e.g. azure.com)
     * @param port Port number (e.g. 80)
     * @param token Token in query
     * @param namespace Socket namespace
     */
    constructor(url: string, port: number, token: string, namespace: ArrayArray<string>) {
        // ロガーを設定
        this.set_logger("server");
        // パラメータを設定
        this.url   = url;
        this.port  = port;
        this.token = token;
        this.namespace = namespace;
        // 接続数を設定
        this.number_of_connection = 0;
        // サーバーを設定
        const app: express.Express = express();
        this.server = http.createServer(app);
        this.io = socketio(this.server, {
            pingInterval: 1000,
            pingTimeout: 5000
        });
    }

    /**
     * Check connection status
     * @param class_name Please set class name
     */
    protected set_logger(class_name: string) {
        this.logger = Logger.getLogger(class_name);
    }

    /**
     * Check connection status
     * @return Connect(true) or not(false)
     */
    protected check_connection(): number {
        return this.number_of_connection;
    }

    /**
     * Add number of connection
     */
    protected add_connection() {
        this.number_of_connection ++;
        this.logger.debug(`Number of connection is ${this.number_of_connection}`);
    }

    /**
     * Add number of connection
     */
    protected remove_connection() {
        if (this.number_of_connection == 0) {
            this.logger.error("Number of connection is invalid");
            return;
        }
        this.number_of_connection --;
        this.logger.debug(`Number of connection is ${this.number_of_connection}`);
    }

    protected listen() {
        this.server.listen(this.port, this.url);
    }

    /**
     * Disconnect from server
     */
    protected disconnect(socket: socketio.Socket) {
        this.logger.info("Disconnect from server");
        // 切断処理
        try {
            socket.disconnect();
            this.logger.info(`Disconnected from server. (ID: ${socket.id})`);
        } catch (e) {
            this.logger.error(`Cannot disconnect from server. ${e}`);
        }
    }

    /**
     * Run server module.
     * Listen and set events
     */
    async run () {
        this.listen();
        // 接続イベント
        this.io.on("connect", (socket: socketio.Socket) => {
            this.logger.info(`Connected to client. id: ${socket.id}`);
            this.add_connection();
        });
        // 切断イベント
        this.io.on("disconnect", (socket: socketio.Socket) => {
            this.logger.info(`disconnected from client. id: ${socket.id}`);
            this.remove_connection();
        });
        // 独自のイベントを設定
        this.set_event();
    }
    /**
     * Set callback events
     * @param socket socket instance
     */
    async set_event() {
  // Namespace: this.namespace[0]のイベント設定
        this.io.of(this.namespace[0]).on("connect", (socket: socketio.Socket) => {
            this.logger.info(`Connected to client. id: ${socket.id}`);
            // this.namespace[0]のその他イベント設定
            socket.on("ping_", (msg: Data) => {
                this.logger.info(`Get ping. msg: ${JSON.stringify(msg)}`);
                const data: Data = msg;
                // 時刻取得
                const d = new Date();
                data.header.time_stamp = d.toISOString();
                data.body.time_stamp.push(d.getTime() / 1000);
                this.io.of(this.namespace[1]).json.emit("ping_", data);
            });
        });
  // Namespace: this.namespace[1]のイベント設定
        this.io.of(this.namespace[1]).on("connect", (socket: socketio.Socket) => {
            this.logger.info(`Connected to client. id: ${socket.id}`);
            // this.namespace[1]のその他イベント設定
            socket.on("pong_", (msg: Data) => {
                this.logger.info(`Get pong. msg: ${JSON.stringify(msg)}`);
                const data: Data = msg;
                // 時刻取得
                const d = new Date();
                data.header.time_stamp = d.toISOString();
                data.body.time_stamp.push(d.getTime() / 1000);
                this.io.of(this.namespace[0]).json.emit("pong_", msg);
            });
        });
    }
}
// Ping送信クラス(Clientクラスを継承)
/**
 * Ping class
 */
export class Ping extends Client {
    private socket: socketio.Socket;
    /**
     * Emit ping
     */
    ping() {
        this.logger.info("Ping");
        // 接続状態をチェック
        if (!this.check_connection()) {
            this.logger.warn("Has not connect to server.");
            return;
        }
        // 時刻を取得
        const d = new Date();
        // 送信するデータを定義
        const data: Data = {
            header: {
                type: "PingPong",
                time_stamp: d.toISOString(),
                uuid: UUID.v4(),
                version: 0
            },
            body: {
                time_stamp: [
                    d.getTime() / 1000
                ]
            }
        };
        // データを送信
        try {
            this.io.compress(true).emit("ping_", data);
            this.logger.info("Emit ping data");
            this.logger.debug(`Ping data: ${JSON.stringify(data)}, ${this.io.nsp}`);
        } catch (e) {
            this.logger.error(`Cannot emit pong. ${e}`);
        }
    }
    // pongデータを受信し、RTTを算出
    /**
     * Get pong data and calculate RTT
     * @param msg Pong message
     */
    protected get_rtt(msg: Data): number {
        const data: Data = msg;
        // 時刻を算出
        const d = new Date();
        data.header.time_stamp = d.toISOString();
        data.body.time_stamp.push(d.getTime() / 1000);
        // RTTを算出
        const rtt: number = data.body.time_stamp[data.body.time_stamp.length - 1] - data.body.time_stamp[0];
        this.logger.info(`RTT: ${rtt} [s]`);
        return rtt;
    }
    /**
     * Set callback events
     * @param socket socket instance
     */
    async set_event (socket: socketio.Socket) {
        // ロガー名を設定
        this.socket = socket;
        this.io.on("pong_", (msg: Data) => {
            this.get_rtt(msg);
        });
    }
}
// Pong返信クラス(Clientクラスを継承)
/**
 * Pong class
 */
export class Pong extends Client {
    /**
     * Get ping message and emit pong message
     * @param msg Ping message
     */
    protected pong(msg: Data, socket: socketio.Socket) {
        this.logger.info("Pong");
        const data: Data = msg;
        // 時刻を取得
        const d = new Date();
        data.header.time_stamp = d.toISOString();
        data.body.time_stamp.push(d.getTime() / 1000);
        // Pongデータを送信
        try {
            this.io.compress(true).emit("pong_", data);
            this.logger.info("Emit pong data");
            this.logger.debug(`Pong data: ${data}`);
        } catch (e) {
            this.logger.error(`Cannot emit pong. ${e}`);
        }
    }
    /**
     * Set callback events
     * @param socket socket instance
     */
    async set_event (socket: socketio.Socket) {
        // ロガー名を設定
        this.io.on("ping_", (msg: Data) => {
            this.pong(msg, socket);
        });
    }
}

// Sleep関数
/**
 * Sleep function
 * @param milliseconds Sleep time [ms]
 */
function sleep(milliseconds: number) {
    return new Promise<void>(resolve => {
      setTimeout(() => resolve(), milliseconds);
    });
  }

// main関数(async: 非同期)
async function main() {
    const logger: Logger.Logger = Logger.getLogger("system");
    const server = new Server("localhost", 10000, "secret", ["ping", "pong"]);
    server.run();
    // await: 同期待ち
    await sleep(1000);
    const ping = new Ping("localhost", 10000, "secret", "ping");
    ping.set_logger("ping");
    ping.run();
    await sleep(1000);
    const pong = new Pong("localhost", 10000, "secret", "pong");
    pong.set_logger("pong");
    pong.run();

    await sleep(1000);
    ping.ping();
}

exports.main = main;

if (require.main == module) {
    main();
}

/test/app.test.ts

テストコードapp.test.tsを記載する。
/**
 * SocketIO sample test
 * @summary This is socketIO sample test code (get rtt)
 * @author EmptySet
 * @version 1.0
 * @todo Something
 */

// app.tsをインポート
import * as rtt from "../src/app";
// ロガーのインポート
import * as Logger from "log4js";
Logger.configure("./param/log-config.json");

const logger = Logger.getLogger("test");

describe("Ping class module test", () => {
    const ping = new rtt.Ping("localhost", 10000, "secret", "ping");
    it("Set parameters", () => {
        expect(ping["url"]).toBe("localhost");
        expect(ping["port"]).toBe(10000);
        expect(ping["token"]).toBe("secret");
        expect(ping["namespace"]).toBe("ping");
    });
    it("Check connection status function", () => {
        expect(ping["check_connection"]()).toBe(false);
        ping["set_connection"](true);
        expect(ping["check_connection"]()).toBe(true);
        ping["set_connection"](false);
        expect(ping["check_connection"]()).toBe(false);
    });
    it("Check rtt calculation function", () => {
        const d = new Date();
        const data = {
            header: {
                type: "PingPong",
                time_stamp: "2020-04-01T00:00:00.000Z",
                uuid: "test",
                version: 0
            },
            body: {
                time_stamp: [
                    d.getTime() / 1000,
                    1500000000,
                    1500000001
                ]
            }
        };
        expect(ping["get_rtt"](data)).toBeLessThanOrEqual(0.01);
    });
});

実行時

$npm run start
...
10:39:17 PM - Starting compilation in watch mode...
[Typescript] 
[Node] [nodemon] 2.0.2
[Node] [nodemon] to restart at any time, enter `rs`
[Node] [nodemon] watching dir(s): *.*
[Node] [nodemon] watching extensions: js,mjs,json
[Node] [nodemon] starting `node dist/src/index.js dist/src/app.js`
[Node] [2020-04-05T22:39:19.023] [INFO] ping - Try connect to server
[Node] [2020-04-05T22:39:19.066] [INFO] server - Connected to client. id: 9tfP7UDHqlCblBzRAAAA
[Node] [2020-04-05T22:39:19.067] [DEBUG] server - Number of connection is 1
[Node] [2020-04-05T22:39:19.076] [INFO] server - Connected to client. id: /ping#9tfP7UDHqlCblBzRAAAA
[Node] [2020-04-05T22:39:19.077] [INFO] ping - Connected to server. id: /ping#9tfP7UDHqlCblBzRAAAA
[Node] [2020-04-05T22:39:20.041] [INFO] pong - Try connect to server
[Node] [2020-04-05T22:39:20.044] [INFO] server - Connected to client. id: wTjYvamapXXZV1yDAAAB
[Node] [2020-04-05T22:39:20.045] [DEBUG] server - Number of connection is 2
[Node] [2020-04-05T22:39:20.046] [INFO] server - Connected to client. id: /pong#wTjYvamapXXZV1yDAAAB
[Node] [2020-04-05T22:39:20.047] [INFO] pong - Connected to server. id: /pong#wTjYvamapXXZV1yDAAAB
[Node] [2020-04-05T22:39:21.044] [INFO] ping - Ping
[Node] [2020-04-05T22:39:21.046] [INFO] ping - Emit ping data
[Node] [2020-04-05T22:39:21.046] [DEBUG] ping - Ping data: {"header":{"type":"PingPong","time_stamp":"2020-04-05T13:39:21.044Z","uuid":"cb6087b4-68d1-447a-b4d2-8e1271b9f735","version":0},"body":{"time_stamp":[1586093961.044]}}, /ping
[Node] [2020-04-05T22:39:21.048] [INFO] server - Get ping. msg: {"header":{"type":"PingPong","time_stamp":"2020-04-05T13:39:21.044Z","uuid":"cb6087b4-68d1-447a-b4d2-8e1271b9f735","version":0},"body":{"time_stamp":[1586093961.044]}}
[Node] [2020-04-05T22:39:21.050] [INFO] pong - Pong
[Node] [2020-04-05T22:39:21.051] [INFO] pong - Emit pong data
[Node] [2020-04-05T22:39:21.051] [DEBUG] pong - Pong data: [object Object]
[Node] [2020-04-05T22:39:21.051] [INFO] server - Get pong. msg: {"header":{"type":"PingPong","time_stamp":"2020-04-05T13:39:21.050Z","uuid":"cb6087b4-68d1-447a-b4d2-8e1271b9f735","version":0},"body":{"time_stamp":[1586093961.044,1586093961.048,1586093961.05]}}
[Node] [2020-04-05T22:39:21.052] [INFO] ping - RTT: 0.00800013542175293 [s]
[Typescript] 
...

まとめ


  • TypescriptでSocketIOの通信モジュールを実装し、nodeで実行する方法を調査、記載した

参考文献



変更履歴


  1. 2020/04/05: 新規作成

MQTTの導入

背景 IoTデバイスの接続環境構築のため、MQTT(mosquitto)の導入を行った。 記事の目的 MQTT(mosquitto)をUbuntuに導入する mosquitto ここではmosquittoについて記載する。 MQTT MQTT(Message Qu...