Node 강의/주특기 플러스

2-3 (중요)이벤트 구분, 바이트 배열 분해

kagan-draca 2024. 10. 21. 20:52

1. 이벤트, 패킷 헤더

이벤트를 구분해서 관리할 수 있다.

바이트 배열에서 패킷 헤더를 구분할 수 있다.

 

1) 소켓 이벤트 분리하기

const server = net.createServer((socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

현재 서버가 만들어지고, 서버와 클라이언트가 연결 되고 클라이언트가 이벤트를 발생시키면

socket.on으로

 

"data" : 데이터가 Client에게서 서버로 왔을 때,

 

"end" : 서버가 클라이언트와 연결을 끊을 때,

 

"error" : 서버에 오류가 발생했을 때

 

모두 처리를 하고 있는 상태 입니다.

 

위와 같이 "data", "end", "error" 이벤트를 관리하기 위해 events 폴더를 만들고,

각각의 .js 파일을 생성해줍니다.

 

 

onConnection.js 파일은 서버와 클라이언트가 연결 됐을 때인

const server = net.createServer((socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

에서 

(socket) => {
 
}

인 콜백 함수를 넣어줄 것 입니다. 그래서, onConnection.js 파일에는

export const onConnection = (socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

위와 같이 data, end, error 이벤트 또한 들어가게 됩니다.

 

그리고, net.createServer() 메서드를

import { onConnection } from './events/onConnection.js';

const server = net.createServer(onConnection);

위와 같이 수정해줍니다.

 

다시, onConnection.js 파일로 돌아와 각각의 이벤트에 따른 콜백 함수도 .js 파일로 빼내봅시다.

 

2) 커링(Currying)

'커링' : 여러 인수를 받는 함수를 인수가 하나인 함수들의 연속으로 변환하는 기법입니다.

 

ex)

export const onData = (socket) => (data) => {
  console.log(data);
};

 

쉽게 설명하자면, onData보다 상위인 onConnection 함수가 가지고 있는 매개변수인 socket도 쓰고,

onData의 매개변수인 data도 쓰겠다라는 문법 입니다.

 

onConnection → (socket) → onData → (data)  2개 다 사용. 이벤트, 패킷 헤더

이벤트를 구분해서 관리할 수 있다.

 

바이트 배열에서 패킷 헤더를 구분할 수 있다.

 

 

 

1) 소켓 이벤트 분리하기

const server = net.createServer((socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

 

현재 서버가 만들어지고, 서버와 클라이언트가 연결 되고 클라이언트가 이벤트를 발생시키면

 

socket.on으로

 

"data" : 데이터가 Client에게서 서버로 왔을 때,

 

"end" : 서버가 클라이언트와 연결을 끊을 때,

 

"error" : 서버에 오류가 발생했을 때

 

모두 처리를 하고 있는 상태 입니다.

 

위와 같이 "data", "end", "error" 이벤트를 관리하기 위해 events 폴더를 만들고,

 

각각의 .js 파일을 생성해줍니다.

 

 

onConnection.js 파일은 서버와 클라이언트가 연결 됐을 때인

const server = net.createServer((socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

 

에서 

(socket) => {

}

인 콜백 함수를 넣어줄 것 입니다. 그래서, onConnection.js 파일에는

export const onConnection = (socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

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

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

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

위와 같이 data, end, error 이벤트 또한 들어가게 됩니다.

 

그리고, net.createServer() 메서드를

import { onConnection } from './events/onConnection.js';
const server = net.createServer(onConnection);

위와 같이 수정해줍니다.

 

다시, onConnection.js 파일로 돌아와 각각의 이벤트에 따른 콜백 함수도 .js 파일로 빼내봅시다.

 

2) 커링(Currying)

'커링' : 여러 인수를 받는 함수를 인수가 하나인 함수들의 연속으로 변환하는 기법입니다.

 

ex) onData.js를 아래와 같이 구현

export const onData = (socket) => (data) => {
  console.log(data);
};

 

쉽게 설명하자면, onData보다 상위인 onConnection 함수가 가지고 있는 매개변수인 socket도 쓰고,

 

onData의 매개변수인 data도 쓰겠다라는 문법 입니다.

 

 

onConnection → (socket) → onData → (data) 2개 다 사용

 

onData.js를 위와 같이 구현했다면, 

import { onData } from './onData.js';
export const onConnection = (socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

  socket.on('data', onData(socket));

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

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

onConnection.js의 onConnection 함수도 위와 같이 내용이 바꿔야 합니다.

 

나머지 이벤트도 동일하게 변경해주면,

 

변경된 onError.js

export const onError = (socket) => (error) => {
  console.log('소켓 오류 : ', error);
};

 

변경된 onEnd.js

export const onEnd = (socket) => () => {
  console.log('클라이언트 연결이 종료 되었습니다.');
};

 

변경된 onConnection.js

import { onData } from './onData.js';
import { onEnd } from './onEnd.js';
import { onError } from './onError.js';

export const onConnection = (socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

  socket.on('data', onData(socket));

  socket.on('end', onEnd(socket));

  socket.on('error', onError(socket));
};

 

3) 바이트 배열 분해하기

 

가장 처음 할 일은 각 클라이언트 마다 고유한 버퍼를 할당해주는 것 입니다.

고유한 버퍼를 유지함으로써 데이터를 독립적으로 처리할 수 있게 해 혼선을

방지하고, 안정성과 성능 향상을 노릴 수 있습니다.

 

수정된 코드 : 

import { onData } from './onData.js';
import { onEnd } from './onEnd.js';
import { onError } from './onError.js';

export const onConnection = (socket) => {
  console.log(`Client connected from ${socket.remoteAddress} : ${socket.remotePort}`);

  socket.buffer = Buffer.alloc(0);
  // 소켓 객체에 buffer 속성을 추가하여 각 클라이언트에 고유한 버퍼를 유지

  socket.on('data', onData(socket));

  socket.on('end', onEnd(socket));

  socket.on('error', onError(socket));
};

 

클라이언트와 서버는 socket 객체를 통해 통신을 하게 되는데,

  socket.buffer = Buffer.alloc(0);

소켓에 왜 빈 버퍼를 만들어주냐면 

 

각 클라이언트마다 고유한 버퍼를 유지 시키기 위해서 입니다.

그래서, 해당 빈 버퍼에 데이터를 계속해서 쌓아나갈 수 있게 됩니다.

 

다시 onData.js에 돌아와

 

  const totalHeaderLength = config.packet.totalLength + config.packet.typeLength;

  // 버퍼의 해더 크기 보다 큰 경우 => 그때부터 진짜 message, data가 오는 상황이 된다.
  while (socket.buffer.length >= totalHeaderLength) {}

위와 같은 내용을 추가해줍니다.

 

  const totalHeaderLength = config.packet.totalLength + config.packet.typeLength;

config.packet.totalLength 와 config.packet.typeLength로

 

totalLength : 메시지 전체 길이

typeLength : 패킷의 타입 정보 길이

 

로 전체 패킷의 총 해더 길이를 구해

 

while문에서 패킷에 해더 길이보다 큰 경우에만 while문이 동작되게 만듭니다.

그렇게 하기 위해서는 

  socket.buffer = Buffer.concat([socket.buffer, data]);
  // 기본 버퍼에 새로 수신된 데이터 추가하기

socket.buffer에 새로 들어온 데이터가 socket.buffer에 쌓이도록 만들어줘야 합니다.

 

여태까지 수정된 코드 : 

 

import { config } from '../config/config.js';

export const onData = (socket) => (data) => {
  console.log(data);

  socket.buffer = Buffer.concat([socket.buffer, data]);
  // 기본 버퍼에 새로 수신된 데이터 추가하기

  const totalHeaderLength = config.packet.totalLength + config.packet.typeLength;

  // 버퍼의 해버 크기 보다 큰 경우 => 그때부터 진짜 message, data가 오는 상황이 된다.
  while (socket.buffer.length >= totalHeaderLength) {}
};

 

while문 안에서는 

while (socket.buffer.length >= totalHeaderLength) {
    const length = socket.buffer.readUInt32BE(0);
    // 버퍼에 0 ~ 4Byte(4Byte 크기) 까지는 패킷 길이 정보

    const packetType = socket.buffer.readUInt8(config.packet.totalLength);
    // 4Byte ~ 5Byte(1Byte 크기) 만큼은 packet의 타입

    if (socket.buffer.length >= length) {
      // 퍼버의 현재 길이가 원하는 버퍼의 사이즈 보다 크거나 같을 경우

      const packet = socket.buffer.slice(totalHeaderLength, length);
      // 해더 부분을 제거해서 원하는 packet Data만 가져온다.
      socket.buffer = socket.buffer.slice(length);
      // 남은 해더 부분도 제거해서 다음 이벤트에 따른 Header와 packet Data 정보를 기다린다.
      console.log(`length : ${length}`);
      console.log(`packetType : ${packetType}`);
      console.log(packet);
    } else break;
    // 아직 전체 패킷이 도착하지 않았을 때
  }

위와 같이 작성합니다.

(아래 부터 설명)

 

    const length = socket.buffer.readUInt32BE(0);

0 ~ 32 bit(4 Byte) 만큼 자르고 그 안의 16진수 값을 10진수 정수로 바꿉니다.

=> 이 값은 Packet의 해더와 Packet Data의 크기 입니다.

즉, Packet의 전체 크기

 

    const packetType = socket.buffer.readUInt8(config.packet.totalLength);
    // 4Byte 위치에서 (1Byte 크기) 만큼은 packet의 타입

그 후, 위와 같이

 

socket.buffer.readUInt8(config.packet.totalLength)

로 4Byte 위치에서 부터 8bit(1Byte)만큼 1Byte 크기를 잘라 안의 16진수 값을

10진수로 변환해 packet의 타입 정보를 알아냅니다.

 

    if (socket.buffer.length >= length) {

socket.buffer.length >= length

즉, 현재 socket의  buffer에 들어온 data 크기가 해더로 알게 된 packet의 크기와 같거나 크다면

=> 받아야 할 data를 받았거나 더 받았다면,

 

      const packet = socket.buffer.slice(totalHeaderLength, length);
      // 해더 부분을 제거해서 원하는 packet Data만 가져온다.

현재 socket.buffer 안의 packetData를 slice로 해더에서부터 전체 길이만큼 자릅니다

=> 해더를 제외한 실제 packet Data만 남긴다.

 

그리고,

      socket.buffer = socket.buffer.slice(length);
      // 남은 해더 부분도 제거해서 다음 이벤트에 따른 Header와 packet Data 정보를 기다린다.

 socket.buffer.slice(length)로 0 ~ 해더 길이 만큼

남은 socket.buffer의 packet을 제거해 다음 이벤트에 따른 data를 받을 준비를 합니다.

 

 

 

우리는 packet의 타입을 아직 정해주지 않았기 때문에

constants/header.js 파일에

export const PACKET_TYPE = {
  PING: 0,
  NORMAL: 1,
};

을 추가해줍니다.

 

전체 코드 내용 : 

export const TOTAL_LENGTH = 4;
export const PACKET_TYPE_LENGTH = 1;
export const PACKET_TYPE = {
  PING: 0,
  NORMAL: 1,
};

 

 

그리고, 최상위 폴더에 Test를 위해서 Client.js 파일을 만들어줍니다. 그리고,

import net from 'net';

const TOTAL_LENGTH = 4; // 전체 길이를 나타내는 4바이트
const PACKET_TYPE_LENGTH = 1; // 패킷타입을 나타내는 1바이트

const readHeader = (buffer) => {
  return {
    length: buffer.readUInt32BE(0),
    packetType: buffer.writeUInt8(TOTAL_LENGTH),
  };
};

const writeHeader = (length, packetType) => {
  const headerSize = TOTAL_LENGTH + PACKET_TYPE_LENGTH;
  const buffer = Buffer.alloc(headerSize);
  buffer.writeUInt32BE(length + headerSize, 0);
  buffer.writeUInt8(packetType, TOTAL_LENGTH);
  return buffer;
};

// 서버에 연결할 호스트와 포트
const HOST = 'localhost';
const PORT = 3000;

const client = new net.Socket();

client.connect(PORT, HOST, () => {
  console.log('Connected to server');

  const message = 'Hi, There!';
  const test = Buffer.from(message);

  const header = writeHeader(test.length, 11);
  const packet = Buffer.concat([header, test]);
  client.write(packet);
});

client.on('data', (data) => {
  const buffer = Buffer.from(data); // 버퍼 객체의 메서드를 사용하기 위해 변환

  const { handlerId, length } = readHeader(buffer);
  console.log(`handlerId: ${handlerId}`);
  console.log(`length: ${length}`);

  const headerSize = TOTAL_LENGTH + PACKET_TYPE_LENGTH;
  // 메시지 추출
  const message = buffer.slice(headerSize); // 앞의 헤더 부분을 잘라낸다.

  console.log(`server 에게 받은 메세지: ${message}`);
});

client.on('close', () => {
  console.log('Connection closed');
});

client.on('error', (err) => {
  console.error('Client error:', err);
});

위와 같은 내용을 작성해줍니다.

(TCP Echo Server의 내용과 동일한 내용)

 

그 결과 Server를 실행 시키고, Client가 접속과 동시에 "Hi, There"이라는 Packet Data를 보내면,

 

클라이언트가 서버에 접속

서버는 클라이언트가 보낸 Header로 부터 Packet의 전체 길이 Length와 Packet Data의 Type을 알 수 있게 되고,

Header와 Packet Data에서 Packet Data 만 추출해 원하는 정보를 가공 및 처리할 수 있다.