나만의 작은 도서관
[TIL][C++] 250425 MMO 서버 개발 5일차: 구조체 선언과 포인터, 그리고 Windows API 명명 규칙, GetQueuedCompletionStatus(), 기타 고민거리 본문
Today I Learn
[TIL][C++] 250425 MMO 서버 개발 5일차: 구조체 선언과 포인터, 그리고 Windows API 명명 규칙, GetQueuedCompletionStatus(), 기타 고민거리
pledge24 2025. 4. 26. 02:40주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
구조체 선언과 포인터, 그리고 Windows API 명명 규칙
- Winsock 라이브러리를 보다보면 아래와 같이 구조체 정의 뒤에 포인터 타입의 이름이 명명된걸 볼 수 있다.
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
- 위 코드는 두 가지 타입 정의를 동시에 선언하는 방식으로 아래 코드와 동일한 효과를 갖는다.
typedef struct _OVERLAPPED { ... } OVERLAPPED;
typedef OVERLAPPED* LPOVERLAPPED;
- 즉, OVERLAPPED 구조체를 포인터 타입으로 변환하고자 한다면 LPOVERLAPPED 타입을 사용하면 된다.
OVERLAPPED overlapped;
LPOVERLAPPED lpoverlapped = &overlapped;
// LPOVERLAPPED* 타입을 매개변수로 받음
bool ret = ::GetQueuedCompletionStatus(..., &lpoverlapped, ...);
GetQueuedCompletionStatus()
- GQCS함수는 반환 시, 단 하나의 완료 패킷에 대한 정보만 넘겨준다. 즉, GQCS 호출당 완료 패킷 하나가 처리된다는 것이다.
- CP에 존재하는 여러 완료 패킷을 pop하는 것이 더 효율적이지 않을까 싶지만, 하나씩 처리하는게 IOCP와 같은 고성능 서버에선 더 안전하고 확장성이 높다고 한다.
- 여러 완료 패킷을 하나의 쓰레드 pop해가면 특정 쓰레드에게 일감이 몰리는 현상이 발생할 수 있다. → 균등하지 못한 작업 수행이 발생
GetQueuedCompletionStatus 매개변수
- 일감을 받을 iocp 핸들
- 완료된 입출력 연산의 바이트 수
- 클라이언트 소켓시 등록했던 키값. 안넣어도 상관없지만 세션을 넣어줘서 쓰기도 한다.
- 내부에서 사용하는 OVERLAPPED 구조체 주소. 추가 데이터를 덧대서 입출력 타입(IO_TYPE) 데이터를 추가한다.
- 대기시간. timeout이라고 생각하면 된다.
구성(Composition)과 상속(Inheritance) 중 어떤걸 골라야할까
- 어느 클래스에 다른 클래스의 정보가 포함되어 있어야 할 경우, 멤버 클래스 형식인 포함 관계(has-a)와 상속 관계(is-a)중 하나를 고를 수 있다.
class OverlappedEx
{
OVERLAPPED overlapped;
IO_TYPE type;
}
class OverlappedEx : public OVERLAPPED
{
IO_TYPE type;
}
- 오늘 고민한 내용은 위 코드처럼 ‘OVERLAPPED 구조체를 들고 있는 확장 클래스를 만드는 두 가지 방식중 어느 방식으로 골라야 할까?’ 였다.
- 여러 의미로 상속이 훨씬 좋아보여서 상속을 선택했다. 왜냐하면 이름도 OverlappedEx로 OVERLAPPED 구조체가 확장되었다는 의미이기 때문에 상속의 의미를 보다 명확히 할 수 있었기 때문이었다.
- 하지만, 일반적으로 객체지향 설계에서는 “상속보단 구성을 선호”하는 원칙이 권장되어서 구성이 더 권장된다고 한다. 나중에 오늘과 같은 문제가 다시 발생하면 다시 고려해봐야겠다.
멤버 클래스는 포인터로 들고 있어야할까? 아니면 객체 전체를 들고 있어야할까?
- 객체 전체를 들고 있게되면 객체 생성 시 자동으로 멤버 클래스 객체 또한 생성이됨.
- ⇒ 이게 좀 관리가 어려울 수 있음(포인터라는 개념이 있는데 굳이? 라는 생각이 듬)
- 포인터로 들고있으면 원하는 시점에 객체를 생성할 수 있음.
- ⇒ 근데 이게 “원하는 시점”이라는 것 때문에, 객체를 생성해도 코드를 누락하면 멤버 클래스가 비어있을 수 있음(요거는 조심해야함)\
결론
- 멤버 클래스는 포인터로 들고 있돼, 신중하게 그 시점을 결정해야한다.
??
- 메인 쓰레드에서 서버 서비스를 만들고, 만든 서비스를 각 워커 쓰레드에게 뿌린다.
- IOCP가 아니라면 이게 맞을까?
Session과 Listener를 하나의 인터페이스로 묶어서 관리해야할까?
- Listener는 서버 주소와 바인딩된 ListenSocket을 가지며, 새로운 클라이언트의 Connect요청을 Accept하는 역할을 한다.
- Session은 클라이언트와 연결된 소켓을 가지며, 연결된 클라이언트와의 네트워크 이벤트 중 Send, Recv, Disconnect를 처리하는 역할을 한다.
- 이 둘의 공통점은 하나의 소켓을 가지며 해당 소켓으로 들어오는 네트워크 이벤트를 처리한다는 것이다.그래서 이 둘을 하나의 인터페이스로 묶어 일관성을 부여하는 것이 좋을지 고민이다.
class IocpObject
{
public:
virtual HANDLE GetHandle() abstract;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) abstract;
private:
SOCKET _socket;
}
class Listener : public IocpObject {}
class Session : public IocpObject {}