ラベル SocketIO の投稿を表示しています。 すべての投稿を表示
ラベル SocketIO の投稿を表示しています。 すべての投稿を表示

2020/05/10

python-socketioでAttributeError: 'module' object has no attribute 'main_thread'が出る場合の対処法

背景


Python2.7でpython-socketioを使用したとき時、下記のエラーが発生した。
$ python ./test_socketio_client.py 
Traceback (most recent call last):
  File "./test_socketio_client.py", line 123, in 
    sio_client = SocketIOClient('127.0.0.1', 10000, 'test', 'secret')
  File "./test_socketio_client.py", line 76, in __init__
    self.sio_        = socketio.Client()
  File "/home/emptyset/.local/lib/python2.7/site-packages/socketio/client.py", line 88, in __init__
    threading.current_thread() == threading.main_thread():
AttributeError: 'module' object has no attribute 'main_thread'

記事の目的


python-socketioのAttributeErrorを解決する

原因と対策


エラー原因

threadingモジュールのバージョン問題である。threadingの説明によると、main_thread関数は、バージョン 3.4以降で追加されたメソッドであるため、Python2系では使用できない。

対策

該当モジュールのmain_thread関数を修正する。
  1. python-socketioの該当モジュール(例/home/emptyset/.local/lib/python2.7/site-packages/socketio/client.py)を修正する
  2. 修正前
            global original_signal_handler
            if original_signal_handler is None and \
                    threading.current_thread() == threading.main_thread():
                original_signal_handler = signal.signal(signal.SIGINT,
                                                        signal_handler)
    修正後
            global original_signal_handler
            if original_signal_handler is None and\
                    threading.current_thread().isDaemon() is False:
                original_signal_handler = signal.signal(signal.SIGINT,
                                                        signal_handler)
    Clientのインスタンス生成がmainスレッドでのみ可能になってるが、この部分を削除した。代わりに、スレッドがデーモンかの判定を追加した。(mainスレッド以外がデーモンであると仮定)
  3. python-engineioの該当モジュール(例/home/emptyset/.local/lib/python2.7/site-packages/engineio/client.py)を修正する
  4. 修正前
            global original_signal_handler
            if original_signal_handler is None and \
                    threading.current_thread() == threading.main_thread():
                original_signal_handler = signal.signal(signal.SIGINT,
                                                        signal_handler)
    修正後
            global original_signal_handler
            if original_signal_handler is None and \
                    threading.current_thread().isDaemon() is False:
                original_signal_handler = signal.signal(signal.SIGINT,
                                                        signal_handler)
    Clientのインスタンス生成がmainスレッドでのみ可能になってるが、この部分を削除した。代わりに、スレッドがデーモンかの判定を追加した。(mainスレッド以外がデーモンであると仮定)
  5. 動作確認を行う
  6. $ python ./test_socketio_client.py 
    Try to connect to server(localhost:10000, namespace="/test", query="secret")
    Namespace may be invalid.(namespace="/test")
    Connected to server (localhost:10000, namespace="/test", query="secret")
    Send message {'test_data': 'send_from_client'} (namespace="/test")
    Emit message {'test_data': 'emit_from_client'} (namespace="/test")
    Received message {u'test_ack': u'send_from_server'}
    Received message {u'test_ack': u'emit_from_server'}
    ...

まとめ


  • python-socketioでAttributeError: 'module' object has no attribute 'main_thread'が出る場合の対処法について調査、記載した

参考文献



変更履歴


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

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: 新規作成

2019/08/17

PythonでSocketIO(クライアント編)

背景


PythonでSocketIO通信を行う必要があったため、今後の開発の効率化のためにSocketIO通信を行うクラスのテンプレートを作成する。

記事の目的


PythonでSocketIOクライアントを作成する

python-socketio


python-socketioを使用したSocketIOクライアントを作成する

選定理由

python-socketioの選定理由は、下記の通りである。

導入方法

python-socketioの導入手順は、下記の通りである。
  1. pipを利用して必要なライブラリをインストールする
  2. $ pip install requests python-socketio

テンプレート

python-socketioの使用テンプレートは、下記の通りである。
# socketioライブラリのインポート
import socketio
# ログ出力用ライブラリのインポート
from logging import getLogger, StreamHandler, DEBUG, INFO, ERROR
# 時間関係ライブラリのインポート
import time
# 終了シグナルをキャッチするライブラリのインポート
import signal
# マルチスレッドライブラリのインポート
import threading

# ロガーのインスタンス作成
logger         = getLogger(__name__)
stream_handler = StreamHandler()

# ログレベルを設定
log_level = INFO
logger.setLevel(log_level)
stream_handler.setLevel(log_level)

# ロガーにハンドラーをセット
logger.addHandler(stream_handler)

# SocketIOのテンプレート
class SocketIOClient:
    # namespaceの設定用クラス
    class NamespaceClass(socketio.ClientNamespace):
        def on_connect(self):
            pass
        def on_disconnect(self):
            pass
        def on_message(self, data):
            pass
        def on_server_to_client(self, data):
            pass
    # 接続時に呼ばれるイベント
    def on_connect(self):
        logger.info('Connected to server (%s:%d, namespace="%s", query="%s")',\
                     self.ip_,self.port_,self.namespace_,self.query_)
        self.is_connect_ = True
    # 切断時に呼ばれるイベント
    def on_disconnect(self):
        logger.info('Disconnected from server (%s:%d, namespace="%s", query="%s")',\
                     self.ip_,self.port_,self.namespace_,self.query_)
        self.is_connect_ = False
    def on_message(self, data):
        logger.info('Received message %s', str(data)) 
    # サーバーからイベント名「server_to_client」でデータがemitされた時に呼ばれる
    def on_server_to_client(self, data):
        logger.info('Received message %s', str(data)) 
    # Namespaceクラス内の各イベントをオーバーライド
    def overload_event(self):
        self.Namespace.on_connect          = self.on_connect
        self.Namespace.on_disconnect       = self.on_disconnect
        self.Namespace.on_message          = self.on_message
        self.Namespace.on_server_to_client = self.on_server_to_client
    # 初期化
    def __init__(self,ip,port,namespace,query):
        self.ip_         = ip
        self.port_       = port
        self.namespace_  = namespace
        self.query_      = query
        self.is_connect_ = False
        self.sio_        = socketio.Client()
        self.Namespace   = self.NamespaceClass(self.namespace_)
        self.overload_event()
        self.sio_.register_namespace(self.Namespace)
    # 接続確認
    def isConnect(self):
        return self.is_connect_
    # 接続
    def connect(self):
        # 接続先のURLとqueryの設定
        url = 'ws://'+self.ip_+':'+str(self.port_)+'?query='+self.query_
        logger.info('Try to connect to server(%s:%d, namespace="%s", query="%s")',\
                     self.ip_,self.port_,self.namespace_,self.query_)
        try:
            self.sio_.connect(url, namespaces=self.namespace_)            
        except: 
            logger.error('Cannot connect to server(%s:%d, namespace="%s", query="%s")',\
                          self.ip_,self.port_,self.namespace_,self.query_)
        else: 
            if not self.is_connect_:
                logger.error('Namespace may be invalid.(namespace="%s")',\
                              self.namespace_)   
    # 切断
    def disconnect(self):
        try:
            self.sio_.disconnect()
        except:
            logger.error('Cannot disconnect from server(%s:%d, namespace="%s", query="%s")',\
                          self.ip_,self.port_,self.namespace_,self.query_)
        else:
            self.is_connect_ = False
    # 再接続
    def reconnect(self):
        self.disconnect()
        time.sleep(5)
        self.connect()
    # クライアントからデータ送信(send)する
    def sendData(self, data):
        try:
            self.sio_.send(data, namespace=self.namespace_)
        except:
            logger.error('Has not connected to server(%s:%d, namespace="%s", query="%s")',\
                          self.ip_,self.port_,self.namespace_,self.query_)
        else:
            logger.info('Send message %s (namespace="%s")', str(data), self.namespace_) 
    # 独自定義のイベント名「client_to_server」で、クライアントからデータ送信(emit)する
    def emitData(self, data):
        try:
            self.sio_.emit('client_to_server', data, namespace=self.namespace_)
        except:
            logger.error('Has not connected to server(%s:%d, namespace="%s", query="%s")',\
                          self.ip_,self.port_,self.namespace_,self.query_)
        else:
            logger.info('Emit message %s (namespace="%s")', \
                         str(data), self.namespace_)  
    # メインの処理
    def run(self):
        while True:
            self.connect()
            time.sleep(1)
            if self.is_connect_:
                break
        p = threading.Thread(target=self.sio_.wait)
        p.setDaemon(True)
        p.start()
    
if __name__ == '__main__':
    # Ctrl + C (SIGINT) で終了
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    # SocketIO Client インスタンスを生成
    sio_client = SocketIOClient('localhost', 10000, '/test', 'secret')
    # SocketIO Client インスタンスを実行
    sio_client.run()
    # データを送信
    for i in range(10):
        sio_client.sendData({'test_data': 'send_from_client'})
        sio_client.emitData({'test_data': 'emit_from_client'})
        time.sleep(1)
    # 切断
    sio_client.disconnect()
    logger.info('Finish')
    # 終了
    exit()

備考

  • namespaceは、必ず「/」(スラッシュ)から始まる必要がある
  • 上記テンプレートは、サーバーと通信テストができる

まとめ


  • Python-SocketIOを利用したクライアントモジュールを作成した

参考文献



変更履歴


  1. 2019/08/17: 新規作成

PythonでSocketIO(サーバー編)

背景


PythonでSocketIO通信を行う必要があったため、今後の開発の効率化のためにSocketIO通信を行うクラスのテンプレートを作成する。

記事の目的


PythonでSocketIOサーバーを作成する

python-socketio


python-socketioを使用したSocketIOサーバーを作成する

選定理由

python-socketioの選定理由は、下記の通りである。

導入方法

python-socketioの導入手順は、下記の通りである。
  1. pipを利用してインストールする
  2. $ pip install requests eventlet python-socketio

テンプレート

python-socketioの使用テンプレートは、下記の通りである。
# socketioライブラリのインポート
import socketio
# ログ出力用ライブラリのインポート
from logging import getLogger, StreamHandler, DEBUG, INFO, ERROR
# 時間関係ライブラリのインポート
import time
# 終了シグナルをキャッチするライブラリのインポート
import signal
# eventletライブラリのインポート
import eventlet
# マルチスレッドライブラリをインポート
import threading

# ロガーのインスタンス作成
logger         = getLogger(__name__)
stream_handler = StreamHandler()

# ログレベルを設定
log_level = INFO
logger.setLevel(log_level)
stream_handler.setLevel(log_level)

# ロガーにハンドラーをセット
logger.addHandler(stream_handler)

# SocketIOのテンプレート
class SocketIOServer:
    # namespaceの設定用クラス
    class NamespaceClass(socketio.Namespace):
        def on_connect(self, sid, environ):
            pass
        def on_disconnect(self, sid):
            pass
        def on_message(self, sid, data):
            pass
        def on_client_to_server(self, sid, data):
            pass
    # 接続時に呼ばれるイベント
    def on_connect(self, sid, environ):
        logger.info('Connected to client (sid: %s, environ: %s)',\
                     str(sid), str(environ))
        self.is_connect_ = True
    # 切断時に呼ばれるイベント
    def on_disconnect(self, sid):
        logger.info('Disconnected from client (sid: %s)',str(sid))
        self.is_connect_ = False
    # サーバーからデータ送信(send)する
    def send_data(self, data):
        try:
            self.sio_.send(data, namespace=self.namespace_) 
        except:
            logger.error('Has not connected to client')
        else:
            logger.info('Send message %s (namespace="%s")',\
                         str(data), self.namespace_) 
    # 独自定義のイベント名「server_to_client」で、サーバーからデータ送信(emit)する
    def emit_data(self, data):
        try:
            self.sio_.emit('server_to_client', data, namespace=self.namespace_)
        except:
            logger.error('Has not connected to client')
        else:
            logger.info('Emit message %s (namespace="%s")',\
                         str(data), self.namespace_) 
    def on_message(self, sid, data):
        logger.info('Received message %s', str(data)) 
        self.send_data({'test_ack': 'send_from_server'})
    # クライアントからイベント名「on_client_to_server」でデータがemitされた時に呼ばれる
    def on_client_to_server(self, sid, data):
        logger.info('Received message %s', str(data))
        self.emit_data({'test_ack': 'emit_from_server'})
    # Namespaceクラス内の各イベントをオーバーライド
    def overload_event(self):
        self.Namespace.on_connect          = self.on_connect
        self.Namespace.on_disconnect       = self.on_disconnect
        self.Namespace.on_message          = self.on_message
        self.Namespace.on_client_to_server = self.on_client_to_server
    # 初期化
    def __init__(self,ip,port,namespace):
        self.ip_          = ip
        self.port_        = port
        self.namespace_   = namespace
        self.is_connect_  = False
        self.sio_         = socketio.Server(async_mode='eventlet')
        self.app_         = socketio.WSGIApp(self.sio_)
        self.Namespace    = self.NamespaceClass(self.namespace_)
        self.overload_event()
        self.sio_.register_namespace(self.Namespace)
    # 接続確認
    def isConnect(self):
        return self.is_connect_
    # 切断
    def disconnect(self,sid):
        try:
            self.sio_.disconnect(sid,namespace=self.namespace_)
        except:
            logger.error('Cannot disconnect from Client(sid="%s", namespace="%s")',\
                          namespace=self.namespace_)
        else:
            self.is_connect_ = False
    # 開始
    def start(self):
        try:
            logger.info('Start listening(%s:%d, namespace="%s")',\
                         self.ip_,self.port_,self.namespace_)
            eventlet.wsgi.server(eventlet.listen((self.ip_, self.port_)), self.app_)
        except: 
            logger.error('Cannot start listening(%s:%d, namespace="%s")',\
                          self.ip_,self.port_,self.namespace_)
    # メインの処理
    def run(self):
        p = threading.Thread(target=self.start)
        p.setDaemon(True)
        p.start()

if __name__ == '__main__':
    # Ctrl + C (SIGINT) で終了
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    # SocketIO Server インスタンスを生成
    sio_server = SocketIOServer('localhost', 10000, '/test')
    # SocketIO Server インスタンスを実行
    sio_server.run()
    # 接続待ち
    while not sio_server.isConnect():
        time.sleep(1)
    # 切断待ち
    while sio_server.isConnect():
        time.sleep(1)
    logger.info('Finish')
    # 終了
    exit()

備考

  • namespaceは、必ず「/」(スラッシュ)から始まる必要がある
  • eventletを使用した場合、他の作業スレッドから、SocketIO内のEmitやSendを呼び出せない
  • 上記テンプレートは、クライアントと通信テストができる

まとめ


  • Python-SocketIOを利用したサーバーモジュールを作成した

参考文献



変更履歴


  1. 2019/08/17: 新規作成

MQTTの導入

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