dh_0e

[C++ Server] PacketSession 본문

C++ Server/Framework

[C++ Server] PacketSession

dh_0e 2026. 5. 7. 11:37

PacketSession

  • Session 클래스(TCP 소켓의 기본 송수신을 담당)를 상속받아 패킷 조립 기능을 얹은 클래스
  • TCP 특성상 패킷이 모두 도착했다는 보장을 할 수가 없는 TCP 데이터 경계 문제(TCP Boundary Problem)를 해결
    • TCP는 스트림(Stream) 기반이라 데이터의 경계가 없음
    • 패킷이 쪼개져서 오기도 하고(Fragmentation), 뭉쳐서 오기도 함(Sticky Packets)
struct PacketHeader {
	uint16 size;
	uint16 id;
};
  • size(2Byte) + id(2Byte)로 구성된 헤더를 만듦
    • size: 패킷 전체 크기 (header + data)
    • id: 프로토콜 ID (ex. 1=로그인, 2=회원가입)
  • OnRecv (final)
    • 네트워크 I/O 스레드에서 실제 데이터를 수신했을 때 호출되는 함수
    • 역할
      • 패킷 쪼개짐/뭉침 문제를 모두 처리
      • 받은 데이터 버퍼를 읽으면서 헤더의 size만큼 데이터가 다 모였는지 확인
        • 다 모이지 않았다면 다음 수신을 기다림
        • 다 모였다면 온전한 하나의 패킷을 잘라내어 OnRecvPacket() 호출
    • final: 하위 클래스에서 실수로 재정의하는 것을 방지
  • OnRecvPacket (abstract)
    • OnRecv에서 온전한 1개의 패킷이 조립되었을 때마다 호출
    • 역할
      • 클래스를 상속받을 실제 게임 세션에서 컨텐츠 로직을 구현하는 곳 (원래 OnRecv 역할)
      • 넘어온 데이터가 무조건 결함 없는 1개의 완벽한 패킷이기 때문에, 헤더의 id를  읽고 컨텐츠 처리
  • GetPacketSessionRef
    • 멀티스레드 환경에서 세션 객체의 생명 주기를 안전하게 관리하기 위해 shared_from_this 반환

 

Session.h

/*--------------------
	PacketSession
--------------------*/
// TCP 특성상 패킷이 모두 도착했다는 보장을 할 수가 없어서 이를 처리하는 세션

struct PacketHeader {
	uint16 size;
	uint16 id;
};

// [size(2Byte)][id(2Byte)][data...]
// size: 패킷 전체 크기 (header + data)
// id: 프로토콜 ID (ex. 1=로그인, 2=회원가입)

class PacketSession : public Session {
public:
	PacketSession();
	virtual ~PacketSession();

	PacketSessionRef GetPacketSessionRef() { return static_pointer_cast<PacketSession>(shared_from_this()); }

protected:
	virtual int32 OnRecv(BYTE* buffer, int32 len) final;
	virtual int32 OnRecvPacket(BYTE* buffer, int32 len) abstract;
};

 

Session.cpp

/*--------------------
	PacketSession
--------------------*/

PacketSession::PacketSession() {}

PacketSession::~PacketSession() {}

int32 PacketSession::OnRecv(BYTE* buffer, int32 len) {
	int32 processLen = 0;
	while (true) {
		int32 dataSize = len - processLen;
		// 최소한 헤더는 파싱할 수 있는지
		if (dataSize < sizeof(PacketHeader))
			break;

		PacketHeader header = *(reinterpret_cast<PacketHeader*>(&buffer[processLen]));
		// 헤더에 기록된 패킷 크기를 파싱할 수 있는지
		if (dataSize < header.size)
			break;

		// 패킷 조립 성공
		OnRecvPacket(&buffer[processLen], header.size);

		processLen += header.size;
	}
	return processLen;
}

 

 

DummyClient.cpp / GameServer.cpp / GameSession.h / GameSession.cpp / Types.h 수정

 

PacketSession, Disconnect Logic Debug · znfnfns0365/CPP_Server_Practice@c305ec7

+ MakeShared<ClientService>(NetAddress(L"127.0.0.1", 7777), MakeShared<IocpCore>(), MakeShared<ServerSession>, 1000);

github.com

  • DummyClient.cpp
    • OnRecv를 OnRecvPacket으로 바꾸고, OnRecv에서 받은 패킷을 복사, 출력
    • 클라이언트 1000개를 만들어 서버 연결 시도
  • GameServer.cpp / GameSession.cpp: Broadcast를 GameServer에서 주기적으로(250ms) 보내도록 수정 
  • GameSession.h: GameSession이 PacketSession을 상속, OnRecv가 아닌 OnRecvPacket을 재정의 하도록 수정
  • Types.h: PacketSessionRef 선언

 

Session.cpp 수정 (Line 43~43 → Line 230~232)

  • 원래 코드에서 Client를 1000개로 늘려서 실행 중 Client를 접속 해제(종료)하면 CRASH 발생

  • 이유: 원래 코드의 로직으로 GameSession::OnRecvPacket에서 Send 처리 중 클라이언트가 접속 해제 시
    1. Session::RegisterSend()에서 WSASend 중 HandleError 발생
    2. Session::HandleError()에서 Disconnect(L"HandleError"); 호출
    3. Session::Disconnect()에서 OnDisconnected() 호출
    4. GameSession::OnDisconnected()에서 세션 삭제
      • Send 처리 중 세션을 삭제한다는 뜻 → CRASH (Dangling Pointer) 발생

  • 다음과 같이 OnDisconnected() 호출 부분 ProcessDisconnect()로 옮겨서 GameSession이 소멸된 이후에 세션을 삭제하게끔 만들어줌
    • 기존 (위험): Send 하다가 에러 발생 → 그 즉시 ReleaseSession 호출 → 메모리 해제
    • 수정 (안전): 에러 발생 → RegisterDisconnect (끊어달라고 통보만 함) → 나중에 IOCP가 완료 신호를 주면 ProcessDisconnect에서 정리

클라이언트 세션 1000개가 잘 소멸됨