背景
仕事でTypescriptを使用する機会が出てきたので、使い方を調査した
記事の目的
TypescriptでSocketIO通信を実装する
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で実行する方法を調査、記載した
参考文献
変更履歴
- 2020/04/05: 新規作成
0 件のコメント:
コメントを投稿