int main()
{
SOCKET socket = SocketUtils::CreateSocket();
SocketUtils::BindAnyAddress(socket, 7777);
SocketUtils::Listen(socket);
SOCKET clientSOcket = ::accept(socket, nullptr, nullptr);
cout << "Client Connected!" << endl;
while (true)
{
}
//CoreGloabl에서 선언한 전역변수를 아무것도 사용하지 않으면 컴파일러가 알아서
//우리가 전역으로 선언한 모든 클래스들을 생성하지 않는다.
//그래서 아래 코드임의로 추가해서 전역 클래스 사용한다고 컴파일러에게 알림
GThreadManager->Join();
}
이번에 작업할 내용은 위의 예제코드에서 accept하는 부분을 우리가 BindWindowFunction()으로 런타임에 사용할 준비가 완료된 AcceptEx 함수로 바꿔주는 작업이다.
하지만 그러기 위해서는 AcceptEx를 그냥 호출하고 끝이 아니라 IOCP 구조를 이용해서 호출할 것이기 때문에 먼저
IocpCore를 만들어 주도록 하겠다.
(AcceptEx 함수만 처리했기 때문에 지금 서버에서는 클라이언트를 Accept하는 기능밖에 없다.)
IocpCore
Iocp에서 사용하는 2개의 핵심 함수를 랩핑하였다.
(CreateIoCompletionPort(), GetQueuedCompletionStatus())
//IocpCore.h
#pragma once
/*============================
*
* IocpObject
*
============================*/
//Completion Prot에 등록하는 오브젝트들을 IocpObject라고 별도로 관리.
class IocpObject
{
public:
virtual HANDLE GetHandle() abstract;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) abstract;
};
/*============================
*
* IocpCore
*
============================*/
class IocpCore
{
public:
IocpCore();
~IocpCore();
//iocp에서 사용한 2개의 핵심 함수를 랩핑
//CreateIoCompletionPort(), GetQueuedCompletionStatus()
HANDLE GetHandle() { return _iocpHandle; }
bool Register(class IocpObject* iocpObject); //등록 : CreateIoCompletionPort()
bool Dispatch(uint32 timeoutMs = INFINITE); //일감 확인 : GetQueuedCompletionStatus()
private:
HANDLE _iocpHandle;
};
// TEMP
extern IocpCore GIocpCore;
class IocpObject
Completion Port에 등록하는 오브젝트들을 IocpObject라고 별도로 관리한다.
bool Register(class IocpObject* iocpObject);
등록 : CreateIoCompletionPort()
bool Dispatch(uint32 timeoutMs = INFINITE);
일감 확인 : GetQueuedCompletionStatus()
//IocpCore.cpp
#include "pch.h"
#include "IocpCore.h"
#include "IocpEvent.h"
//TEMP
IocpCore GIocpCore;
/*============================
*
* IocpCore
*
============================*/
IocpCore::IocpCore()
{
//Completion Port 생성
_iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
ASSERT_CRASH(_iocpHandle != INVALID_HANDLE_VALUE);
}
IocpCore::~IocpCore()
{
::CloseHandle(_iocpHandle);
}
bool IocpCore::Register(IocpObject* iocpObject)
{
return ::CreateIoCompletionPort(iocpObject->GetHandle(), _iocpHandle, /*key*/reinterpret_cast<ULONG_PTR>(/*&빼주니까 오류 없어짐*/iocpObject), 0);
}
bool IocpCore::Dispatch(uint32 timeoutMs)
{
DWORD numOfBytes = 0;
//IocpObject, IocpEvent 복원
IocpObject* iocpObject = nullptr;
IocpEvent* iocpEvent = nullptr;
if (::GetQueuedCompletionStatus(_iocpHandle, OUT & numOfBytes, OUT reinterpret_cast<PULONG_PTR>(&iocpObject),
OUT reinterpret_cast<LPOVERLAPPED*>(&iocpEvent), timeoutMs))
{
//성공적으로 IocpObject와 IocpEvent 복원
iocpObject->Dispatch(iocpEvent, numOfBytes);
}
else
{
int32 errCode = ::WSAGetLastError();
switch (errCode)
{
case WAIT_TIMEOUT:
return false;
default:
//TODO : 로그 찍기
iocpObject->Dispatch(iocpEvent, numOfBytes);
break;
}
}
return true;
}
bool IocpCore::Register(IocpObject* iocpObject)
원래는 클라이언트 소켓을 받아서 등록했었으나 이번에는 IocpCore의 핸들과 iocpObject의 핸들을 연결시킨다.
bool IocpCore::Dispatch(uint32 timeoutMs)
IocpObect와 IocpEvent를 복원시킨다.
성공적으로 복원하였다면 그 iocpObject의 Dispatch를 호출한다.
원래는 이렇게 등록을 할 때에는 레퍼런스 카운팅을 해줘서 절대로 중간에 날라가지 않도록 해줘야 한다.
(지금 코드는 전체적인 구조만 있음.)
IocpCore를 만들고 listen 소켓을 만들어 줘서 그 소켓을 Register() 함수를 통해 등록해주면된다.
그리고 지금은 코드에서 우선 임시로 IocpCore를 전역으로 하나 만들어서 사용한다.
IocpEvent
EventType을 통해 이벤트를 구분한다.
OVERLAPPED를 상속한 IocpEvent를 만들어 주어서 이벤트를 관리한다.
//IocpEvent.h
#pragma once
class Session;
enum class EventType : uint8
{
Connect,
Accept,
//PreRecv, //0 byte recv
Recv,
Send,
};
/*=======================
*
* IocpEvent
*
=======================*/
class IocpEvent : public OVERLAPPED
{
public:
IocpEvent(EventType type);
void Init();
EventType GetType() { return _type; }
protected:
EventType _type;
};
/*=======================
*
* ConnectEvent
*
=======================*/
class ConnectEvent : public IocpEvent
{
public:
ConnectEvent() : IocpEvent(EventType::Connect) {}
};
/*=======================
*
* AcceptEvent
*
=======================*/
class AcceptEvent : public IocpEvent
{
public:
AcceptEvent() : IocpEvent(EventType::Accept) {}
void SetSession(Session* session) { _session = session; }
Session* GetSession() { return _session; }
private:
Session* _session = nullptr;
};
/*=======================
*
* RecvEvent
*
=======================*/
class RecvEvent : public IocpEvent
{
public:
RecvEvent() : IocpEvent(EventType::Recv) {}
};
/*=======================
*
* SendEvent
*
=======================*/
class SendEvent : public IocpEvent
{
public:
SendEvent() : IocpEvent(EventType::Send) {}
};
IocpEvent 클래스
첫번째 멤버변수로 OVERLAPPED를 들고있거나 상속을 받아서 사용하면 되는데 이 코드에서는 상속을 받아서 사용한다.
그렇게 하면 offset 0번에는 무조건 OVERLAPPED 메모리가 들어가 있기 때문에, IocpEvent 포인터로 사용하나 OVERLAPPED 포인터로 사용하나 똑같아 질 것이다.
주의)
event 별로 클래스를 분리해서 관리 할 때는
virtual 함수를 사용하면 안된다. 왜냐하면 virtual 소멸자를 사용하게 되면, virtual 함수 때문에 가상함수 테이블이 offset 0번에 들어가게 되면서 맨 처음 offset에 있던 메모리가 OVERLAPPED와 관련된 메모리가 아닌 다른 메모리가 채워질 수 있기 때문이다.
ConnectEvent, AcceptEvent, RecvEvent, SendEvent
생성자에서 이벤트 타입을 알맞게 바꿔줌.
AcceptEvent
클라이언트 정보가 담긴 Session을 등록하고 리턴해주는 함수를 추가해준다.
//IocpEvent.cpp
#include "pch.h"
#include "IocpEvent.h"
IocpEvent::IocpEvent(EventType type) : _type(type)
{
Init();
}
void IocpEvent::Init()
{
OVERLAPPED::hEvent = 0;
OVERLAPPED::Internal = 0;
OVERLAPPED::InternalHigh = 0;
OVERLAPPED::Offset = 0;
OVERLAPPED::OffsetHigh = 0;
}
void IocpEvent::Init()
OVERLAPPED 구조체 초기화하는 함수.
Listener
리스너 소켓(문지기, 안내원) 역할. (소켓을 안내해줌)
Listener를 IocpObject로 인지해서 IocpCore에 등록해준다.
//Listener.h
#pragma once
#include "IocpCore.h"
#include "NetAddress.h"
class AcceptEvent;
/*======================
*
* Listener
*
======================*/
class Listener : public IocpObject
{
public:
Listener() = default;
~Listener();
public:
/* 외부에서 사용 */
bool StartAccept(NetAddress netAddress);
void CloseSocket();
public:
/* 인터페이스 구현 */
virtual HANDLE GetHandle() override;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;
private:
/* 수신 관련 */
void RegisterAccept(AcceptEvent * acceptEvent);
void ProcessAccept(AcceptEvent * acceptEvent);
protected:
SOCKET _socket = INVALID_SOCKET;
xvector<AcceptEvent*> _acceptEvents;
};
AcceptEx
BOOL AcceptEx(
SOCKET sListenSocket, // ListenSocket
SOCKET sAcceptSocket, // 미리 만들어 놓은 소켓, 바인드되거나 연결된 소켓은 안됨.
PVOID lpOutputBuffer, // 소켓의 로컬 , 원격 주소와 첫번째 바이트 스트림을 수신할 버퍼. NULL 불가.
DWORD dwReceiveDataLength, //첫번째로 수신할 바이트스트림의 크기, 0이면 수신작업을 수행하지 않는다.
DWORD dwLocalAddressLength, //소켓 주소 구조체 + 16
DWORD dwRemoteAddressLength, //소켓 주소 구조체 + 16
LPDWORD lpdwBytesReceived, //실제로 수신한 바이트수, 동기로 작업했을때만 유효하다.
LPOVERLAPPED lpOverlapped //overlapped 구조체 포인터
);
AcceptEx function (mswsock.h) - Win32 apps
The AcceptEx function (mswsock.h) accepts a new connection, returns the local and remote address, and receives the first block of data sent by the client application.
learn.microsoft.com
MSDN을 해석해보면 대충
- accept 함수보다 효율적이다.
- AcceptEx함수는 비동기 작업을 지원한다.
- 한번의 AcceptEx 함수 호출로 연결을 수용하고,
클라이언트 주소정보를 기록하고 첫번째 바이트 스트림을 수신할 수 있다. - AcceptEx 함수 호출에 Listen Socket과 AcceptSocket(클라이언트가 할당될 소켓)가 필요하다.
- AcceptSocket은 반드시 열린 소켓이어야 하며 , 닫힌 소켓이거나 bind되어있지 않아야한다.
등등....
하지만 이것만으론 AcceptEx를 쓰는이유에 대해서 잘 알지 못하겠다.
구글링 결과 단순히 효율이 좋기때문에를 넘어서서 몇가지 이유와 주의사항이 더 있다.
사용하는 이유
1. DisconnectEx나 TransmitFile을 통해 연결이 종료된 소켓을 재사용가능하게 만들고
이를 AcceptEx함수로 전달하여 재사용이 가능하다. 즉, 미리 소켓풀을 만들어서
서버 가동중 생기는 소켓 생성/반환 리소스를 아낄수있고, 빈번한 생성/반환으로 인해
생기는 메모리 단편화를 방지할 수 있다.
2.평상시에는 단순한 accept함수 성능만으로 충분할지 모르지만,
서버가 Off되었다 On될때 순간적으로 몰리는 접속을 처리할때는 확실히 유의미하다
주의사항.
1. 소켓풀을 만들때 WSASocket을 사용하지말고 socket 함수를 사용할것.
WSASocket으로 생성한 소켓은 IOCP에 등록이 되지 않는다.
2. accept함수와 사용법이 다르다. 보통 accept함수는 별개의 accept를 담당하는 쓰레드에
블록방식으로 수행한다.
while(1)
accept(...)
물론 accpet에 논블록 리슨 소켓을 사용해서, 스레드를 블록상태로 만들지 않을수도 있지만,
그러자니 accept스레드는 할일이없고 할일이 없는 스레드는 자러가는것이 원칙. 굳이 논블록 소켓을 사용하지 않는다.
acceptEx함수는? accept스레드가 있다면 마찬가지아닌가? 그럴리가 없다. 다른방식일 것이고 실제로 그러하다.
acceptEx함수는 미리 소켓풀을 만들어놓고 만든 소켓을 전부 비동기작업으로 미리 요청해둔다.
for(i=0;i<소켓풀크기;i++)
acceptEx(미리만들어둔 소켓)
그리고 워커쓰레드에서 GQCS(Gob Queue Completion Statuts)등으로 완료통지를 받고, 처리하고, 사용이 끝난 소켓은 TransmitFile을 통해 재사용가능하게 만들고 다시 AcceptEx를 걸고.. 결론적으로 accept 스레드가 있을 필요가없다.
#include "pch.h"
#include "Listener.h"
#include "SocketUtils.h"
#include "IocpEvent.h"
#include "Session.h"
Listener::~Listener()
{
SocketUtils::CloseSocket(_socket);
for (AcceptEvent* acceptEvent : _acceptEvents)
{
//TODO
xdelete(acceptEvent);
}
}
bool Listener::StartAccept(NetAddress netAddress)
{
_socket = SocketUtils::CreateSocket();
if (_socket == INVALID_SOCKET)
return false;
//Listen 소켓도 관찰해야할 대상이기 때문에 IocpCore(Completion Port)에 등록해준다.
if (GIocpCore.Register(this) == false)
return false;
if (SocketUtils::SetReuseAddress(_socket, true) == false)
return false;
if (SocketUtils::SetLinger(_socket, 0, 0) == 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++)
{
AcceptEvent* acceptEvent = xnew<AcceptEvent>();
_acceptEvents.push_back(acceptEvent);
RegisterAccept(acceptEvent);
}
return false;
}
void Listener::CloseSocket()
{
SocketUtils::CloseSocket(_socket);
}
HANDLE Listener::GetHandle()
{
return reinterpret_cast<HANDLE>(_socket);
}
void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
ASSERT_CRASH(iocpEvent->GetType() == EventType::Accept);
AcceptEvent* acceptEvent = static_cast<AcceptEvent*> (iocpEvent);
ProcessAccept(acceptEvent);
}
void Listener::RegisterAccept(AcceptEvent* acceptEvent)
{
Session* session = xnew<Session>();
acceptEvent->Init();
acceptEvent->SetSession(session);
DWORD bytesReceived = 0;
//AcceptEx 호출
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 errCode = WSAGetLastError();
if (errCode != WSA_IO_PENDING)
{
//일단 다시 Accept 걸어준다.
RegisterAccept(acceptEvent);
}
}
}
void Listener::ProcessAccept(AcceptEvent* acceptEvent)
{
Session* session = acceptEvent->GetSession();
if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket))
{
//문제가 일어났다고 그냥 종료하면 영영 통신이 안됨.
//다시 Accept 걸어준다.
RegisterAccept(acceptEvent);
return;
}
SOCKADDR_IN sockAddress;
int32 sizeOfSockAddr = sizeof(sockAddress);
if (SOCKET_ERROR == ::getpeername(session->GetSocket(), OUT reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddr))
{
//다시 Accept 걸어준다.
RegisterAccept(acceptEvent);
return;
}
session->SetNetAddress(NetAddress(sockAddress));
cout << "Client Connected!" << endl;
RegisterAccept(acceptEvent);
}
bool StartAccept(NetAddress netAddress);
리스너 소켓을 만들어 주고, 소켓옵션을 설정한 다음, Bind 와 Listen을 해준다.
리스너 소켓도 관찰해야할 대상이기 때문에 IocpCore(Completion Port)에 등록해준다.
그런 다음 AcceptEvent를 만들어주고, 그 AcceptEvent를 통해 RegisterAccept(acceptEvent); 를 호출한다.
void CloseSocket();
소켓을 닫는다.
HANDLE Listener::GetHandle()
(IocpObject에서 상속받은 인터페이스를 구현해준다.)
_socket을 HANDLE로 캐스팅하여 리턴해줌.
void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
(IocpObject에서 상속받은 인터페이스를 구현해준다.)
IocpCore의 Dispatch를 통해 GetQueuedCompletionStatus()로 일감이 생길경우
Listener의 Dispatch로 들어와서 ProcessAccept()를 호출해서 일감을 마저 실행한다.
매개변수로 받아온 iocpEvent가 Accept인지 확인.
받아온 iocpEvent를 AcceptEvent*로 캐스팅한 다음,
ProcessAccept(acceptEvent) 호출해준다.
void RegisterAccept(AcceptEvent * acceptEvent);
Session을 만들어서(Session 생성자에서 소켓도 같이 생성됨. 이 소켓이 클라이언트 소켓임)
acceptEvent의 session에 넣어준다.
AcceptEx를 호출한다.
혹시나 문제가 일어났다고 그냥 종료하면 영영 통신이 안되므로
다시 한번더 RegisterAccept(acceptEvent); 를 호출해준다.
void ProcessAccept(AcceptEvent * acceptEvent);
소켓이 연결되었는지 getpeername()으로 확인하고 정상적으로 연결 되었을시 접속로그를 남기고 RegisterAccept(acceptEvent); 한번더 호출한다.
왜냐하면 AcceptEvent를 다 사용하고 삭제를 하는게 아니라, 기존의 사용했던 것을 계속 재사용 한다.
문제가 있을시 다시 RegisterAccept(acceptEvent); 호출하여 다시 클라이언트 접속을 기다린다.
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
설명
getpeername함수는 소켓 지시자 sockfd에 연결한 상대의 주소 정보를 가져온다. 주소 정보는 addr로 넘어온다. addrlen는 addr구조체의 크기다. 함수가 반환된 다음에는 가져온 addr 자료구조의 크기 값을 바이트로 돌려준다.
반환
성공하면 0을 실패하면 -1을 반환한다.
Session
클라이언트가 접속 했을 때, 클라이언트와 관련된 모든 정보를 Session 클래스로 관리한다.
//Session.h
#pragma once
#include "IocpCore.h"
#include "IocpEvent.h"
#include "NetAddress.h"
/*======================
*
* Session
*
======================*/
//클라이언트가 접속을 했을 때
//클라와 관련된 모든 정보를 Session 클래스로 관리함.
class Session : public IocpObject
{
public:
Session();
virtual ~Session();
public:
/* 정보 관련 */
void SetNetAddress(NetAddress address) { _netAddress = address; }
NetAddress GetAddress() { return _netAddress; }
SOCKET GetSocket() { return _socket; }
public:
/* 인터페이스 구현 */
virtual HANDLE GetHandle() override;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;
public:
//TEMP
char _recvBuffer[1000];
private:
SOCKET _socket = INVALID_SOCKET;
NetAddress _netAddress = {};
Atomic<bool> _connected = false;
};
//Session.cpp
#include "pch.h"
#include "Session.h"
#include "SocketUtils.h"
Session::Session()
{
_socket = SocketUtils::CreateSocket();
}
Session::~Session()
{
SocketUtils::CloseSocket(_socket);
}
HANDLE Session::GetHandle()
{
return reinterpret_cast<HANDLE>(_socket);
}
void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
//TODO
//나중에 recv 또는 send일때 여기로 들어옴
}
전체적인 구조
'네트워크 프로그래밍' 카테고리의 다른 글
Session (1) (0) | 2023.03.03 |
---|---|
Server Service (0) | 2023.03.03 |
Socket Utils (0) | 2023.02.26 |
IOCP 모델 (0) | 2023.02.24 |
Overlapped 모델 (콜백 기반) (0) | 2023.02.24 |