[C++]SocketUtils

강병우·2023년 8월 28일

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버를 참고하여 정리한 내용입니다.

지금까지 소켓 프로그래밍을 다뤄보았다. 하지만 실제로 소켓 코드 자체를 사용하기엔 꽤 복잡하다. 그래서 C계열 네트워크 프로그래밍을 할 때 네트워크 라이브러리를 사전에 작성한다고 한다. 이번엔 Socket 관련 유틸 라이브러리를 작성해보고자 한다.

쉽게 말하자면, 우리가 직접 프레임워크를 만든다고 보면 된다.

Accept부터 bind, listen 등을 외부 라이브러리로 참조하게끔 작성하면 된다.

NetAddress : 주소 정보를 담고 있는 클래스이다.

  • 아이피/포트 정보를 넘겨준다.

NetAddress.h

#pragma once
/*
	NetAddress
*/

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

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

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

private:
	SOCKADDR_IN _sockAddr = {};
};

NetAddress.cpp

#include "pch.h"
#include "NetAddress.h"

NetAddress::NetAddress(SOCKADDR_IN 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::GetAddress()
{
	WCHAR buffer[100];
	::InetNtopW(AF_INET, &_sockAddr.sin_addr, buffer, len32(buffer)));	//Types.h에 매크로 정의.
	return wstring(buffer);
}

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

이렇게 주소 정보를 만들었다면, 이를 받고 연결하거나 리슨을 받는 소켓파트를 작성한다.

SocketUtils : 소켓 관련 클래스이다.

SocketUtils.h

#pragma once
#include "NetAddress.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));
};

SocketUtils.cpp

#include "pch.h"
#include "SocketUtils.h"

/* ---------------------
		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_linger = linger;
	option.l_onoff = onoff;
	
	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)
{
	SOCKADDR_IN addr = netAddr.GetSockAddr();
	return SOCKET_ERROR != ::bind(socket, reinterpret_cast<const SOCKADDR*>(&addr), 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;
}

SetLinger : 소켓이 닫힌 이후 전송되지 않은 데이터를 어떻게 처리할 지 설정. 0으로 설정할 경우, 모두 버린다. 만약 켜져 있는 경우, 일정 시간동안 데이터를 받고 소켓을 닫는다.
SetReuseAddress : 다른 소켓이 포트로 다시 바인딩할 수 있게 한다.
SetRecv/SendBufferSize : 소켓 통신에 사용될 송/수신 버퍼 크기를 설정한다. 알아둬야할 것이, 송신버퍼는 수신 버퍼의 크기보다 작게 설정된다.
SetTcpNoDelay : 이 설정이 켜져있을 경우, 가능한 빨리 데이터를 전송한다. 느려지는 이유는 현재로선 잘 모르겠다.
SetUpdateAcceptSocket : 이 설정이 켜져있을 경우, ListenSocket의 특성을 ClientSocket에 그대로 적용한다.

SetSockOpt를 통해 소켓 옵션을 조정할 수 있다.

int WSAAPI setsockopt(
  [in] SOCKET     s,		// 소켓을 식별하는 설명자
  [in] int        level,	// 옵션 (SOL_SOCKET만 사용)
  [in] int        optname,	// 값을 설정할 소켓 옵션
  [in] const char *optval,	// 요청된 옵션의 값이 지정된 버퍼에 대한 포인터
  [in] int        optlen	// 포인터의 크기
);

이렇게 작성했다면, main에서 이렇게 사용할 수 있다.

SOCKET socket = SocketUtils::CreateSocket();
	SocketUtils::BindAnyAddress(socket, 7777);
	SocketUtils::Listen(socket);

	SOCKET clientSocket = ::accept(socket, nullptr, nullptr);
	cout << "Client Connected!" << endl;

BindAnyaddress에서 받아온 인자로 NetAddress객체를 생성하여 바인딩한 뒤, 리슨을 시작한다. 사실 여기서 무한루프로 계속 클라이언트를 받아줘야 하는데, 테스트용으로 accept를 한 번만 시행했다. 클라이언트측에서 WSASocket 연결을 하면 성공적으로 연결된다.


출처
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
마이크로소프트 docs(setsockopt함수)

0개의 댓글