1. Server.cpp

주요 흐름:

  1. SocketUtils 초기화
  2. Listener 객체 생성 및 설정
  3. IOCP 쓰레드 실행
  4. 소켓 종료 및 자원 정리

코드 분석

int main()
{
	SocketUtils::Init();
  • SocketUtils::Init():
    • Winsock을 초기화합니다.
    • 내부적으로 WSAStartup을 호출하여 TCP/IP 소켓을 사용할 수 있는 환경을 만듭니다.
    • 비동기 확장 기능(AcceptEx, ConnectEx 등)을 사용할 준비도 이 과정에서 설정됩니다.
	Listener listener;
	listener.StartAccept(NetAddress(L"127.0.0.1", 7777));
  • Listener 객체:
    • Listener는 IOCP와 연동된 소켓의 연결 대기 역할을 합니다.
    • StartAccept:
      • NetAddress 클래스를 통해 지정된 IP(127.0.0.1)와 포트(7777)로 바인딩됩니다.
      • 비동기 Accept 작업을 준비합니다.
	for (int32 i = 0; i < 5; i++)
	{
		GThreadManager->Launch([=]()
			{
				while (true)
				{
					GIocpCore.Dispatch();
				}
			});
	}
  • GThreadManager:
    • IOCP 작업을 처리할 5개의 워커 스레드를 실행합니다.
    • GIocpCore.Dispatch():
      • IOCP 큐에서 완료된 작업을 가져와 처리합니다.
      • 각 스레드가 비동기로 처리된 결과를 받아 이벤트를 실행합니다.
	GThreadManager->Join();
  • Join():
    • 메인 스레드가 모든 워커 스레드의 종료를 대기합니다.
	SocketUtils::Clear();
}
  • SocketUtils::Clear():
    • Winsock 자원을 해제합니다.
    • 내부적으로 WSACleanup을 호출하여 네트워크 환경을 정리합니다.

2. SocketUtils.h

주요 목적:

  • 소켓 생성, 바인딩, 설정 등을 포함한 소켓 작업의 편리한 유틸리티 제공.

코드 분석

static LPFN_CONNECTEX ConnectEx;
static LPFN_DISCONNECTEX DisconnectEx;
static LPFN_ACCEPTEX AcceptEx;
  • 런타임에 로드되는 확장 함수 포인터:
    • ConnectEx, DisconnectEx, AcceptEx는 확장된 비동기 소켓 기능을 제공합니다.
    • WSAIoctl를 통해 함수 포인터를 동적으로 바인딩합니다.

static SOCKET CreateSocket();
  • 소켓 생성:
    • WSASocket API를 사용하여 비동기 소켓(OVERLAPPED 플래그 포함)을 생성합니다.

static bool BindAnyAddress(SOCKET socket, uint16 port);
  • 소켓을 특정 포트에 바인딩:
    • INADDR_ANY를 사용하여 모든 네트워크 인터페이스에서 접속을 허용합니다.

3. SocketUtils.cpp


Init 함수

void SocketUtils::Init()
{
	WSADATA wsaData;
	assert(::WSAStartup(MAKEWORD(2, 2), OUT &wsaData) == 0);

	SOCKET dummySocket = CreateSocket();
	assert(BindWindowsFunction(dummySocket, WSAID_CONNECTEX, reinterpret_cast<LPVOID*>(&ConnectEx)));
	assert(BindWindowsFunction(dummySocket, WSAID_DISCONNECTEX, reinterpret_cast<LPVOID*>(&DisconnectEx)));
	assert(BindWindowsFunction(dummySocket, WSAID_ACCEPTEX, reinterpret_cast<LPVOID*>(&AcceptEx)));

	Close(dummySocket);
}
  1. WSAStartup:

    • Winsock 환경을 초기화합니다.
    • TCP/IP 네트워크 프로그램에서 반드시 호출해야 합니다.
  2. 더미 소켓 생성:

    • 비동기 확장 함수(AcceptEx 등)를 바인딩하기 위한 임시 소켓입니다.
  3. BindWindowsFunction:

    • 소켓 핸들을 통해 WSAIoctl을 호출하여 특정 GUID(예: WSAID_ACCEPTEX)에 해당하는 함수 포인터를 얻습니다.
  4. Close:

    • 더미 소켓을 닫아 자원을 정리합니다.

CreateSocket 함수

SOCKET SocketUtils::CreateSocket()
{
	return ::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
}
  • WSASocket:
    • 비동기 I/O를 지원하는 소켓을 생성합니다(WSA_FLAG_OVERLAPPED 사용).

SetLinger 함수

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);
}
  • SO_LINGER 설정:
    • 연결 종료 시 대기 시간(linger)을 설정합니다.
    • onoff가 1일 경우, 데이터가 모두 전송될 때까지 종료를 지연시킵니다.

BindAnyAddress 함수

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));
}
  1. SOCKADDR_IN 초기화:

    • sin_family: 주소 패밀리 설정(AF_INET).
    • sin_addr: 모든 인터페이스(INADDR_ANY)로 설정.
    • sin_port: 주어진 포트로 설정.
  2. bind 호출:

    • 소켓과 주소를 연결합니다.

4. Listener 클래스


StartAccept 함수

bool Listener::StartAccept(NetAddress netAddress)
{
	_socket = SocketUtils::CreateSocket();
	if (_socket == INVALID_SOCKET)
		return false;

	if (GIocpCore.Register(this) == false)
		return false;

	if (SocketUtils::SetReuseAddress(_socket, true) == false)
		return false;

	if (SocketUtils::Bind(_socket, netAddress) == false)
		return false;

	if (SocketUtils::Listen(_socket) == false)
		return false;

	const int32 acceptCount = 1;
	for (int32 i = 0; i < acceptCount; i++)
	{
		IocpEvent* acceptEvent = new IocpEvent(EventType::Accept);
		_acceptEvents.push_back(acceptEvent);
		RegisterAccept(acceptEvent);
	}

	return true;
}
  1. 소켓 생성 및 IOCP 등록:

    • 새 소켓을 생성하고, 이를 IOCP 객체(GIocpCore)에 등록합니다.
  2. 소켓 설정:

    • SO_REUSEADDR: 동일한 포트에서 여러 소켓을 사용할 수 있도록 설정.
    • bind: 지정된 네트워크 주소와 포트에 소켓을 바인딩.
    • listen: 소켓을 수신 대기 상태로 설정.
  3. Accept 이벤트 등록:

    • acceptCount 횟수만큼 IocpEvent 객체를 생성하고, 비동기 Accept 작업을 등록합니다.

RegisterAccept 함수

void Listener::RegisterAccept(IocpEvent* acceptEvent)
{
	Session* session = new Session();

	acceptEvent->Init();
	acceptEvent->session = session;

	DWORD bytesReceived = 0;
	if (false == SocketUtils::AcceptEx(_socket, session->GetSocket(), session->_recvBuffer, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, OUT &bytesReceived, static_cast<LPOVERLAPPED>(acceptEvent)))
	{
		const int32 errorCode = ::WSAGetLastError();
		if (errorCode != WSA_IO_PENDING)
		{
			RegisterAccept(acceptEvent);
		}
	}
}
  • AcceptEx:
    • 비동기 Accept 작업을 요청합니다.
    • 실패 시 에러를 확인하고 재등록합니다.

ProcessAccept 함수

void Listener::ProcessAccept(IocpEvent* acceptEvent)
{
	Session* session = acceptEvent->session;

	if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	SOCKADDR_IN sockAddress;
	int32 sizeOfSockAddr = sizeof(sockAddress);
	if (SOCKET_ERROR == ::getpeername(session->GetSocket(), OUT reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddr))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	session->SetNetAddress(NetAddress(sockAddress));

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

	RegisterAccept(acceptEvent);
}
  1. Accept 작업 완료:

    • 클라이언트 소켓을 IOCP에 업데이트합니다.
    • 클라이언트 주소 정보를 확인합니다.
  2. Accept 재등록:

    • 새로운 클라이언트 연결 대기를 위해 Accept 작업을 다시 등록합니다.

1. Listener 클래스

역할

  • Listener 클래스는 서버에서 클라이언트 연결을 대기하는 역할을 합니다.
  • IOCP와 연동된 소켓을 사용하여 비동기 방식으로 연결을 관리합니다.

주요 멤버 변수

protected:
    SOCKET _socket = INVALID_SOCKET;
    vector<IocpEvent*> _acceptEvents;
  • _socket:
    • 클라이언트 연결을 대기하는 소켓입니다.
    • INVALID_SOCKET 값으로 초기화됩니다.
  • _acceptEvents:
    • Accept 작업을 위해 생성된 이벤트 객체(IocpEvent)의 목록입니다.

StartAccept 함수

bool Listener::StartAccept(NetAddress netAddress)
  1. 소켓 생성:

    • SocketUtils::CreateSocket를 호출하여 소켓을 생성합니다.
    • 실패 시 false를 반환합니다.
  2. IOCP에 등록:

    • GIocpCore.Register(this)를 호출하여 Listener 객체를 IOCP에 등록합니다.
  3. 소켓 설정:

    • SetReuseAddress: 소켓 주소 재사용 가능 여부 설정.
    • SetLinger: 연결 종료 시 대기 시간을 설정.
    • Bind: 네트워크 주소를 소켓에 바인딩.
    • Listen: 소켓을 수신 대기 상태로 설정.
  4. Accept 이벤트 등록:

    • acceptCount 횟수만큼 IocpEvent 객체를 생성하고 Accept 작업을 준비합니다.

RegisterAccept 함수

void Listener::RegisterAccept(IocpEvent* acceptEvent)
  • Accept 작업 등록:
    • SocketUtils::AcceptEx를 호출하여 비동기 Accept 작업을 요청합니다.
    • 실패 시 에러를 확인하고, 재등록을 시도합니다.

ProcessAccept 함수

void Listener::ProcessAccept(IocpEvent* acceptEvent)
  1. Accept 작업 완료:

    • 연결된 클라이언트 소켓을 업데이트합니다.
    • 클라이언트의 IP 주소 정보를 가져옵니다.
  2. Accept 재등록:

    • 새로운 클라이언트 연결을 대기하기 위해 Accept 작업을 다시 등록합니다.

Dispatch 함수

void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
  • IOCP에서 수신한 이벤트를 처리하는 함수입니다.
  • 이벤트 유형(EventType)에 따라 적절한 작업(ProcessAccept)을 호출합니다.

2. IocpCore 클래스

역할

  • IOCP 객체를 관리하는 클래스입니다.
  • IOCP 큐에서 작업을 처리하거나 이벤트를 등록하는 역할을 합니다.

주요 멤버 변수

private:
    HANDLE _iocpHandle;
  • _iocpHandle:
    • IOCP 객체의 핸들입니다.

Register 함수

bool IocpCore::Register(IocpObject* iocpObject)
  • 역할:
    • 주어진 IocpObject의 핸들을 IOCP 객체에 등록합니다.
  • CreateIoCompletionPort 호출:
    • 소켓(또는 파일 핸들)과 IOCP 객체를 연결합니다.

Dispatch 함수

bool IocpCore::Dispatch(uint32 timeoutMs)
  1. GetQueuedCompletionStatus 호출:

    • IOCP 큐에서 완료된 작업을 가져옵니다.
    • 큐에서 작업이 없을 경우, timeoutMs 시간만큼 대기합니다.
  2. 에러 처리:

    • WAIT_TIMEOUT 오류가 발생하면 작업이 없음을 의미합니다.
    • 기타 에러는 로깅하거나 별도의 처리 과정을 거칩니다.
  3. 작업 실행:

    • 작업과 연관된 IocpObjectDispatch 메서드를 호출하여 작업을 처리합니다.

생성자와 소멸자

IocpCore::IocpCore()
{
    _iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
    assert(_iocpHandle != INVALID_HANDLE_VALUE);
}

IocpCore::~IocpCore()
{
    ::CloseHandle(_iocpHandle);
}
  • 생성자:
    • IOCP 객체를 생성합니다.
    • 실패 시 프로그램이 종료됩니다.
  • 소멸자:
    • IOCP 객체를 닫아 자원을 해제합니다.

3. IocpObject 클래스

역할

  • IOCP와 연동되는 객체의 기본 인터페이스를 제공합니다.
  • 모든 IOCP 관련 클래스(Listener, Session 등)는 이를 상속받아 구현됩니다.

주요 메서드

virtual HANDLE GetHandle() abstract;
virtual void Dispatch(IocpEvent* iocpEvent, int32 numOfBytes = 0) abstract;
  • GetHandle:
    • IOCP에 등록된 핸들을 반환합니다.
  • Dispatch:
    • IOCP 큐에서 받은 작업을 처리합니다.

4. IocpEvent 클래스

역할

  • IOCP 작업을 표현하는 객체입니다.
  • IOCP 큐에서 관리되는 각 작업은 IocpEvent의 객체로 표현됩니다.

주요 멤버 변수

EventType type;
Session* session = nullptr;
  • type:
    • 이벤트 유형(예: Connect, Accept, Recv, Send).
  • session:
    • 이벤트와 연관된 클라이언트 세션 객체(예: AcceptEvent에서 사용).

Init 함수

void IocpEvent::Init()
{
    hEvent = 0;
    Internal = 0;
    InternalHigh = 0;
    Offset = 0;
    OffsetHigh = 0;
}
  • 역할:
    • OVERLAPPED 구조체의 필드를 초기화합니다.

5. Session 클래스

역할

  • 서버와 연결된 각 클라이언트를 나타내는 클래스입니다.
  • 클라이언트 소켓과 관련된 데이터를 관리합니다.

주요 멤버 변수

private:
    SOCKET _socket = INVALID_SOCKET;
    NetAddress _netAddress = {};
    Atomic<bool> _connected = false;
    char _recvBuffer[1000];
  • _socket:
    • 클라이언트와의 연결을 나타내는 소켓.
  • _netAddress:
    • 클라이언트의 IP 주소 및 포트를 저장.
  • _connected:
    • 현재 연결 상태를 나타냄.
  • _recvBuffer:
    • 수신 데이터를 임시 저장하는 버퍼.

Dispatch 함수

void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
  • IOCP 큐에서 받은 이벤트를 처리합니다.
  • 예를 들어, Recv 이벤트의 경우 수신 데이터를 처리합니다.

6. NetAddress 클래스

역할

  • 네트워크 주소(IP, Port)를 관리하는 클래스입니다.
  • 소켓 바인딩 또는 연결 시 사용됩니다.

주요 멤버 변수

private:
    SOCKADDR_IN _sockAddr = {};
  • _sockAddr:
    • IP 주소와 포트를 저장하는 구조체.

Ip2Address 함수

IN_ADDR NetAddress::Ip2Address(const WCHAR* ip)
{
    IN_ADDR address;
    ::InetPtonW(AF_INET, ip, &address);
    return address;
}
  • 역할:
    • 문자열 형태의 IP 주소를 IN_ADDR 구조체로 변환합니다.

profile
李家네_공부방

0개의 댓글