KoreanFoodie's Study

[C++ 게임 서버] 4-1. Socket Utils 본문

Game Dev/Game Server

[C++ 게임 서버] 4-1. Socket Utils

GoldGiver 2023. 10. 25. 23:50

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 4-1. Socket Utils

핵심 :

1. IOCP 서버 구축을 위해, ::InetNtopW, ::setsockopt 함수 등을 활용하여 필요한 네트워크 함수들을 Wrapping 한 헬퍼 함수들을 만들어 보자.

2. 소켓을 사용할 때, Connect/Disconnect/Accept 에 해당하는 함수 포인터를 만들고, 소켓과 바인딩하여 해당 주소를 얻어 와 간편하게 사용할 수 있다. ::WSAIoctl 함수를 참고하자.

이번 4 챕터는, 네트워크 라이브러리를 제작하는 부분이다. 사실 이번 챕터는 조금 생소해 보이는 API 들을 사용하면서, 앞으로 구축할  IOCP 서버를 위한 밑작업을 하는 부분이다.

그러니 낯설어 보이는 녀석들이 많이 보이더라도 '그려려니...' 하면서, 문서를 보면서 대략적인 감(?) 을 익히는 것에 집중하자! 절대 귀찮아서 그런 것이 아니다 😅

 

먼저, Socket 을 사용함에 있어 앞으로 유용하게 활용할 헬퍼 함수들을 만들어 보자.

먼저, 소켓을 받아 소켓/IP 주소나 포트 번호를 반환하는 클래스를 만들어 볼 것이다.

NetAddress.h

class NetAddress
{
public:
	NetAddress() = default;
	NetAddress(SOCKADDR_IN sockAddr);
	NetAddress(wstring ip, uint16 port);

	SOCKADDR_IN&	GetSockAddr() { return _sockAddr; }
	wstring			GetIpAddress();
	uint16			GetPort() { return ::ntohs(_sockAddr.sin_port); }

public:
	static IN_ADDR	Ip2Address(const WCHAR* ip);

private:
	SOCKADDR_IN		_sockAddr = {};
};

 

NetAddress.cpp

NetAddress::NetAddress(SOCKADDR_IN sockAddr) : _sockAddr(sockAddr)
{
}

NetAddress::NetAddress(wstring ip, uint16 port)
{
	::memset(&_sockAddr, 0, sizeof(_sockAddr));
	_sockAddr.sin_family = AF_INET;
	_sockAddr.sin_addr = Ip2Address(ip.c_str());
	_sockAddr.sin_port = ::htons(port);
}

wstring NetAddress::GetIpAddress()
{
	WCHAR buffer[100];
	::InetNtopW(AF_INET, &_sockAddr.sin_addr, buffer, len32(buffer));
	return wstring(buffer);
}

IN_ADDR NetAddress::Ip2Address(const WCHAR* ip)
{
	IN_ADDR address;
	::InetPtonW(AF_INET, ip, &address);
	return address;
}

/** 참고 : len32 는 다음과 같이 정의됨 */
#define size32(val)		static_cast<int32>(sizeof(val))
#define len32(arr)		static_cast<int32>(sizeof(arr)/sizeof(arr[0]))

사실 코드를 잘 뜯어보면, 이전에 작업한 내용들의 연장선상에 있다는 것을 알 수 있다.

조금 낯설어 보이는 것은 InetNtopW 인데...

문서를 보면, 그냥 IP 주소를 문자열로 반환하는 간단한 API 임을 알 수 있다. 아, 참고로 InetPtonW 는 값을 이진 형식으로 반환하는 녀석이다. 

 

이제 위 NetAddress 를 활용하는 SoketUtils 라는 클래스를 만들 것이다.

SoketUtils.h

/*----------------
	SocketUtils
-----------------*/

class SocketUtils
{
public:
	static LPFN_CONNECTEX		ConnectEx;
	static LPFN_DISCONNECTEX	DisconnectEx;
	static LPFN_ACCEPTEX		AcceptEx;

public:
	static void Init();
	static void Clear();

	static bool BindWindowsFunction(SOCKET socket, GUID guid, LPVOID* fn);
	static SOCKET CreateSocket();

	static bool SetLinger(SOCKET socket, uint16 onoff, uint16 linger);
	static bool SetReuseAddress(SOCKET socket, bool flag);
	static bool SetRecvBufferSize(SOCKET socket, int32 size);
	static bool SetSendBufferSize(SOCKET socket, int32 size);
	static bool SetTcpNoDelay(SOCKET socket, bool flag);
	static bool SetUpdateAcceptSocket(SOCKET socket, SOCKET listenSocket);

	static bool Bind(SOCKET socket, NetAddress netAddr);
	static bool BindAnyAddress(SOCKET socket, uint16 port);
	static bool Listen(SOCKET socket, int32 backlog = SOMAXCONN);
	static void Close(SOCKET& socket);
};

template<typename T>
static inline bool SetSockOpt(SOCKET socket, int32 level, int32 optName, T optVal)
{
	return SOCKET_ERROR != ::setsockopt(socket, level, optName, reinterpret_cast<char*>(&optVal), sizeof(T));
}

일단 시작부터 정신이 혼미해질 것 같다. 하지만 걱정하지 마라. Alt + G 는 F12 보다 빠르니까.

먼저, LPFN_CONNECTEX ... 녀석의 정의를 보면, 대충 아래와 같이 생겼다는 것을 확인할 수 있다 :

typedef
BOOL
(PASCAL FAR * LPFN_CONNECTEX) (
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR *name,
    _In_ int namelen,
    _In_reads_bytes_opt_(dwSendDataLength) PVOID lpSendBuffer,
    _In_ DWORD dwSendDataLength,
    _Out_ LPDWORD lpdwBytesSent,
    _Inout_ LPOVERLAPPED lpOverlapped
    );

#define WSAID_CONNECTEX \
    {0x25a207b9,0xddf3,0x4660,{0x8e,0xe9,0x76,0xe5,0x8c,0x74,0x06,0x3e}}

으음.. 여기서 알 수 있는 것은 무엇일까? ConnetEx, DisconnectEx, AcceptEx 은 그냥 Function Pointer 이라는 것이다! 우리는 이것을 추후 런타임에서 불러와 사용할 것이다. 😁

헤더 파일에는 그 외에도, 이름만 읽었을 때 앞으로 유용하게 사용할 헬퍼 함수들이 다수 들어 있다는 것을 확인할 수 있다.

 

이제 대망의 SocketUtils 구현부를 보자.

SocketUtils.cpp

/*----------------
	SocketUtils
-----------------*/

LPFN_CONNECTEX		SocketUtils::ConnectEx = nullptr;
LPFN_DISCONNECTEX	SocketUtils::DisconnectEx = nullptr;
LPFN_ACCEPTEX		SocketUtils::AcceptEx = nullptr;

void SocketUtils::Init()
{
	WSADATA wsaData;
	ASSERT_CRASH(::WSAStartup(MAKEWORD(2, 2), OUT &wsaData) == 0);
	
	/* 런타임에 주소 얻어오는 API */
	SOCKET dummySocket = CreateSocket();
	ASSERT_CRASH(BindWindowsFunction(dummySocket, WSAID_CONNECTEX, reinterpret_cast<LPVOID*>(&ConnectEx)));
	ASSERT_CRASH(BindWindowsFunction(dummySocket, WSAID_DISCONNECTEX, reinterpret_cast<LPVOID*>(&DisconnectEx)));
	ASSERT_CRASH(BindWindowsFunction(dummySocket, WSAID_ACCEPTEX, reinterpret_cast<LPVOID*>(&AcceptEx)));
	Close(dummySocket);
}

void SocketUtils::Clear()
{
	::WSACleanup();
}

bool SocketUtils::BindWindowsFunction(SOCKET socket, GUID guid, LPVOID* fn)
{
	DWORD bytes = 0;
	return SOCKET_ERROR != ::WSAIoctl(socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), fn, sizeof(*fn), OUT & bytes, NULL, NULL);
}

SOCKET SocketUtils::CreateSocket()
{
	return ::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
}

bool SocketUtils::SetLinger(SOCKET socket, uint16 onoff, uint16 linger)
{
	LINGER option;
	option.l_onoff = onoff;
	option.l_linger = linger;
	return SetSockOpt(socket, SOL_SOCKET, SO_LINGER, option);
}

bool SocketUtils::SetReuseAddress(SOCKET socket, bool flag)
{
	return SetSockOpt(socket, SOL_SOCKET, SO_REUSEADDR, flag);
}

bool SocketUtils::SetRecvBufferSize(SOCKET socket, int32 size)
{
	return SetSockOpt(socket, SOL_SOCKET, SO_RCVBUF, size);
}

bool SocketUtils::SetSendBufferSize(SOCKET socket, int32 size)
{
	return SetSockOpt(socket, SOL_SOCKET, SO_SNDBUF, size);
}

bool SocketUtils::SetTcpNoDelay(SOCKET socket, bool flag)
{
	return SetSockOpt(socket, SOL_SOCKET, TCP_NODELAY, flag);
}

// ListenSocket의 특성을 ClientSocket에 그대로 적용
bool SocketUtils::SetUpdateAcceptSocket(SOCKET socket, SOCKET listenSocket)
{
	return SetSockOpt(socket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, listenSocket);
}

bool SocketUtils::Bind(SOCKET socket, NetAddress netAddr)
{
	return SOCKET_ERROR != ::bind(socket, reinterpret_cast<const SOCKADDR*>(&netAddr.GetSockAddr()), sizeof(SOCKADDR_IN));
}

bool SocketUtils::BindAnyAddress(SOCKET socket, uint16 port)
{
	SOCKADDR_IN myAddress;
	myAddress.sin_family = AF_INET;
	myAddress.sin_addr.s_addr = ::htonl(INADDR_ANY);
	myAddress.sin_port = ::htons(port);

	return SOCKET_ERROR != ::bind(socket, reinterpret_cast<const SOCKADDR*>(&myAddress), sizeof(myAddress));
}

bool SocketUtils::Listen(SOCKET socket, int32 backlog)
{
	return SOCKET_ERROR != ::listen(socket, backlog);
}

void SocketUtils::Close(SOCKET& socket)
{
	if (socket != INVALID_SOCKET)
		::closesocket(socket);
	socket = INVALID_SOCKET;
}

 

 

생각보다 긴데, 쫄 필요는 없다.

먼저 Init 에서는 소켓에 ConnectEx, DisconnectEx, AcceptEx 함수를 바인딩한다. 이때 WSAIoctl 라는 함수를 사용하는데... 설명은 다음과 같다 :

 

또한 ::setsockopt 함수를 이용해, 소켓의 여러 옵션을 세팅해 주고 있다!

일단 너무 디테일한 부분에 집착하지 말고, 큰 그림을 보면서 길을 잃지 않도록 하자 😊

Comments