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);
});
});
에서
인 콜백 함수를 넣어줄 것 입니다. 그래서, 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);
});
});
에서
인 콜백 함수를 넣어줄 것 입니다. 그래서, 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 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 만 추출해 원하는 정보를 가공 및 처리할 수 있다.