타입스크립트 기반 간단한 TCP 서버/클라이언트 개발

이번에는 타입스크립트를 사용하여 간단한 TCP 서버/클라이언트를 만드는 방법에 대해 설명합니다.

[TCP 서버]

tcp_server라는 폴더를 생성한 후에 터미널에서 다음과 같이 입력하여 프로젝트를 초기화합니다.

yarn init

그러면 다음과 같은 package.json 파일이 생성됩니다.

{
  "name": "tcp_server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true
}

그리고 다음과 같이 입력하여 타입스크립트 환경을 구성합니다.

yarn add -D typescript
tsc --init

그러면 다음과 같이 tsconfig.json 파일이 생성됩니다. 아래 내용과 다르면 같게 변경해줍니다.

{
  "compilerOptions": {
    "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "outDir": "./dist",                        /* Redirect output structure to the directory. */
    "rootDir": "./src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

다음으로 src 라는 폴더를 생성한 후에 index.ts 파일을 생성하고 다음과 같이 입력합니다.

import net from 'net';

let socketCnt: number = 0;
let socketMap = new Map<number, net.Socket>();

const server = net.createServer((socket) => {
  const socketNo = socketCnt;
  socketMap.set(socketCnt++, socket);

  console.info(`socket(${socketNo}) connected`);

  socket.setEncoding('utf8');
  socket.on('data', (data) => {
    console.info(`socket(${socketNo}): ${data.toString('utf8')}`);
  });

  socket.on('error', (err) => {
    console.error(`socket(${socketNo}) Error: ${err.message}`);
  });

  socket.on('close', (hadError)=>{
    socketMap.delete(socketNo);
    if(hadError){
      console.error(`socket(${socketNo}) had an error. close socket`);
      return;
    }
    console.info(`socket(${socketNo}) closed`);
  });
});

server.on('error', (err)=>{
  console.error(`server error: ${err.message}`);
});

server.on('close', () => {
  console.info(`server closed`);
  socketMap.clear();
});

server.listen(3000, ()=>{
  const serverInfo = server.address();
  console.dir(serverInfo);
  console.log(`listen server`);

  setInterval(()=>{
    console.log('send packet to clients');
    for(const socket of socketMap.values()){
      socket.write('Hello World');
    }
  }, 3000);
});

net 모듈을 통해 TCP 서버/클라이언트를 구현할 수 있습니다. 이 모듈을 사용하기 위해 터미널에 다음과 같이 입력합니다.

yarn add -D @types/node
yarn add net

@types/node를 통해 node에서 사용하는 기본 모듈의 타입 정보를 설치하고, 그 후에 net 모듈을 설치합니다.

타입스크립트 코드는 tsc를 통해 빌드를 한 후에 node로 자바스크립트 파일을 실행해야 하지만, ts-node 모듈을 사용하면 타입스크립트 파일을 바로 실행할 수 있습니다.

yarn global add ts-node

ts-node를 global로 설치함으로써 모든 프로젝트에서 바로 사용할 수 있게 합니다.

이제, 다음의 명령어를 입력하여 TCP 서버가 정상적으로 작동하는지 확인합니다.

ts-node ./src/index.ts

만약 다음과 같은 로그가 출력되면 정상적으로 작동되었다고 볼 수 있습니다.

{ address: '::', family: 'IPv6', port: 3000 }
listen server

이제, TCP 서버 프로젝트를 패키지로 만들기 위해 다음의 모듈을 설치합니다.

yarn add -D pkg

그리고 package.json을 다음과 같이 수정합니다.

{
  "name": "tcp_server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "scripts": {
    "pkg-win-x64": "tsc && pkg . --targets node12-win-x64 --output ./build/x64/app.exe",
    "pkg-linux": "tsc && pkg . --targets node12-linux --output ./build/linux/app"
  },
  "dependencies": {
    "net": "^1.0.2"
  },
  "devDependencies": {
    "@types/node": "^13.1.1",
    "pkg": "^4.4.2",
    "typescript": "^3.7.4"
  },
  "bin": "./dist/index.js"
}

여기서 "bin"은 패키지의 시작이 되는 스크립트 파일을 지정합니다. 그리고 "scripts"에서 "pkg-win-x64"는 윈도우 x64용으로 패키지를 생성하는 작업을 의미하고, "pkg-linux"는 리눅스 용으로 패키지를 생성하는 작업을 의미합니다.

이제 터미널에 다음과 같이 입력하여 윈도우 x64용 패키지를 생성해봅니다.

yarn run pkg-win-x64

그러면 'tcp_server/build/x64/' 폴더에 app.exe라는 파일이 생성된 것을 확인할 수 있습니다. 해당 파일을 실행하면 콘솔 창에 TCP 서버 관련 로그가 표출됩니다.

만약, 리눅스 용으로 빌드하고 싶다면 'yarn run pkg-linux'를 입력하면 됩니다. 단, 리눅스 빌드는 리눅스 운영체제에서 수행해야 정상적으로 실행됩니다.

[TCP 클라이언트]

TCP 클라이언트도 TCP 서버와 마찬가지의 방식으로 프로젝트를 구성하면 되며, index.ts의 소스코드는 다음과 같습니다.

import net from 'net';
import fs from 'fs';
import path from 'path';

function main(){
  let serverInfo;
  let appDir = process.argv[0];

  try{
    serverInfo = JSON.parse(fs.readFileSync(path.join(appDir, '../server.json')).toString('utf8'));
  }
  catch(ex){
    console.error(ex.message);
    return;
  }

  let connectOpts: net.NetConnectOpts = {
    port: serverInfo['port'],
    host: serverInfo['host']
  };

  let socket = net.connect(connectOpts);

  socket.on('connect', () => {
    console.log(`connected to server`);
  });

  socket.on('data', (data) => {
    console.log(`data: ${data.toString('utf8')}`);
  });

  socket.on('end', () => {
    console.log('disconnected');
  });

  socket.on('error', (err)=>{
    console.error(`error: ${err.message}`);
  });
}

main();

TCP 클라이언트는 처음 실행 시 server.json 파일로부터 서버 정보를 읽어들입니다. server.json 파일은 다음과 같습니다.

{
  "host": "192.168.0.5",
  "port": "3000"
}

여기서 host는 각자의 IP 주소로 변경하면 됩니다. server.json은 패키지 파일과 같은 경로에 두면 됩니다.

TCP 서버를 실행한 후에 TCP 클라이언트를 실행하면 3초에 한 번씩 TCP 클라이언트 콘솔에 "Hello World"라는 메시지가 출력되는 것을 확인할 수 있습니다.