지난번에 메시지를 쌓아서 한번에 보내는 바람에 원래는 2개로 인식되어야할 메시지가 한덩어리로 가는 문제(?)를 발견했다. 그리고 경험하지는 않았지만 원래 TCP 특성 상 100바이트를 보내도 50바이트만 먼저 도착하는 경우도 발생할 수 있다.
그래서 "여기서부터 100바이트만큼이 하나의 메시지다" 하는 헤더를 항상 메시지의 맨 앞에 추가해서 패킷을 받는 입장에서 어디서부터 어디까지가 한덩어리의 메시지인지 알 수 있도록 만들자.
먼저 지금 ProcessRecv의 동작을 살펴보자.
void Session::ProcessRecv(int32 numOfBytes)
{
_recvEvent.Clear();
if (numOfBytes == 0) // 클라이언트가 정상적으로 연결을 종료한 경우
{
RegisterDisconnect();
return;
}
_recvBuffer.OnWrite(numOfBytes);
SendBufferRef sendBuffer = make_shared<SendBuffer>(_recvBuffer.ReadPos(), numOfBytes);
_recvBuffer.OnRead(numOfBytes);
_recvBuffer.Clean();
if (dynamic_pointer_cast<ClientService>(_service.lock()))
{//더미 클라이언트 서비스라면.
string str((char*)sendBuffer->GetBuffer(), sendBuffer->GetDataLen());
Utils::LockPrint("recv from server : ", str);
RegisterRecv();
return;
}
if (ServiceRef service = _service.lock())
{
service->broad_cast_test(sendBuffer);
}
RegisterRecv();
}
지금은 recvBuffer에 있는 데이터를 전부 sendBuffer에 담은 다음, service->broad_cast_test 함수로 모든 session에 send해버린다.
원하는 동작은 이렇다.
- recv한 데이터가 패킷 헤더크기는 되는지 확인. 안되면 return하고 다음 recv에서 처리해야됨.
- recv한 데이터가 패킷에 적힌 size 만큼은 되는지 확인.(헤더크기 포함) 안되면 return하고 다음 recv에서 처리해야됨.
- 최소 1개의 온전한 패킷이 왔으니, 해당 패킷을 처리. (지금은 채팅서버이니 새 Header+data SendBuffer를 만들어 broad_cast 해주면 될 것 같다.)
- recvBuffer.OnRead로 recvBuffer에서 size만큼 썼음을 기록.
- 1~4 반복 (recvBuffer에 패킷 여러개 와 있을 있을 수도 있으니까)
Session
PacketHeader, PacketSession 추가
struct PacketHeader
{
uint16 id;
uint16 size;
};
class PacketSession : public Session
{
public:
PacketSession(SOCKET socket) : Session(socket) {}
virtual ~PacketSession() {}
virtual uint32 OnRecv(BYTE* buffer, uint32 len) sealed;
virtual void OnRecvPacket(BYTE* buffer, uint32 size) abstract;
};
uint32 PacketSession::OnRecv(BYTE* buffer, uint32 len)
{
uint32 processLen = 0;
while (true)
{
uint32 dataLen = len - processLen;
if (dataLen < sizeof(PacketHeader))
break;
PacketHeader* header = reinterpret_cast<PacketHeader*>(&buffer[processLen]);
if (dataLen < header->size)
break;
OnRecvPacket(&buffer[processLen], header->size);
processLen += header->size;
}
return processLen;
}
OnRecv는 Session에 추상함수로 추가해준다. 그리고 ProcessRecv에서 아래와 같이 사용해준다.
void Session::ProcessRecv(int32 numOfBytes)
{
_recvEvent.Clear();
if (numOfBytes == 0) // 클라이언트가 정상적으로 연결을 종료한 경우
{
RegisterDisconnect();
return;
}
_recvBuffer.OnWrite(numOfBytes);
int processLen = OnRecv(_recvBuffer.ReadPos(), _recvBuffer.DataSize());
_recvBuffer.OnRead(processLen);
_recvBuffer.Clean();
RegisterRecv();
}
OnRecv에서 패킷단위로 짤라서 일처리를 해주고, 총 처리한 바이트를 리턴한다.
OnRecv 안에서 패킷하나씩 OnRecvPacket을 실행시키는데, 서버랑 더미클라랑 서로 다른 동작을 하게 하려고 이렇게 만들었다. 각각 GameSession(서버에서 쓰는 클라이언트용 세션), ServerSession(더미클라에서 쓰는 서버용 세션).
GameSession
class GameSession : public PacketSession
{
public:
GameSession(SOCKET socket) : PacketSession(socket) {}
~GameSession() {}
virtual void OnRecvPacket(BYTE* buffer, uint32 size) override;
};
void GameSession::OnRecvPacket(BYTE* buffer, uint32 size)
{
uint16 headerSize = sizeof(PacketHeader);
SendBufferRef sendBuffer = Utils::MakeChatSendBuffer(1, buffer + headerSize, size - headerSize);
if (ServiceRef service = _service.lock())
{
service->broad_cast_test(sendBuffer);
}
}
클라이언트로부터 채팅 패킷이 오면 새 채팅패킷을 SendBuffer에 담고, 모든 접속한 클라에 뿌려준다.
버퍼 만드는건 임시로 Utils에 MakeChatSendBuffer를 만들어 사용한다. 나중에 패킷 핸들러 만들면 버릴 함수.
ServerSession
class ServerSession : public PacketSession
{
public:
ServerSession(SOCKET socket) : PacketSession(socket) {}
~ServerSession() {}
virtual void OnRecvPacket(BYTE* buffer, uint32 size) override;
};
void ServerSession::OnRecvPacket(BYTE* buffer, uint32 size)
{
uint16 headerSize = sizeof(PacketHeader);
string str((char*)(buffer + headerSize), size - headerSize);
Utils::LockPrint("recv from server : ", str);
}
서버로부터 패킷이 오면 그걸 출력한다.
Service
게임서버와 더미클라가 서로 다른 Session으로 분기되었으니 Service의 CreateSession에서 리턴되는 SessionRef도 바꿔줘야한다. sessionFactory를 저장해서 make_shared 대신에 사용한다. 세션팩토리는 Service를 생성할 때 람다함수로 넣어준다.
using SessionFactory = function<SessionRef(SOCKET)>;
class Service : public std::enable_shared_from_this<Service>
{
public:
/*...*/
SessionRef CreateSession();
/*...*/
protected:
/*...*/
SessionFactory _sessionFactory;
};
SessionRef Service::CreateSession()
{
SOCKET socket = SocketUtils::CreateSocket();
if (socket == INVALID_SOCKET)
return nullptr;
//SessionRef session = make_shared<Session>(socket);
SessionRef session = _sessionFactory(socket);// 이렇게 사용
if (session == nullptr)
{
SocketUtils::CloseSocket(socket);
return nullptr;
}
session->_service = shared_from_this();
return session;
}
게임서버 main에서 Service 생성할 때
int main()
{
/*...*/
ServiceRef service = make_shared<ServerService>(
NetAddress("0.0.0.0", 7777),
[](SOCKET socket) {
return make_shared<GameSession>(socket);
}
);
/*...*/
}
더미클라 main에서 Service 생성할 때
int main()
{
/*...*/
ClientServiceRef service = make_shared<ClientService>(
NetAddress("127.0.0.1", 7777),
[](SOCKET socket) {
return make_shared<ServerSession>(socket);
},
10
);
/*...*/
}
그외로 더미클라에서 메시지를 보낼때도 패킷에 헤더를 달아서 보내야 하니까 그 부분도 수정해준다.
int main()
{
cout << "=== DummyClient ===" << endl;
/*...*/
this_thread::sleep_for(chrono::seconds(1));
string msg = "Hello Iocp Server!";
service->broad_cast_test(Utils::MakeChatSendBuffer(0, (BYTE*)msg.data(), msg.length()));
}
테스트

이전에는 한번에 여러개의 메시지를 뭉쳐서 보내면 문구가 한번에 겹쳐서 나오는 경우가 있었는데 그런 경우가 사라졌다.
게임서버는 다양한 패킷을 주고받아야 하는데 지금은 채팅패킷 하나 뿐이다.
다양한 패킷을 쉽게 생성, 직렬화, 역직렬화 하기위해 다음에는 Protobuf를 추가하자.
현재까지의 git 버전
Feat: PacketHeader PacketSession · Dodontak/Project_Island_GameServer@2141e6b
Session - Packet처리를 할 PacketSession 추가. Session을 상속받음. - PacketSession의 OnRecv에서 패킷 헤더를 해석해 충분한 양의 데이터가 recv 됐는지 확인하고 진행함. GameSession, ServerSession - 각각 서버측, 더
github.com
'프로젝트 > Project_Island' 카테고리의 다른 글
| 36. PacketHandler추가, 코드 자동화 (0) | 2026.04.03 |
|---|---|
| 35. Protobuf 추가하기 (0) | 2026.04.02 |
| 33. 서버가 send를 너무 많이 하는 문제 해결 (0) | 2026.04.02 |
| 32. 세션 소멸 안되는 문제 해결 (0) | 2026.04.01 |
| 31. 더미 클라이언트 (0) | 2026.03.31 |