Post

(TIL) 2025-02-19 FINAL(12)

(TIL) 2025-02-19 FINAL(12)

분산 서버 작업 - 3일차


서버 아키택처

클라이언트로부터 어떤 요청을 받았을 경우 게이트 웨이 서버가 각 서버에 전달을 하고 결과를 받아오고 다시 클라이언트로 응답을 하기 위해서는 요청한 클라이언트(유저)의 소켓을 관리를 해야하는데 로그인 전에는 그 유저에 대한 정보를 특정할 수가 없어 어떻게 처리를 할지 고민을 하다보니 왜 로그인 서버를 따로 두는지 이해가 가기 시작했다. 로그인(인증 서버)를 별도로 두어 로그인이 되었을 때 해당 유저에 대한 정보를 정확하게 유저 아이디나 기타 정보로 유저가 연결을 종료할때까지 유지할 수 있으니 관리 편리상 따로 두는 것을 이해할 수 있었다.

그리하여 로그인 서버까지 따로 별도로 만들면 작업이 많아지니 우선은 게이트 웨이서버에서 인증기능을 처리하기로 하였다.

게이트 웨이 서버 작업

1
2
3
4
5
6
7
8
9
10
11
const onConnection = async (socket) => {
  console.log('클라이언트가 연결되었습니다:', socket.remoteAddress, socket.remotePort);

  socket.buffer = Buffer.alloc(0);

  socket.on('data', onData(socket));
  socket.on('end', onEnd(socket));
  socket.on('error', onError(socket));
  // id정보 없는 유저 생성
  userSession.addUser(socket);
};

클라이언트와 연결이 되면 소켓을 키값으로 유저 세션에 등록처리를하여 소켓을 관리하기로 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// header 생성
  const packetTypeBuffer = Buffer.alloc(2);
  // 생략
  //유저 정보 추가
  const userIdBuffer = Buffer.from('' + userId);

  const userIdLengthBuffer = Buffer.alloc(1);
  userIdLengthBuffer.writeUInt8(userIdBuffer.length);

  const payloadLengthBuffer = Buffer.alloc(4);
  payloadLengthBuffer.writeUInt32BE(payloadBuffer.length);

  const packet = Buffer.concat([
    packetTypeBuffer,
    versionLengthBuffer,
    versionBuffer,
    userIdLengthBuffer,
    userIdBuffer,
    payloadLengthBuffer,
    payloadBuffer,
  ]);

그리고 로그인을 하게되면 앞으로 각 서버에 패킷을 전달할때 패킷 헤더에 유저ID값을 담아 전송하게되며 다시 클라이언트로 응답 패킷을 보낼 때 서버에서 온 패킷의 유저ID 값을 디코딩하여 해당 유저의 소켓을 찾아 전송하는 방법으로 진행하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 로비서버 핸들러
const onLobbyServerHandler = ({ socket, payload, packetType }) => {
  // 유저 객체 조회
  const user = userSession.getUser(socket.id);
  if (!user || !user.id) {
    throw new CustomError('유저 정보가 없습니다.');
  }

  if (user.getGameState()) {
    throw new CustomError(`올바르지 못한 요청입니다. (USER ID: ${user.id})`);
  }

  const packetInfo = Object.values(config.packetType).find(([type, name]) => type === packetType);

  const packet = makeServerPacket(packetInfo, payload, user.id);

  const serverSocket = serverSession.getServerById(config.server.lobbyServer);
  serverSocket.write(packet);
};

각 서버로 패킷을 생성하는 메소드를 만들어 각 서버 핸들러별로 로직을 구현작업을 마무리하였다.

로비서버 작업

게이트웨이 서버에서 로그인을 성공하게되면 로비서버에서도 유저관리를 위해 동기화 처리를 해줘야 하니 별도로 로비서버에 패킷을 전송하여 로비서버에도 유저세션 정보를 추가할 수 있는 핸들러를 만들었다.

1
2
3
4
5
6
7
8
9
10
11
const loginCastHandler = ({ socket, payload }) => {
  const { user, success } = payload;

  if (!user.userId || !user.name || !success) {
    throw new CustomError('올바르지않은 요청입니다.');
  }

  userSession.addUser(user.userId, user.name, socket);

  console.log('들어간 유저들' + userSession.getUserIds());
};

테스트

그리고 기존에 로비서버의 프로세스들은 구현이 되어있으니 패킷전송하는 부분만 분산서버에 맞게 수정을 하여 더미클라이언트를 통해서 로그인 -> 방생성 -> 게임 시작까지 잘 패킷이 서로 주고 받는지 테스트를 해보았다.

전송 테스트

  1. 로그인 클라이언트에서 로그인을 시도하게되면 게이트웨이 서버에서 DB로 로그인 정보를 조회하여 로그인 처리를 하고 로비서버에 정상적으로 유저정보를 보낸 것을 로비 서버 로그(1005번)을 통해 확인이 가능했다.

  2. 방 생성 로비 서버 로그를 보면 packetType 2001 로그가 생성된 걸 보면 정상적으로 게이트웨이 서버에서 로비서버로 전송된 것을 알 수 있었고 생성 로직 또한 방이 생성되었다는 로그를 통해 알 수 있었다.

  3. 게임 시작 게임 시작 요청을 보내면 게이트웨이 서버가 로비 서버에 시작 요청을 보내고 로비 서버에서 유저와 룸 상태 정보를 변경하는 로직 처리를 한다. 그리고 게임 서버에서 게임 세션을 만들기 위한 룸 세션 정보를 게임 서버로 전송을 하게된다. 아직 게임 서버에서의 작업은 만들지는 않았고 정상적으로 로비 서버 > 게이트 웨이 서버 > 게임 서버로 패킷을 전달하고 게임 서버에서 패킷이 도착한 결과까지 오늘 확인 할 수 있었다.

이것으로 클라이언트 - 게이트 웨이 - 로비 - 게임 서버까지 패킷이 잘 전달되고 있다는 것을 확인 할 수 있었다. 어제 오늘로 게이트웨이 , 로비 서버에 대한 작업을 완성하였고 내일은 게임 서버를 구조에 맞게 변경을 할 예정이다.

This post is licensed under CC BY 4.0 by the author.