본문 바로가기

Node.js

Node.js Socket Programming (2)

이번 챕터는 레거시 시스템 어플리케이션에서 프로토콜을 정의하는 일반적인 방식에 대해 기술합니다.  지금은  HTTP 기반의 JSON Format처럼 String형태로 데이터를 주고받는게 일반적이지만 과거에는 Network나 System Overhead를 최대한 줄이기 위해 Binary형태로 데이터를 주고 받았습니다.  TLV Encoding이 가장 흔하게 사용됩니다.

 

TLV Encoding

TLV(Type-Length-Value) 인코딩은 효율적이고 유연한 방식으로 구조화된 데이터를 인코딩하는 데 사용되는 방법입니다. 네트워크를 통해 또는 서로 다른 시스템 간에 데이터를 전송해야 하는 통신 프로토콜에서 일반적으로 사용됩니다.

TLV 인코딩을 사용하려면 다음 단계를 따라야 합니다.

데이터 구조 정의: TLV를 사용하여 인코딩하려는 데이터의 구조를 정의해야 합니다. 여기에는 포함하려는 Data Type, 각 데이터 필드의 length 및 각 필드의 Value를 지정하는 작업이 포함됩니다.

데이터 인코딩: 데이터 구조를 정의한 후에는 TLV를 사용하여 인코딩할 수 있습니다. 이렇게 하려면 먼저 데이터 유형을 지정한 다음 데이터 필드의 길이, 필드의 실제 값을 지정해야 합니다. 이 프로세스는 각 데이터 필드에 대해 반복됩니다.

데이터 디코딩: 데이터를 디코딩하려면 인코딩 프로세스를 반대로 해야 합니다. 먼저 데이터 유형을 읽은 다음 데이터 필드의 길이를 읽습니다. 그런 다음 필드의 실제 값을 읽습니다. 이 프로세스는 각 데이터 필드에 대해 반복됩니다.

예를 들어 보겠습니다.


이름: John Smith
나이: 35세
성별 남성

 

이 데이터의 TLV 인코딩은 아래와 같이 정의 할 수 있습니다.

이름: Type=1, 길이=10, 값=John Smith
연령: Type=2, 길이=4, 값=35
성별: Type=3, 길이=1, 값=M

 

Type은 1부터 3까지범위를 가지기 떄문에 1byte로 표현이 가능합니다.

Length는 Int로 표현할 수 있습니다. 대부분의 시스템에서 Int형은 4bytes의 크기를 가집니다. (만일 Value 최대 크기가 65535 bytes 미만일 경우 short형을 사용할 수 있습니다.)

Value는 Length에서 지정된 값만큼 자유롭게 가질 수 있습니다.

 

이제 TLV 형태로 인코딩하여 네트워크로 전송 해보도록 하겠습니다.

 

const net = require('net');

const TAG_USERNAME = 1;
const TAG_AGE = 2;
const TAG_SEX = 3;

function encodeTLV(tag, value) {
  const length = value.length;
  const buffer = Buffer.alloc(5 + length);
  buffer.writeUInt8(tag, 0);
  buffer.writeUInt32LE(length, 1);
  buffer.write(value, 5);
  return buffer;
}


const client = net.createConnection({ port: 3000 }, () => {
  console.log(`Connected to TCP server: ${client.remoteAddress}:${client.remotePort}`);

  const username = 'john smith';
  const usernameBuffer = encodeTLV(TAG_USERNAME, username);
  client.write(usernameBuffer);
});

client.on('data', data => {
  console.log(`Received data from server: ${data.toString()}`);
  client.end();
});

client.on('end', () => {
  console.log(`Disconnected from TCP server`);
});

<tlvclient.js>


Buffer는 Binary데이터를 처리하기 위한 node.js 기본 Class입니다.  encodeTLV함수는 TLV형태로 저장하여 Buffer값을 반환합니다. 

 

맨처음 1bytes에 TYPE에 해당되는 tag값을 저장합니다.

 

  buffer.writeUInt8(tag, 0);

 

그 다음 4byte에 value의 길이를 저장합니다.  Int로 10이 될것이며 Little Endian으로 채웁니다.  (Intel CPU의 경우 Little Endian ByteOrder 입니다. 일부 Unix시스템의 경우 BigEndian이 사용될 수 있으니 Node.js를 Unix에서 구동한다면 제조사별로 정확한 확인이 필요합니다.)

 

  buffer.writeUInt32LE(length, 1);

 

 마지막에는 Value에 해당되는 'jhon smith'를 저장 합니다.

 

  buffer.write(value, 5);

이제 buffer객체를 소켓으로 전달하면 끝납니다. node.js는 Buffer객체를 지정하면 자동으로 바이너리 type으로 인지하여 전송합니다.

이제 서버에서 TLV로 인코딩한 데이터를 수신해보도록 하겠습니다.

 

const net = require('net');

const server = net.createServer((socket) => {
  let buffer = Buffer.alloc(0);

  socket.on('data', (data) => {

    console.log('got a data : ', data, ', length = ', data.length);

    // 네트워크에서 수신된 데이터를 Append합니다.
    buffer = Buffer.concat([buffer, data]);

    // type과 length를 합쳐 최소한 5bytes이상일때만 처리합니다.
    while (buffer.length >= 5) {
      // 1byte길이의 TYPE을 읽습니다.
      const type = buffer.readUInt8(0);
      // 4bytes길이의 length를 읽습니다.
      const length = buffer.readUInt32LE(1);

      console.log(`Received TLV: type=${type}, length=${length}`);

      // value가 전부 수신되지 않았다면 처리하지 않습니다.
      if (buffer.length < 5 + length) {
        break;  // Wait for more data to arrive
      }

      // 이제 Value를 수신하여 저장합니다.
      const value = buffer.slice(5, 5 + length);

      // 처리한 데이터는 buffer에서 제거합니다.
      buffer = buffer.slice(5 + length);

      console.log(`Received TLV: type=${type}, length=${length}, value=${value.toString()}`);
      socket.write(`Received TLV: type=${type}, length=${length}, value=${value.toString()}`);
    }
  });
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});

<tlvserver.js>

 

서버와 클라이언트는 사전에 TLV형식으로 데이터를 주고 받기로 미리 약속한 상태입니다. 서버는 3000번 포트를 개방하고 접속을 기다립니다.  데이터를 수신되면 TLV Parsing을 합니다. TYPE 1bytes, Length 4bytes로 약속된 상태입니다. 만일 클라이언트가 사전에 약속한 규격으로 전송하지 않으면 적절한 응답을 주지 않습니다. 규격이 맞으면 서버는 Parsing한 정보를 Console에 Dump하고 이를 클라이언트로 반환합니다. (서버에서 클라이언트로 보내는 규격은 편의상 정의하지 않았습니다. 실제 필드에서는 적절한 TLV형태로 응답하게 됩니다.)

 

TLV규격은 처음접하면 난해하고 복잡해 보이지만  어려워 보이지만 익숙해지면 예외처리가 매우 간단합니다. 특히 TCP 네트워크에서 발생하는 Packet Fragment문제에 완벽히 대처합니다. 다음 챕터는 네트워크에서 발생할 수 있는 예외상황들에 대해 기술해 보도록 하겠습니다.

 

읽어주셔서 감사합니다.

 

'Node.js' 카테고리의 다른 글

Node.js Socket Programming (4)  (0) 2023.02.28
Node.js Socket Programming (3)  (0) 2023.02.23
Node.js Socket Programming (1)  (0) 2023.02.18
Node.js로 Restful API Server 만들기(5)  (1) 2023.02.16
Node.js로 Restful API Server 만들기(4)  (0) 2023.02.15