CLOVER🍀

That was when it all began.

TypeScript+Node.jsで、Echo Server/Clientを書いてみる

これは、なにをしたくて書いたもの?

前に、Node.jsでEcho Server/Clientを書いてみたのですが。

Node.jsで、Echo Client/Serverを書いてみる - CLOVER🍀

今回は、こちらをTypeScriptに書き換えつつ、テストコードも書いてみようかなと思います。

環境

今回の環境は、こちら。

$ node --version
v16.13.0


$ npm --version
8.1.0

準備

まずは、TypeScriptプロジェクトの準備。Jestのインストールも合わせて。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D jest @types/jest ts-jest
$ npx ts-jest config:init
$ mkdir src test

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

jest.config.js

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

Node.jsの型宣言もインストール。

$ npm i -D @types/node

バージョンはこうなりました。

  "devDependencies": {
    "@types/jest": "^27.0.3",
    "@types/node": "^16.11.9",
    "jest": "^27.3.1",
    "prettier": "2.4.1",
    "ts-jest": "^27.0.7",
    "typescript": "^4.5.2"
  }

サーバーを書く

まずは、サーバーを書いていきます。テストコードで動かすことを考えて、サーバーの定義と起動は別々にしておきます。

クライアントと共通で使うログ出力用関数。

src/logger.ts

export const logger = (fun: () => any) =>
  console.log(`[${new Date().toISOString()}] ${fun()}`);

サーバー定義。

src/server.ts

import { Server, createServer } from 'net';
import { logger } from './logger';

function newServer(): Server {
  const server = createServer((socket) => {
    const clientAddress = `${socket.remoteAddress}:${socket.remotePort}`;

    logger(() => `connect client = ${clientAddress}`);

    socket.setEncoding('utf8');

    socket.on('data', (data: string) => {
      const trimmedData = data.trim();

      logger(() => `received data[${trimmedData}]`);
      socket.write(`★${trimmedData}★`);
    });

    socket.on('close', () =>
      logger(() => `disconnect client = ${clientAddress}`)
    );
  });

  server.on('error', (err) => {
    throw err;
  });

  server.on('close', () => logger(() => 'echo server, shutdown'));

  return server;
}

export const server = newServer();

送られてきたメッセージに、「★」を付けて返すことにします。

      socket.write(`★${trimmedData}★`);

起動する部分。

src/runServer.ts

import { logger } from './logger';
import { server } from './server';

const address = 'localhost';
const port = 3000;

const options = {
  host: address,
  port: port,
};

server.listen(options, () =>
  logger(() => `echo server[${address}:${port}], startup`)
);

確認してみましょう。

$ npx tsc

起動。

$ node dist/runServer.js
[2021-11-20T12:56:22.235Z] echo server[localhost:3000], startup

確認。

$ echo hello | nc localhost 3000
★hello★


$ echo world | nc localhost 3000
★world★

サーバー側のログ。

[2021-11-20T12:56:46.182Z] connect client = 127.0.0.1:59306
[2021-11-20T12:56:46.183Z] received data[hello]
[2021-11-20T12:56:49.068Z] disconnect client = 127.0.0.1:59306
[2021-11-20T12:57:05.967Z] connect client = 127.0.0.1:59348
[2021-11-20T12:57:05.968Z] received data[world]
[2021-11-20T12:57:07.053Z] disconnect client = 127.0.0.1:59348

クライアント側

続いて、クライアント側を書いていきます。

クライアント定義。

src/client.ts

import net from 'net';
import { logger } from './logger';

export function sendMessage(
  address: string,
  port: number,
  message: string
): Promise<string> {
  return new Promise((resolve) => {
    const client = net.createConnection({ host: address, port: port }, () =>
      logger(() => `start client`)
    );

    client.setEncoding('utf8');

    client.on('connect', () => {
      logger(() => `connected server[${address}:${client.remotePort}]`);
      client.write(message);
      logger(() => `send message[${message}]`);
    });

    client.on('data', (data: string) => {
      resolve(data);
      client.end();
      client.destroy();
    });
  });
}

起動部分。起動時の引数で、送信するメッセージや送信先を指定できるようにしています。

src/runClient.ts

import { sendMessage } from './client';
import { logger } from './logger';

const args = process.argv.slice(2);

let address;
let port;
let message;

if (args.length > 2) {
  address = args[0];
  port = parseInt(args[1]);
  message = args[2];
} else if (args.length > 1) {
  address = 'localhost';
  port = parseInt(args[0]);
  message = args[1];
} else if (args.length == 1) {
  address = 'localhost';
  port = 3000;
  message = args[0];
} else {
  address = 'localhost';
  port = 3000;
  message = 'Hello World';
}

sendMessage(address, port, message).then((message) =>
  logger(() => `receive message[${message}]`)
);

ビルドして

$ npx tsc

引数別に確認。サーバー側は起動したままとします。

$ node dist/runClient.js
[2021-11-20T13:13:28.345Z] start client
[2021-11-20T13:13:28.347Z] connected server[localhost:3000]
[2021-11-20T13:13:28.347Z] send message[Hello World]
[2021-11-20T13:13:28.349Z] receive message[★Hello World★]


$ node dist/runClient.js TypeScript
[2021-11-20T13:13:40.328Z] start client
[2021-11-20T13:13:40.331Z] connected server[localhost:3000]
[2021-11-20T13:13:40.331Z] send message[TypeScript]
[2021-11-20T13:13:40.333Z] receive message[★TypeScript★]


$ node dist/runClient.js 3000 TypeScript
[2021-11-20T13:13:43.449Z] start client
[2021-11-20T13:13:43.450Z] connected server[localhost:3000]
[2021-11-20T13:13:43.451Z] send message[TypeScript]
[2021-11-20T13:13:43.453Z] receive message[★TypeScript★]


$ node dist/runClient.js 127.0.0.1 3000 TypeScript
[2021-11-20T13:14:01.253Z] start client
[2021-11-20T13:14:01.257Z] connected server[127.0.0.1:3000]
[2021-11-20T13:14:01.257Z] send message[TypeScript]
[2021-11-20T13:14:01.259Z] receive message[★TypeScript★]

OKですね。

これで、いったんサーバー側も停止しておきます。

テストを書く

最後に、テストコードを書いて確認します。

test/echo.test.ts

import { server } from '../src/server';
import { sendMessage } from '../src/client';

const address = 'localhost';
const port = 3001;

beforeAll(
  () =>
    new Promise((resolve) =>
      server.listen({ host: address, port: port }, () => resolve('start'))
    )
);

test('echo, Hello World', async () => {
  const reply = await sendMessage(address, port, 'Hello World');

  expect(reply).toBe('★Hello World★');
});

test('echo, こんにちは、世界', async () => {
  const reply = await sendMessage(address, port, 'こんにちは、世界');

  expect(reply).toBe('★こんにちは、世界★');
});

afterAll(() => new Promise((resolve) => server.close(() => resolve('end'))));

テスト開始時にサーバーを起動して、終了時に停止するようにしておきました。

確認。

$ npx jest

テスト自体は成功します。

 PASS  test/echo.test.ts
  ✓ echo, Hello World (34 ms)
  ✓ echo, こんにちは、世界 (8 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.321 s, estimated 3 s
Ran all test suites.

ただ、ログがたくさん出るのと

  console.log
    [2021-11-20T13:22:21.193Z] connect client = 127.0.0.1:55074

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.213Z] start client

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.216Z] connected server[localhost:3001]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.217Z] send message[Hello World]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.219Z] received data[Hello World]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.225Z] connect client = 127.0.0.1:55076

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.226Z] start client

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.227Z] connected server[localhost:3001]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.228Z] send message[こんにちは、世界]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.229Z] disconnect client = 127.0.0.1:55074

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.230Z] received data[こんにちは、世界]

      at logger (src/logger.ts:2:11)

  console.log
    [2021-11-20T13:22:21.232Z] echo server, shutdown

      at logger (src/logger.ts:2:11)

最後にテスト後に出力しようとしたログが、警告されたり。

  ●  Cannot log after tests are done. Did you forget to wait for something async in your test?
    Attempted to log "[2021-11-20T13:22:21.243Z] disconnect client = 127.0.0.1:55076".

      1 | export const logger = (fun: () => any) =>
    > 2 |   console.log(`[${new Date().toISOString()}] ${fun()}`);
        |           ^
      3 |

      at console.log (node_modules/@jest/console/build/CustomConsole.js:187:10)
      at logger (src/logger.ts:2:11)
      at Socket.<anonymous> (src/server.ts:20:13)

asyncを使っているからですね。

まとめ

前にNode.jsで書いたEcho Server/Clientを、TypeScriptで書き直しつつ、テストコードも付けてみました。

まだまだTypeScriptに慣れないので、こういうのを繰り返さないとですねぇ。