상세 컨텐츠

본문 제목

네트워크 라이브러리 제작(Socket Utils)

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 11. 27. 14:19

본문

이전에 학습한 네트워크 기본 지식 + 함수를 바탕으로 네트워크 라이브러리를 제작해보자

CorePch.h

...
#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
...

먼저 앞으로 사용할 매크로 함수를 정의하자. size 와 array의 len을 반환하는 함수다.

Types.h

...
#define size16(val) static_cast<int16>(sizeof(val))
#define size32(val) static_cast<int32>(sizeof(val))
#define len16(arr) static_cast<int16>(sizeof(arr)/sizeof(arr[0]))
#define len32(arr) static_cast<int32>(sizeof(arr)/sizeof(arr[0]))
...

 

클라이언트 정보(주소 등)를 담을 수 있는 자료구조를 만든다

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 GetIpAddress();
    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::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;
}

 

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 backing = 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_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 backing)
{
    return SOCKET_ERROR != ::listen(socket, backing);
}

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

 

지금까지 만든 모듈을 테스트 해보자.

GameServer.cpp

...
#include "SocketUtils.h"

int main()
{
	SOCKET socket = SocketUtils::CreateSocket();

	SocketUtils::BindAnyAddress(socket, 7777);

	SocketUtils::Listen(socket);

	SOCKET clientSocket = ::accept(socket, nullptr, nullptr);

	cout << "Client Connected!" << endl;

	GThreadManager->Join();
}

GThreadManager Join 콜 한 것은 프로젝트 다른 곳에서 CoreGlobal을 사용하지 않아 Init이 되지 않는 상황을 방지 하기 위해서다.

 

전역 Scope에서 SocketUtils를 초기화 해준다.

CoreGlobal.cpp

...
#include "SocketUtils.h"

ThreadManager* GThreadManager = nullptr;
Memory* GMemory = nullptr;
DeadLockProfiler* GDeadLockProfiler = nullptr;

class CoreGlobal
{
public:
    CoreGlobal()
    {
        GThreadManager = new ThreadManager();
        GMemory = new Memory();
        GDeadLockProfiler = new DeadLockProfiler();
        SocketUtils::Init();
    }
    ~CoreGlobal()
    {
        delete GThreadManager;
        delete GMemory;
        delete GDeadLockProfiler;
        SocketUtils::Clear();
    }
} GCoreGlobal;

 

1. BindWindowsFunction 함수가 하는 일은?

- win socket에서 사용할 함수 포인터 얻어옴.

2. Bind와 BindAnyAddress 차이는?

- 특정 ip 사용 또는 아무 ip 주소나 사용할 건지 차이

3. SetTcpNoDelay, SetUpdateAcceptSocket 이 하는 일은?

- SetTcpNoDelay : 네이글 알고리즘 적용 여부

- SetUpdateAcceptSocket  : Listen socket 특성을 client socket에 그대로 적용함.

 

 

출처 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

관련글 더보기

댓글 영역