지금은 클라이언트가 서버에 접속하고, C_CHAT 패킷을 보내면 게임 서버에 접속한 모든 유저에게 메시지를 보낸다.
void Handle_C_CHAT(const PacketSessionRef& session, const Protocol::C_CHAT& pkt)
{
Protocol::S_CHAT response;
GameSessionRef gameSession = static_pointer_cast<GameSession>(session);
response.set_msg(pkt.msg());
gameSession->broad_cast_test(ClientPacketHandler::MakeSendBuffer(response));
}
void GameSession::broad_cast_test(SendBufferRef sendBuffer)
{
if (ServiceRef service = _service.lock())
{
service->broad_cast_test(sendBuffer);
}
}
하지만 실제 게임에서는 특별한 상황을 제외하고는 전체 서버에 내 채팅을 보여줄 일은 없다. 함수 이름처럼 그냥 테스트를 위해 사용한 기능.
실제로는 내가 있는 곳 주변이나 내가 참가한 방에 있는 인원들에게만 메시지가 전달될 것이다.
그래서 이번에는 Room을 만들어서, 플레이어가 채팅을 치면 유저가 속한 방 안에 있는 인원들에게만 메시지가 전달되도록 만들어보자.
패킷 흐름
클라 서버
-------------------> C_LOGIN 요청 (jwt)
<------------------ S_LOGIN 성공 (success, user_id)
-------------------> C_ENTER_ROOM 요청(room_id)
<------------------ S_ENTER_ROOM 성공 (success)
<------------------ S_CHAT 접속한 room의 모든 플레이어에게 입장 소식 전달
OnConnect
위와 같은 흐름이 생기려면 연결 이후에 클라이언트가 먼저 C_LOGIN 요청을 해야한다.
그러니 OnConnect 만들어서 TCP/TLS 연결 이후에 Room에 Enter 요청하는 함수 호출하도록 만들어주자.
Session에서는 virtual 함수만 만들고 더미클라의 session인 ServerSession에서 구현한다.
원래는 인증서버에게 받은 JWT를 전달해야하는데, 일단은 pass 라는 문자열을 보내는걸로 하자.
class Session : public IocpObject
{
/*...*/
public:
/*...*/
virtual uint32 OnRecv(BYTE* buffer, uint32 len) { return len; }
virtual void OnConnect() {}
/*...*/
void TLSConnect();
};
void Session::TLSConnect()
{
OnConnect();
RegisterRecv();
}
class ServerSession : public PacketSession
{
public:
/*...*/
virtual void OnRecvPacket(BYTE* buffer, uint32 size) override;
void ServerSession::OnConnect()
{
Protocol::C_LOGIN pkt;
// TODO 인증서버로 부터 받은 jwt를 게임서버로 전달.
pkt.set_jwt("pass");
Send(ServerPacketHandler::MakeSendBuffer(pkt));
}
uint32 _userId = 0;
};
void TLSSession::TLSConnect()
{
SslStatus status = _ssl.Connect();
uint32 pendingDataSize;
switch (status)
{
case SslStatus::Ok:
Utils::LockPrint("TLS Connect OK");
HandshakeSend();
_recvEvent.SetEventType(EventType::Recv);
OnConnect();
RegisterRecv();
break;
/*...*/
}
}
Player
플레이어 데이터를 protobuf 객체에 담아 관리하면 편하다고 한다.
특히 위치정보같이 자주 보내야하는 데이터를 매번 패킷 만들고 패킷에 데이터 복사해서 보내는 것보다 바로 직렬화해서 보낼 수 있으니 좋을 듯 하다.
class Player
{
public:
Player(const Protocol::Player& player, GameSessionRef owner);
~Player();
Protocol::Player _info;
GameSessionRef _owner;
};
#include "pch.h"
#include "Player.h"
Player::Player(const Protocol::Player& player, GameSessionRef owner) : _owner(owner)
{
_info.CopyFrom(player);
}
Player::~Player()
{
}
Player proto파일
message Player
{
uint64 id = 1;
string name = 2;
PlayerType playerType = 3;
Position pos = 4;
}
message Position
{
float x = 1;
float y = 2;
float z = 3;
}
Room
우선 Enter와 Leave만 만들어봤다.
접속하거나 접속 종료 시 BroadCast 함수로 접속한 플레이어들에게 접속을 알린다.
#pragma once
#include "Player.h"
class Room
{
public:
Room();
~Room();
void Enter(PlayerRef player);
void Leave(PlayerRef player);
void Broadcast(SendBufferRef sendBuffer);
private:
mutex _m;
uint32 _roomId;
map<uint64, PlayerRef> _players;
};
extern Room GRoom[2];
#include "pch.h"
#include "Room.h"
#include "Session.h"
#include "ClientPacketHandler.h"
#include "GameSession.h"
Room GRoom[2];
Room::Room()
{
static atomic<uint32> s_roomId = 0;
_roomId = s_roomId.fetch_add(1);
}
Room::~Room() {}
void Room::Enter(PlayerRef player)
{
lock_guard<mutex> lock(_m);
_players.insert({ player->_info.id(), player });
Protocol::S_CHAT pkt;
string msg = player->_info.name() + " Entered Room" + to_string(_roomId);
pkt.set_msg(msg);
Broadcast(ClientPacketHandler::MakeSendBuffer(pkt));
}
void Room::Leave(PlayerRef player)
{
lock_guard<mutex> lock(_m);
Protocol::S_CHAT pkt;
string msg = "Room " + to_string(_roomId) + " : " + player->_info.name() + " Left Room.";
pkt.set_msg(msg);
_players.erase(player->_info.id());
Broadcast(ClientPacketHandler::MakeSendBuffer(pkt));
}
void Room::Broadcast(SendBufferRef sendBuffer)
{
for (auto& player : _players)
{
player.second->_owner->Send(sendBuffer);
}
}
패킷 핸들러
패킷 흐름대로 적어보자면
클라이언트가 보낸 로그인 패킷 처리, S_LOGIN response 전송
void Handle_C_LOGIN(const PacketSessionRef& session, const Protocol::C_LOGIN& pkt)
{
Protocol::S_LOGIN response;
string jwt = pkt.jwt();
if (jwt != "pass")
{// TODO 인증 실패
response.set_success(false);
session->Send(ClientPacketHandler::MakeSendBuffer(response));
return;
}
response.set_success(true);
GameSessionRef gameSession = static_pointer_cast<GameSession>(session);
// 임시 userId 전달. gameSession 생성순서대로 1 2 3 4...
// 실제로는 jwt 검증하고 userId 확인해야함.
response.set_user_id(gameSession->_userId);
gameSession->Send(ClientPacketHandler::MakeSendBuffer(response));
}
클라가 성공패킷 받으면 EnterRoom 패킷 전송. 방은 클라가 선택하고, 짝수면 0번방, 홀수면 1번방으로 들어가게 한다.
void Handle_S_LOGIN(const PacketSessionRef& session, const Protocol::S_LOGIN& pkt)
{
if (pkt.success() == false)
{
// TODO 로그인 실패 처리
return;
}
ServerSessionRef serverSession = static_pointer_cast<ServerSession>(session);
serverSession->_userId = pkt.user_id();
Protocol::C_ENTER_ROOM enterRoomPkt;
enterRoomPkt.set_room_id(pkt.user_id() % 2); // 짝수방(0), 홀수방(1) 만있다고 가정
session->Send(ServerPacketHandler::MakeSendBuffer(enterRoomPkt));
}
서버는 클라가 보낸 EnterRoom 요청을 받으면 새 플레이어 객체를 만들어서 room.Enter함수로 넣어준다.
void Handle_C_ENTER_ROOM(const PacketSessionRef& session, const Protocol::C_ENTER_ROOM& pkt)
{
Protocol::S_ENTER_ROOM response;
GameSessionRef gameSession = static_pointer_cast<GameSession>(session);
Protocol::Player playerInfo;
playerInfo.set_id(gameSession->_userId);
playerInfo.set_name("Player" + ::to_string(gameSession->_userId));
switch (gameSession->_userId % 4)
{
case 0:
playerInfo.set_playertype(Protocol::PlayerType::PLAYER_TYPE_ARCHER);
break;
case 1:
playerInfo.set_playertype(Protocol::PlayerType::PLAYER_TYPE_KNIGHT);
break;
case 2:
playerInfo.set_playertype(Protocol::PlayerType::PLAYER_TYPE_MAGE);
break;
}
Protocol::Position pos;
pos.set_x(Utils::GetRandNum(0, 1000));
pos.set_y(Utils::GetRandNum(0, 1000));
pos.set_x(100);
playerInfo.mutable_pos()->CopyFrom(pos);
PlayerRef player = make_shared<Player>(playerInfo, gameSession);
GRoom[pkt.room_id()].Enter(player);
response.set_success(true);
session->Send(ClientPacketHandler::MakeSendBuffer(response));
}
테스트

잘 된다.
0번 방에 플레이어 2, 4. 1번방에 1, 3, 5가 입장한다.
입장 시 입장해있는 Room의 모든 플레이어에게 chat 패킷이 간다.
출력되는 횟수도 정상이다.
버그 해결
처음부터 출력이 정상적이지는 않았다.
만약 0번 room에 플레이어 a, b가 접속한다면, 분명히 a가 접속할 때 1번 chat이 호출(자기자신), b가 접속할 때 2번 chat이 호출(자기자신 + 기존에 접속했던 a)되어야 하는데, 2번만 호출되는 문제가 있었다.
결론부터 말하면 원인은 데이터 수신 과정에서 복호화 과정에 있었다.
복호화 과정은 이랬다. (지금은 수정됨)
- encBuffer에 수신된 데이터 저장됨 (암호화된 상태)
- Decrypt 함수로 encBuffer의 암호화 데이터를 복호화 하여 decBuffer에 저장.
- SSL_has_pending 함수로 rbio에 복호화 할 데이터가 남아있는지 확인하고, 있다면 Decrypt 반복.
여기서 문제는 SSL_has_pending 함수가 내 생각과 다르게 작동한다는 점이었다. (문서를 봤는데 3과같이 작동하는줄 알았다.)
SSL_has_pending은 rbio에 적힌 데이터를 복호화 할 수 있는지 알려주는게 아니라 이미 복호화를 마쳤는데 사용자가 read하지 않은 데이터가 SSL객체 내부버퍼에 존재하는지 확인하는 함수다.
그러니 rbio에 복호화 가능한 암호화된 데이터가 남아있다 해도 내부버퍼에 복호화 되어있는 데이터가 없으니 0을 리턴하고, do while이 의도대로 반복하지 않으니 decBuffer에 전송받은 모든 데이터가 담기지 않게 된다.
그래서 수신은 받았는데, 처리는 되지 않았던 것.
아래와 같이 수정하여 해결했다. Decrypt내부의 SSL_read가 한번에 하나의 레코드 단위로 복호화 하기 때문에 2개의 암호화된 패킷을 동시에 처리하려면 두번 호출되어야한다. 그래서 성공하면 반복해서 호출되도록 하고, 복호화 데이터 부족이나 실패했을 때 반복이 멈추도록 만들었다.
void Session::ProcessRecv(int32 numOfBytes)
{
/*...*/
bool repeat = true;
while (repeat)
{
uint8 ret = Decrypt(encBuffer, decBuffer);
switch (ret)
{
case 0: // 성공. 복호화 할 데이터 더 있을 수 있음. 반복해서 복호화 시도.
break;
case 1: // 복호화 데이터 부족
repeat = false;
break;
case 2: // 상대가 shutdown. shutdown 호출 가능
//TODO shutdown 정상종료
repeat = false;
break;
case 3: // 에러. shutdown 호출 불가능.
RegisterDisconnect();
repeat = false;
break;
}
}
/*...*/
}
이걸 금방찾아내지는 못했고, TLS세션을 붙이면 버그가 생기고 떼면 버그가 안생기길래 뭔가 TLS코드에 문제가 있다는것 정도는 알았는데, 이게 send에서 생기는 문제인지 recv에서 생기는 문제인지 encrypt, decrypt ... 워낙 확인해야할 지점이 많아서 해결하는데 엄청 오래 걸렸다.
그리고 TLS연결 OK 이후에 wbio에 적힌게 있으면 보내는 부분이 있었는데, 문제는 TLS연결 OK가 뜨면 즉시 recv이벤트를 TLSHandshakeRecv에서 일반 Recv로 변경하기 때문에 SSL_connect를 호출하지 않는게 문제되지 않는건가? 하고 확인해봤다.
그런데 다행히도 이건 OK이후에 들어온 데이터를 rbio에 넣어놓은 상태로 SSL_read를 호출하면 알아서 post-handshake 메시지를 처리해준다고 한다.
버그를 해결하려고 클라이언트쪽에서 로그를 찍으면서 찾아보다가 510바이트의 암호화데이터가 recv됐는데 0바이트가 복호화되는 경우가 있어서 문제가 있는건가? 하고 알아봤는데 상기한 이유로 문제는 아니었다.
현재까지의 git 버전
Test: 클라이언트 5명으로 늘림. room 2개 사용하도록 변 · Dodontak/Project_Island_GameServer@dab2b4d
@@ -24,7 +24,7 @@ void Handle_S_LOGIN(const PacketSessionRef& session, const Protocol::S_LOGIN& pk
github.com
'프로젝트 > Project_Island' 카테고리의 다른 글
| 43. JobQueue(2) - GlobalQueue (0) | 2026.04.17 |
|---|---|
| 42. JobQueue(1) (0) | 2026.04.16 |
| 40. 외부 라이브러리들 dll -> lib 변경 (0) | 2026.04.13 |
| 39. DBConnection, Pool 만들기 (0) | 2026.04.09 |
| 38. TLS 적용하기 (0) | 2026.04.05 |