NET을 이용한 서버
-
기존에 웹 서버를 구성하려면 node.js 위에 express 와 socket.io 등 여러 라이브러리를 사용하여야 했다!
-
이번엔 node.js에 기본 포함된 net 을 이용해 서버 통신을 직접 구현하는 프로젝트이다!
서버<>클라이언트
- net을 이용해 socket 통신을 구현해준다!
// 서버
import net from 'net';
const server = net.createServer((socket)=> {
// socket.io 의 emit과 같은 기능! 클라이언트에게 송신
socket.write()
// 데이터 받을 때
socket.on('data', (data) => {
});
// 연결 끝 신호를 수신했을 때
socket.on('end', () => {
});
// 연결이 완전히 끊겼을 때
socket.on('close', () => {
});
// 오류
socket.on('error', (err) => {
});
});
// PORT = 3030 으로 서버 열기
server.listen(3030, () => {
console.log(server.address());
});
// 클라이언트
import net from 'net';
const HOST = 'localhost';
const PORT = 3030;
const socket = new net.Socket();
// HOST 와 PORT를 이용해 서버와 통신 시작!
socket.connect(PORT, HOST, ()=> {
const message = "Hello World";
// 서버에 데이터 보내기
socket.write();
})
// 데이터 받을 때
socket.on('data', (data) => {
});
// 연결 끝 신호를 수신했을 때
socket.on('end', () => {
});
// 연결이 완전히 끊겼을 때
socket.on('close', () => {
});
// 오류
socket.on('error', (err) => {
});
이벤트 구조 분리
-
각 이벤트들이 한 파일 안에 몰려있으면, 가독성과 유지보수가 아쉽다!
-
소켓 이벤트를 분리하기 위해 events 폴더를 생성하고 프로젝트에서 사용할 각 이벤트들의 이름으로 파일을 생성해준다!
.
├── assets // 게임 데이터 폴더
├── clients // 클라이언트 폴더
├── README.md // README
├── .env // 중요한 환경변수(보안!)
└── src // 서버 폴더
├── server.js // 서버 실행 파일
└── events // socket 이벤트 분리
├── onConnection.js // 클라이언트와 연결
├── onData.js // 클라이언트의 데이터 받기
├── onError.js // 소켓 통신 중 에러 발생
└── onEnd.js // 통신 마무리 요청 받기
onConnection
- 클라이언트와 연결이 되었을 때, 이벤트들을 맵핑해주는 역할이다
import { onEnd } from './onEnd.js';
import { onError } from './onError.js';
import { onData } from './onData.js';
export const onConnection = (socket) => {
console.log('클라이언트가 연결되었습니다:', socket.remoteAddress, socket.remotePort);
socket.on('data', onData(socket));
socket.on('end', onEnd(socket));
socket.on('error', onError(socket));
};
- 이를 server.js에서 import하여 사용하게 된다!
import net from 'net';
import initServer from './init/index.js';
import { config } from './config/config.js';
import { onConnection } from './events/onConnection.js';
const server = net.createServer(onConnection);
server.listen(config.server.port, config.server.host, () => {
console.log(server.address());
});
events
- 연결에 매핑되어 실행되는 함수들을 이벤트(onData, onEnd, onError 등등)에 맞게 구성해준다!
/* onData.js */
// (socket) => (data) => : 커링 기법
// onConnection -> (socket)-> onData -> (data) -> 2개 다 사용
export const onData = (socket) => (data) => {
console.log('데이터를 받았습니다!',data);
};
/* onError.js */
export const onError = (socket) => (err) => {
console.error('소켓 오류:', err);
};
/* onEnd.js */
export const onEnd = (socket) => () => {
console.log('클라이언트 연결이 종료되었습니다.');
};
- 이 때 인자를 순서에 따라 여러 번 받는 방법을 커링(Curring) 이라 한다!
Buffer
-
위에서 서버와 클라이언트의 소켓통신을 구현하였다!
-
이제 서로 데이터를 주고 받아야 하는데..
TCP는 Byte 배열을 주고받기에 이에 맞는 객체 Buffer를 사용해준다!
(기본적으로 js는 2 Bytes 단위로 문자를 구성하지만 TCP는 1 Byte를 단위로 사용함 )
장점
-
고정 길이: 모든 데이터가 1바이트 단위로 처리되므로, 데이터를 다루기 쉽고 효율적임
-
빠른 접근: Byte 단위로 데이터를 직접 접근하고 조작할 수 있어 이진 데이터 처리에서 매우 유용함
-
메모리 효율성: Buffer 객체는 필요한 만큼의 메모리만 사용함!
( 예를 들어, 1 Byte 의 데이터를 처리할 때 1 Byte의 메모리만 사용 ) -
호환성: 대부분의 네트워크 프로토콜과 파일 포맷이 Byte 단위로 데이터를 처리하므로,
Buffer 객체를 사용하면 추가적인 변환 과정 없이 쉽게 데이터를 주고받을 수 있음
사용법
// data를 Buffer 객체로 변환
const buffer = Buffer.from(data);
// arr.splice 와 똑같이 일정 길이를 제거해주는 역할
buffer.subarray()
// 32비트(= 4 Bytes) 크기의 정보를 기입
buffer.writeUInt32BE(data, 0);
// 16비트(= 2 Bytes) 크기의 정보를 기입 (뒤에는 offset 거리 조절용)
buffer.writeUInt16BE(data2, data.length);
// 두 Buffer 객체를 하나로 합쳐줌
Buffer.concat([buffer1, buffer2])
// 지정된 사이즈의 버퍼 객체 생성
Buffer.alloc(size)
Header
-
Buffer 객체를 쓰기 전, socket.io(웹소켓 라이브러리) 에서 보낸 Packet들은 각자 무엇에 관한 정보인지 알 수 있었다!
-
이러한 정보들을 Buffer 객체에도 적용해주기 위해 Header 개념을 사용할 것 이다!
-
보낼 데이터 앞에 Header를 붙여 데이터의 정보를 알려주는 방법이다
Field | Type | Description | Size( Byte ) |
---|---|---|---|
totalLength | int | 메세지의 전체 길이 | 4 Byte |
handlerId | int | 요청을 처리할 서버 핸들러의 ID | 2 Byte |
message | string | 메세지(Payload) | Variable |
/* constant.js */
export const TOTAL_LENGTH = 4;
export const HANDLER_ID = 2;
/* utils.js */
import { HANDLER_ID, TOTAL_LENGTH } from "./constant.js";
/* 헤더 읽는 방식 통일 */
export const readHeader = (buffer) => {
// Big Endian 방식 = 오름차순 정렬
return {
length: buffer.readUint32BE(0),
handlerId: buffer.readUint16BE(TOTAL_LENGTH),
};
}
/* 헤더를 추가하는 방식 통일 */
export const writeHeader = (length, handlerId) => {
const headerSize = TOTAL_LENGTH + HANDLER_ID;
const buffer = Buffer.alloc(headerSize);
buffer.writeUInt32BE(length + headerSize, 0);
buffer.writeUInt16BE(handlerId, TOTAL_LENGTH);
return buffer;
}
- 이렇게 만든 설정한 헤더를 클라이언트에서 추가하여 서버에 보내는 연습!
import net from 'net';
import { readHeader, writeHeader } from './utils.js';
client.connect(PORT, HOST, ()=> {
const message = "Hello World";
const buffer = Buffer.from(message);
const header = writeHeader(buffer.length, 10);
const packet = Buffer.concat([header, buffer]);
client.write(packet);
})
바이트 배열 분리
-
현재 Buffer 객체와 Header를 적용해 클라이언트와 서버간 통신을 하고 있는데..
-
만약 여러 곳에서 데이터를 받으면 어떻게 따로 처리를 해줘야 할까!?
-
클라이언트마다 고유한 버퍼를 할당해주어 데이터를 독립적으로 처리할 수 있게 해 혼선을 방지해야 한다!
/* onConnection.js */
import { onEnd } from './onEnd.js';
import { onError } from './onError.js';
import { onData } from './onData.js';
export const onConnection = (socket) => {
console.log('클라이언트가 연결되었습니다:', socket.remoteAddress, socket.remotePort);
// 소켓 객체에 buffer 속성을 추가하여 각 클라이언트에 고유한 버퍼를 유지시킴
socket.buffer = Buffer.alloc(0);
socket.on('data', onData(socket));
socket.on('end', onEnd(socket));
socket.on('error', onError(socket));
};
- 고유한 버퍼들을 처리해주는 과정
/* onData.js */
import { TOTAL_LENGTH, HANDLER_ID } from '../constant.js';
export const onData = (socket) => async (data) => {
// 기존 버퍼에 새로 수신된 데이터를 추가
socket.buffer = Buffer.concat([socket.buffer, data]);
// 패킷의 총 헤더 길이 (패킷 길이 정보 + 핸들러 정보)
const totalHeaderLength = TOTAL_LENGTH + HANDLER_ID;
// 버퍼에 최소한 전체 헤더가 있을 때만 패킷을 처리
while (socket.buffer.length >= totalHeaderLength) {
// 1. 패킷 길이 정보 수신 (4바이트)
const length = socket.buffer.readUInt32BE(0);
// 2. 핸들러 타입 정보 수신 (2바이트) (+offset 사용)
const handlerType = socket.buffer.readUInt16BE(TOTAL_LENGTH);
// 3. 패킷 전체 길이 확인 후 데이터 수신
if (socket.buffer.length >= length) {
// 패킷 데이터를 자르고 버퍼에서 제거
const packet = socket.buffer.slice(totalHeaderLength, length);
// 남은 데이터(다음 패킷)은 다시 buffer에 저장
socket.buffer = socket.buffer.slice(length);
console.log(`length: ${length}`);
console.log(`handlerType: ${handlerType}`);
console.log(packet);
} else {
// 아직 전체 패킷이 도착하지 않음
break;
}
}
};
한줄 평 + 개선점
- 새로운 구조를 배운것 같아 신기하면서도 머리가 조금 아팠다.