2-5 패킷 파싱
먼저,
src 폴더에서 utils라는 폴더를 만들어주고 그 폴더 안에 parser라는 폴더를 만들어준다.
parser 폴더에는 packetParser.js라는 파일을 만들어준다.
packetParser.js 파일
packetParser라는 함수를 만들어주고 (data)를 매개변수로 가져 옵니다.
그 후,
init 폴더에 있는 loadProtos.js의 getProtoMessages() 메서드를 통해
.proto라는 파일 내부 정보들을 가져옵니다.
우리가 제일 처음해줘야 할 내용은 공통 패킷 구조를 디코딩 해주는 것 입니다.
공통 패킷 구조란? .proto 파일에 있는
payload를 제외한 handlerId, userId, clientVersion, sequence를 의미 합니다.
공통 패킷의 구조가
패킷었기 때문에,
Packet이라는 변수를 만들어주고 protoMessages.common.Packet으로 공통 패킷 구조를 가져옵니다.
protoMessages.common.Packet으로 가져올 수 있는 이유는
packetNames.js으로
위와 같이
packetNames 객체에 key로 common을 만들고 해당 key에 대한 value로 다시 객체를 만들어 Packet이라는 키 안에 'common.Packet'으로 만들어놨기 때문에
init 폴더 아래에 있는 loadProtos.js에서
를 바탕으로,
위의 객체에
의 코드를 바탕으로 모든 .proto 파일을 Root 객체로 불러오고, 이를 통해 생성된 메시지 타입을 protoMessages에 저장하는 비동기 함수입니다.
Promise.all로 .proto 파일을 비동기로 한 번에 읽어오고, packetNames 객체에서 정의한 네임스페이스와 타입명을 기반으로 protoMessages 객체에 각 타입을 등록해주었기 때문에
위와 같이 사용할 수 있었습니다.
그 후,
으로, 함수의 매개변수로 전달 받은 data를
디코딩해줍니다.
이때, 오류가 발생할 수 있기 때문에, try-catch 문으로 애러가 발생할 경우 애러를 출력할 수 있게 만들어줍니다.
위의 코드를 바탕으로,
let packet 안에는
payload를 제외한
내용물이 담기게 될 것 입니다.
그래서, packet.~~~~~~로 해당 키에 대한 value를 변수에 담아주고,
clientVersion을 제외한 나머지 내용을 return 객체로 묶어 return 해줍니다.
clientVersion을 보내지 않는 이유는 클라이언트가 가지고 있는 clinetVersion과 서버가 가지고 있는 clientVersion이 다를 경우 Error를 발생시켜 주기 위해서 입니다.
이렇게 만들어진
packetParser는 가장 최초로 데이터를 받는 onData.js 파일의 onData에 호출이 돼야 합니다.
해당 부분은 패킷을 수신하고, 패킷이 모두 수신 됐는지 확인 후 패킷의 정보를 가진 Header 부분을 잘라
Body에 있는 Message만을 추출하는 코드 입니다.
해당 코드의
if문 안에서
switch(packetType)으로 PACKET_TYPE.PING과 PACKET_TYPE.NORMAL인 경우를 가져옵니다.
그렇게 할 수 있는 이유는 우리가 이전에
에
위와 같이 PING과 NORMAL을 PACKET_TYPE 객체안에 KEY와 VALUE로 정의해 놨기 때문입니다.
다시 돌아와
PACKET_TYPE.NORMAL 의 경우 안에
const { handlerId, userId, payload, sequence } = packetParser(packet);
으로 packetParser(packet)을 가져와 객체분하할당으로 packetParser의 return 값을 다 가져옵니다.
이제 클라이언트의 접속에 따른 서버의 동작을 확인하기 위해,
client.js 파일에 아래의 내용을 붙어넣어줍니다.
import net from 'net';
import { getProtoMessages, loadProtos } from './src/init/loadProtos.js';
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 sendPacket = (socket, packet) => {
const protoMessages = getProtoMessages();
const Packet = protoMessages.common.Packet;
if (!Packet) {
console.error('Packet 메시지를 찾을 수 없습니다.');
return;
}
const buffer = Packet.encode(packet).finish();
// 패킷 길이 정보를 포함한 버퍼 생성
const packetLength = Buffer.alloc(TOTAL_LENGTH);
packetLength.writeUInt32BE(buffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0); // 패킷 길이에 타입 바이트 포함
// 패킷 타입 정보를 포함한 버퍼 생성
const packetType = Buffer.alloc(PACKET_TYPE_LENGTH);
packetType.writeUInt8(1, 0); // NORMAL TYPE
// 길이 정보와 메시지를 함께 전송
const packetWithLength = Buffer.concat([packetLength, packetType, buffer]);
socket.write(packetWithLength);
};
// 서버에 연결할 호스트와 포트
const HOST = 'localhost';
const PORT = 5555;
const client = new net.Socket();
client.connect(PORT, HOST, async () => {
console.log('Connected to server');
await loadProtos();
const message = {
handlerId: 2,
userId: 'xyz',
payload: {},
clientVersion: '1.0.0',
sequence: 0,
};
sendPacket(client, message);
});
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);
});
서버를 실행하고 클라리언트를 동작시키면
위와 같이
클라이언트의 이밴트에 따른 handlerId와 ,userId, payload, sequence가 정상적으로 잘 넘어온 것을 볼 수 있습니다.