이제 세션이 본격적으로 역할을 할 수 있도록 고쳐나간다.
세션 클래스에 필요한 것들을 만들어 두고,
세션에서 많은 일들을 할 수 있게 할 것이다.
단계를 나눠서 진행한다.
1. 연결 및 수신
1-1. 클래스 내용 추가
추가된 부분에 대해서만 표시한다.
#pragma once
#include "IocpCore.h"
#include "IocpEvent.h"
#include "NetAddress.h"
class Service;
class Session : public IocpObject
{
// 자유롭게 꺼내쓸 수 있도록 허락
friend class Listener;
friend class IocpCore;
friend class Service;
public:
// 생/소멸자
// 외부에서 사용할 함수들
public:
void Disconnect(const WCHAR* cause);
shared_ptr<Service> GetService() { return _service.lock(); }
void SetService(shared_ptr<Service> service) { _service = service; }
public:
/* 정보 관련 */
// ...
bool IsConnected() { return _connected; }
// 이전엔 일일이 가져온 것을 함수로 만들어 둠
SessionRef GetSessionRef() { return static_pointer_cast<Session>(shared_from_this()); }
private:
/* 인터페이스 구현 */
// ...
private:
/* 전송 관련 */
void RegisterConnect(); // 당장은 사용하지 않음
void RegisterRecv();
void RegisterSend();
void ProcessConnect();
void ProcessRecv(int32 numOfBytes);
void ProcessSend(int32 numOfBytes);
void HandleError(int32 errorCode);
protected:
/* 컨텐츠 코드에서 오버라이딩 */
virtual void OnConnected() { }
virtual int32 OnRecv(BYTE* buffer, int32 len) { return len; }
virtual void OnSend(int32 len) { }
virtual void OnDisconnected() { }
public:
// TEMP BUFFER
private:
// 크래시가 나거나 종료되지 않는 이상
// Service는 어딘가에 있을 것이기 때문에
// weak_ptr로 들고 있도록 함
weak_ptr<Service> _service;
// ...
private:
USE_LOCK;
/* 수신 관련 */
/* 송신 관련 */
private:
/* IocpEvent 재사용 */
RecvEvent _recvEvent;
};
상당히 많은 함수들이 생겼다.
먼저 모든 함수를 public으로 둘 수 없으므로, 해당 함수에 접근해야 하는 클래스들을 위해,
해당 클래스들을 friend 키워드를 사용해 함수들을 꺼내쓸 수 있도록 했다.
weak_ptr을 사용해 service를 들고 있도록 한다.
서비스는 어쨌거나 서버가 돌아가는 이상 살아 있을 것이기 때문이다.
그다음 외부에서 사용할 public 함수들을 선언한다.
IO 관련해서 걸어줄 함수와 처리할 함수, 그리고 에러 핸들링까지 해줄 함수를 만든다.
컨텐츠에서 오버로딩해 사용할 함수들도 만들어 준다.
아래의 RecvEvent는 이벤트를 재사용하기 위해 추가했다.
이제 함수를 정의해 보자.
여기선 송신 관련 함수들은 정의하지 않을 것이다.
void Session::Disconnect(const WCHAR* cause)
{
// 기존 값이 False라면 이미 연결이 끊긴 것
if (_connected.exchange(false) == false)
return;
// TEMP
wcout << "Disconnect : " << cause << endl;
OnDisconnected(); // 컨텐츠 코드에서 오버로딩
SocketUtils::Close(_socket);
GetService()->ReleaseSession(GetSessionRef());
}
_connected는 atomic이므로 exchange()를 사용해 값을 변경해야 한다.
만약 false로 변경이 되지 않는다면 이미 false라는 뜻이므로 리턴한다.
true로 변경됐다면 로그를 찍고 소켓을 닫고 세션을 릴리즈 한다.
로그를 바로 cout으로 찍는 건 효율적이지 않은 방법이지만 지금은 임시로 찍도록 하자.
void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
switch (iocpEvent->eventType)
{
case EventType::Connect:
ProcessConnect();
break;
case EventType::Recv:
ProcessRecv(numOfBytes);
break;
case EventType::Send:
ProcessSend(numOfBytes);
break;
default:
break;
}
}
Dispatch는 이제 전부 세션에서 처리하게 될 것이다.
이벤트 타입 별로 스위치 문을 통해 분기할 것이다.
void Session::RegisterRecv()
{
// 연결 확인
if (IsConnected() == false)
return;
// 실시간으로 만들기 보다는
// 내부적으로 들고있게 해
// 재사용 가능하게 함
_recvEvent.Init();
_recvEvent.owner = shared_from_this(); // ADD_REF, WSARecv가 동작하는 동안 사라지지 않게
WSABUF wsaBuf;
wsaBuf.buf = reinterpret_cast<char*>(_recvBuffer);
wsaBuf.len = len32(_recvBuffer);
DWORD numOfBytes = 0;
DWORD flags = 0;
if (SOCKET_ERROR == ::WSARecv(_socket, &wsaBuf, 1, OUT &numOfBytes, OUT &flags, &_recvEvent, nullptr))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
HandleError(errorCode);
_recvEvent.owner = nullptr; // RELEASE_REF
// 릴리즈 해 주지 않으면 카운트가 줄지 않아
// 영영 해제되지 않게 되고
// 메모리 누수로 이어진다
}
}
}
recvEvent를 초기화 하고 그 owner에 자신의 포인터를 넘겨준다.
만약 무슨 일이 생겨서 사라져 버리면 WSARecv가 정상적으로 처리되지 못할 것이다.
임시로 버퍼 등의 필요한 것들을 만들고 WSARecv를 걸어준다.
만약 Pending 이외의 에러코드를 받는다면, owner의 포인터를 nullptr로 해제해 주어야 한다.
만약 해제해 주지 않으면 사라지지 않게 되어 메모리 누수로 이어질 수 있다.
void Session::ProcessConnect()
{
// Atomic이기 때문에 store() 사용
_connected.store(true);
// 세션 등록
GetService()->AddSession(GetSessionRef());
// 컨텐츠 코드에서 오버로딩
OnConnected();
// 수신 등록
RegisterRecv();
}
연결이 완료되면 _connected를 true로 만들어 주고, 서비스에 세션을 등록한다.
그리고 수신을 받기 위해 Recv를 걸어준다.
void Session::ProcessRecv(int32 numOfBytes)
{
// 다 받았으므로 해제
// 어차피 이벤트를 재사용 할 것이라면 일일이 등록/해제를 할 필요가 없는 것 아닌가?
// 그렇게 해도 되지만 owner에 대한 생명주기를 더 명확히 관리하기 위함
// 해제하지 않아도 연결 끊김 처리는 되지만 세션이 소멸하지 않음
_recvEvent.owner = nullptr; // RELEASE_REF
if (numOfBytes == 0)
{
Disconnect(L"Recv 0");
return;
}
// TODO
cout << "Recv Data Len = " << numOfBytes << endl;
// 수신 등록
RegisterRecv();
}
수신 처리는 먼저 owner의 포인터를 밀어주는 일이다.
이벤트 변수를 재사용 할 것이기 때문에 일일이 포인터를 주고 밀고하는 것이 비효율적으로 보일 수 있지만,
owner에 대한 생명주기를 보다 명확히 관리하기 위함에 그 의도가 있다.
만약 수신한 바이트가 0이면 연결이 끊겼다는 뜻이므로 Disconnec()를 호출해 주고,
1 이상이라면 처리한 후 다시 수신할 수 있도록 Recv를 등록해 준다.
void Session::HandleError(int32 errorCode)
{
switch (errorCode)
{
case WSAECONNRESET:
case WSAECONNABORTED:
Disconnect(L"HandleError");
break;
default:
// TODO : Log
// 바로 콘솔에 로그를 찍는 것도 컨텍스트 스위칭 비용이 듦
cout << "Handle Error : " << errorCode << endl;
break;
}
}
에러 핸들링은 CONNRESET이나 CONNABORTED가 아니라면,
별도의 로그를 찍어 상황을 확인할 수 있도록 할 것이다.
1-2. 흐름 돌아보기
클래스에 넣을 것은 다 넣었으니, 데이터를 수신받기 까지의 흐름을 다시 보자.
- GameServer.cpp에서 ServerService를 만든다.
- ServerService의 Start() 호출.
- Listener를 생성하고 ServerServiceRef에 Service 자신의 포인터를 넘겨준다.
- Listener의 StartAccept()를 호출.
- Service에서 정보들을 가져와 Listen까지 실행.
- AcceptEvent를 생성하고 RegisterAccept()로 걸어준다.
- GameServer에서 생성한 워커 스레드들이 계속 IocpCore의 Dispatch()를 호출하며 감지.
- 일감이 생기면 Listener의 Dispatch()를 호출해 ProcessAccept() 호출.
- 클라이언트의 정보를 받아 연결을 완료하고 session의 ProcessConnect() 호출.
- 서비스에 세션을 등록하고 RegisterRecv() 호출해 걸어준다.
- 수신이 감지되면 세션의 Dispatch()를 호출해 이벤트 타입에 따라 분기.
- 지금은 수신이므로 ProcessRecv() 호출.
- owner를 밀어주고 받은 데이터 길이를 출력한 후, 다시 수신 등록.
- 이제 계속 수신 받고 처리하고 다시 수신 걸고의 반복.
이런 흐름이 되겠다.
결국 IO함수를 걸어주기 까지의 과정만 다 지나면 계속 IO 함수를 걸고 Dispatch()를 호출하여
준비가 됐는지 확인 후 일감을 처리한다.
일감의 처리가 끝나면 다시 IO함수를 걸어 다음 일처리를 대기하는 것의 반복이 된다.
1-3. 실행
이제 제대로 데이터를 수신하는지 확인해 보자.
데이터를 잘 수신하고 있다.
여기서 만약 클라이언트를 종료해 연결을 끊으면 어떻게 될까?
Session에 정의해 둔 Disconnect()가 호출되면서 정상적으로 종료된다.
ProcessRecv()에서 Event의 owner를 nullptr로 밀지 않더라도 연결은 종료되지만,
세션이 소멸하지 않아서 메모리 누수가 발생하게 된다.
2. 송신
이제 Send와 관련한 기능을 추가한다.
완성된 코드가 아닌 동작과 그 흐름을 이해하기 위한 임시 코드가 많다.
먼저 외부에서 사용할 Send()를 정의하자.
void Session::Send(BYTE* buffer, int32 len)
{
// 생각할 문제
// 1) 버퍼 관리?
// 2) sendEvent 관리? 단일? 여러개? WSASend 중첩?
// TEMP
SendEvent* sendEvent = xnew<SendEvent>();
sendEvent->owner = shared_from_this(); // ADD_REF
sendEvent->buffer.resize(len);
::memcpy(sendEvent->buffer.data(), buffer, len);
// Send를 할 때마다 Send를 걸어주기 보단
// 세션에 모아두고 한번에 보낼 수 있도록 유도하는 것이 성능상 유리하다.
WRITE_LOCK;
RegisterSend(sendEvent);
}
인자로는 보낼 내용과 그 길이를 받는다.
사실 여기서 버퍼나 이벤트 관리에 대해서 의문이 생긴다.
세션마다 버퍼를 두는게 맞는지, 이벤트를 하나로 할지, 여러 개로 관리할지 등의 것들이다.
지금 당장은 매 송신마다 이벤트를 생성하기로 하고,
이벤트를 생성한 후 owner에 세션의 포인터를 넘겨준 후, 버퍼의 사이즈를 len으로 설정한다.
resize()를 보면 알겠지만, SendEvent 클래스에 바이트 벡터형의 버퍼 변수를 아래와 같이 추가했다.
class SendEvent : public IocpEvent
{
public:
SendEvent() : IocpEvent(EventType::Send) { }
// TEMP
// 사이즈를 예측할 수 없으므로 벡터로
// using BYTE = unsigned char
vector<BYTE> buffer;
};
여기서 BYTE는 unsigned char를 나타낸다.
memcpy()로 버퍼에 인자로 받은 데이터들을 넘겨준다.
그리고 WRITE_LOCK을 걸고 Send를 걸어줬다.
여기서 WRITE_LOCK을 건 이유는 WSASend()가 Thread Safe 하지 않기 때문이다.
2-1. 의문점
Thread Safe하지 않은 건 WSARecv()도 마찬가지인데, 왜 Recv 할 땐 락을 걸어주지 않지? 라는 궁금증이 생긴다.
결론부터 말하자면, 동일한 세션에 대해 RegisterRecv가 두번 호출될 일이 없기 때문이다.
RegisterRecv는 딱 하나만 걸어줬었다.
낚싯대에 비유했던 것이 생각나는데, 사람마다 낚싯대는 오직 1개만 존재한다.
1개만 있는데, 다른 낚싯대를 드리울 수 없을 것이다.
낚시가 성공이든 아니든 일단 낚싯대를 들어 올리고(ProcessRecv), 다시 낚싯대를 드리워야 한다(RegisterRecv).
하지만 Send는 좀 사정이 다르다.
세션에서 언제 얼마나 Send를 호할 지 알 수가 없다.
별도의 안전 처리가 없다면 여러번 호출될 수가 있다.
걸린 순서대로 일을 처리한다는 보장이 없으므로,
이를 잡아줄 것이 필요한데, 그것이 이번엔 LOCK을 거는 형식으로 나타났다.
다시 돌아와서 RegisterSend()를 보자.
void Session::RegisterSend(SendEvent* sendEvent)
{
if (IsConnected() == false)
return;
WSABUF wsaBuf;
wsaBuf.buf = (char*)sendEvent->buffer.data();
wsaBuf.len = (ULONG)sendEvent->buffer.size();
DWORD numOfBytes = 0;
// 2번째와 3번째 인자는 Scatter-Gather를 수행하기 위함이다.
// BufferCount를 조절함으로써 여러 버퍼에 대해 Send를 한번에 수행할 수 있게 한다.
// 결과적으로 하나씩 보내는 것 보다 비용이 적게 들게 된다.
if (SOCKET_ERROR == ::WSASend(_socket, &wsaBuf, 1, OUT &numOfBytes, 0, sendEvent, nullptr))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
HandleError(errorCode);
sendEvent->owner = nullptr; // RELEASE_REF
xdelete(sendEvent);
}
}
}
RegisterSend()와 마찬가지로 IsConnected()를 통해 연결을 확인해 준다.
연결이 종료됐다면 Send할 이유도 없기 때문.
이제 WSASend()를 호출하게 된다.
여전히 에러의 이유가 Pending인 경우는 문제가 없으니 넘어가고,
다른 에러에 대해 핸들링을 수행한다.
일단 여기선 버퍼 카운트를 이전과 동일하게 1을 준 상태다.
WSASend()를 매 Send마다 호출하는 것은 비용이 꽤 들기 때문에,
WSABUF에 버퍼들을 모아두고 한 번에 여러 개의 버퍼를 처리할 수 있게 해야 한다.
이렇게 하는 편이 결과적으로 비용이 덜 들어 효율적인 처리가 되지만,
지금은 흐름을 보며 공부해 나가는 단계이기 때문에 1로 설정해 두었다.
Send를 걸어두고 작업이 준비되면 스레드가 일어나 ProcessSend를 호출할 것이다.
void Session::ProcessSend(SendEvent* sendEvent, int32 numOfBytes)
{
sendEvent->owner = nullptr; // RELEASE_REF
xdelete(sendEvent);
if (numOfBytes == 0)
{
Disconnect(L"Send 0");
return;
}
// 컨텐츠 코드에서 오버로딩
OnSend(numOfBytes);
}
기존에 함수를 선언할 땐 이벤트를 넘겨주지 않았지만, 이벤트를 받을 수 있게 수정했다.
이벤트를 받아 owner 포인터를 밀어주고 이벤트를 제거한다.
만약 0을 보낸 경우라면 연결이 끊긴 것이기 때문에 Disconnect()를 호출해 주고,
OnSend()를 통해 컨텐츠 코드에서 오버로딩 한 동작을 수행할 수 있게 한다.
ProcessRecv()도 위처럼 OnRecv()를 호출하게끔 수정해 줬다.
2-2. 서버 작성
사실 메인은 건드릴 게 없고 기존에 빈칸으로 뒀던 GameSession 클래스에 내용을 채울 것이다.
이번엔 OnRecv()와 OnSend()만 오버라이드 하여 테스트해 보자.
class GameSession : public Session
{
public:
virtual int32 OnRecv(BYTE* buffer, int32 len) override
{
// Echo
cout << "OnRecv Len = " << len << endl;
Send(buffer, len);
return len;
}
virtual void OnSend(int32 len) override
{
cout << "OnSend Len = " << len << endl;
}
};
에코서버의 형태로 동작할 것이므로, OnRecv에서 받은 데이터의 길이를 출력하고,
바로 Send()를 걸어주게 했다.
전송이 완료되면 그 길이를 출력한다.
이렇게 작성이 완료가 됐으면 빌드를 돌리며 미처 수정하지 못한 부분을 수정하며 구동할 수 있게 한다.
2-3. 실행
GameSession 클래스에서 오버라이드 한 대로 서버가 정상적으로 OnSend와 OnRecv를 출력해 준다.
3. (Dis)connect와 클라이언트 재작성
3-1. (Dis)connect
이전에 Disconnect에 관련한 부분들은 정의하지 않았었다.
SocketUtils 클래스를 작성할 때 (Dis)connectEx함수의 포인터를 받아온 부분이 있었는데,
여기선 이제 그 함수를 사용하기로 하자.
먼저 Disconnect라는 이벤트가 새로 생기는 것이므로 IocpEvent 클래스에 그 내용을 추가한다.
class DisconnectEvent : public IocpEvent
{
public:
DisconnectEvent() : IocpEvent(EventType::Disconnect) { }
};
Session으로 돌아와서 외부에서 사용할 (Dis)connect(), Register/Process(Dis)connect() 등의 필요한 함수를 선언한다.
이벤트도 재사용할 것이므로 RecvEvent처럼 관리한다.
bool Session::Connect()
{
// 서버끼리 연결할 때
// 상대방 서버에 내 서버를 붙일 필요가 있음
return RegisterConnect();
}
여기선 별 달리 해줄 게 없다.
Connect를 걸어주기만 하고 그 결과를 리턴한다.
void Session::Disconnect(const WCHAR* cause)
{
if (_connected.exchange(false) == false)
return;
// TEMP
wcout << "Disconnect : " << cause << endl;
OnDisconnected(); // 컨텐츠 코드에서 재정의
GetService()->ReleaseSession(GetSessionRef());
RegisterDisconnect();
}
Disconnect()의 경우는 크게 달라진 건 없지만,
기존에 소켓을 바로 닫아버리는 부분을 대신,
RegisterDisconnect()를 걸어두는 것으로 대체했다.
void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
switch (iocpEvent->eventType)
{
case EventType::Connect:
ProcessConnect();
break;
case EventType::Disconnect:
ProcessDisconnect();
break;
// ...
}
}
Dispatch()에도 해당 분기문을 추가한다.
bool Session::RegisterConnect()
{
if (IsConnected())
return false;
// 타입이 서버면 상대방이 나에게 붙어야 하기 때문
if (GetService()->GetServiceType() != ServiceType::Client)
return false;
// 주소 재사용
if (SocketUtils::SetReuseAddress(_socket, true) == false)
return false;
// 포트에 0을 넘겨주면 남는 포트 중 아무거나 사용
if (SocketUtils::BindAnyAddress(_socket, 0/*남는거*/) == false)
return false;
_connectEvent.Init();
_connectEvent.owner = shared_from_this(); // ADD_REF
DWORD numOfBytes = 0;
SOCKADDR_IN sockAddr = GetService()->GetNetAddress().GetSockAddr();
if (false == SocketUtils::ConnectEx(_socket, reinterpret_cast<SOCKADDR*>(&sockAddr), sizeof(sockAddr), nullptr, 0, &numOfBytes, &_connectEvent))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
_connectEvent.owner = nullptr; // RELEASE_REF
return false;
}
}
return true;
}
늘 하던 대로 연결 여부를 확인한다.
여기서 서버에서 서버로 붙는 것이라도 서비스타입이 클라이언트여야 한다.
서비스가 서버면 다른 쪽에서 나에게 붙어야 하기 때문에 다른 서버에 붙기 위해선 클라이언트여야 한다.
주소를 재사용할 수 있게 하고, 아무 빈 포트를 사용할 수 있도록 한다.
커넥트 이벤트를 만들어 주고, owner에 세션의 포인터를 넘겨준다.
해당 서버의 주소를 넘겨주는 등, 필요한 인자들을 넘겨줘서 ConnectEx()를 걸어준다.
Pending이 아닐 경우 실제로 문제가 생긴 것이기 때문에 Release 해 준다.
이전까지 RegisterConnect()는 void였지만 이번엔 결과를 리턴할 수 있도록 bool로 변경했다.
void Session::ProcessConnect()
{
_connectEvent.owner = nullptr; // RELEASE_REF
_connected.store(true);
// 세션 등록
GetService()->AddSession(GetSessionRef());
// 컨텐츠 코드에서 재정의
OnConnected();
// 수신 등록
RegisterRecv();
}
ProcessConnect()에는 이벤트의 owner를 nullptr로 밀어주는 부분만 추가했다.
기존엔 ConnectEvent가 없었지만, 이제 생겼으므로 밀어주어야 한다.
void Session::ProcessDisconnect()
{
_disconnectEvent.owner = nullptr; // RELEASE_REF
}
ProcessDisconnect()에서는 별 달리 할 일이 없으므로 포인터만 밀어준다.
세션 쪽은 어느 정도 된 것 같으니, Service 클래스로 가자.
클라이언트도 새로 작성된 라이브러리를 사용하는 것으로 재작성할 것이므로,
기존에 비어있던 ClientService클래스의 Start()를 정의한다.
bool ClientService::Start()
{
if (CanStart() == false)
return false;
const int32 sessionCount = GetMaxSessionCount();
for (int32 i = 0; i < sessionCount; i++)
{
SessionRef session = CreateSession();
if (session->Connect() == false)
return false;
}
return true;
}
먼저 SessionFactory가 있는지 확인한다.
MaxSessionCount에 따라 세션을 생성하고, 생성이 정상적으로 생성되었으면 true를 리턴한다.
3-2. 클라이언트 재작성
먼저 ClientService가 본격적으로 생겼으므로 이의 레퍼런스를 받아줄 친구가 필요하다.
// shared_ptr
// ...
using ServerServiceRef = std::shared_ptr<class ServerService>;
using ClientServiceRef = std::shared_ptr<class ClientService>;
ClientServiceRef를 추가했다.
더미 클라이언트의 기존 메인 함수의 내용을 다 지우고 서버의 메인 함수의 내용을 복사해 온다.
int main()
{
this_thread::sleep_for(1s);
ClientServiceRef service = MakeShared<ClientService>(
NetAddress(L"127.0.0.1", 7777),
MakeShared<IocpCore>(),
MakeShared<ServerSession>, // TODO : SessionManager 등
1);
ASSERT_CRASH(service->Start());
for (int32 i = 0; i < 2; i++)
{
GThreadManager->Launch([=]()
{
while (true)
{
service->GetIocpCore()->Dispatch();
}
});
}
GThreadManager->Join();
}
여기서 다른 점이라면 서비스가 클라이언트 서비스이고,
게임 세션이 아니라 서버 세션이라는 점이다.
스레드도 많이는 필요 없으니 2개 정도로 둔다.
그리고 서버 세션에 필요한 함수들을 오버라이드 해서 작성한다.
char sendBuffer[] = "Hello World";
class ServerSession : public Session
{
public:
~ServerSession()
{
cout << "~ServerSession" << endl;
}
virtual void OnConnected() override
{
cout << "Connected To Server" << endl;
Send((BYTE*)sendBuffer, sizeof(sendBuffer));
}
virtual int32 OnRecv(BYTE* buffer, int32 len) override
{
cout << "OnRecv Len = " << len << endl;
this_thread::sleep_for(1s);
Send((BYTE*)sendBuffer, sizeof(sendBuffer));
return len;
}
virtual void OnSend(int32 len) override
{
cout << "OnSend Len = " << len << endl;
}
virtual void OnDisconnected() override
{
cout << "Disconnected" << endl;
}
};
버퍼 내용으론 역시 헬로 월드만 한 게 없다.
각각 연결, 수신, 전송, 연결종료 시에 동작할 함수들에 대해 정의했다.
기본적으로 로그를 뱉게끔 했고, 연결 시에 바로 Send()를 걸어주는 부분이 다른 부분과 다르다 할 수 있다.
클라이언트도 준비가 된 것 같으니 실행해 보자.
3-3. 실행
이번에 주목할 점은 서버나 클라이언트 한쪽의 프로세스가 종료될 시,
정상적으로 연결이 종료되고 세션이 소멸하는지에 대한 여부이다.
클라이언트 프로세스를 종료했더니, 0을 수신받아 연결이 종료되고 게임세션이 소멸됨을 확인할 수 있다.
서버 프로세스를 종료했더니, 에러에 대한 로그를 찍어주는 것을 확인할 수 있다.
서버세션의 정상 소멸도 로그를 통해 확인했다.
4. 돌아보며
먼저 중간에 오버로딩이라고 돼 있는 곳이 있다면 미처 확인하지 못한 나의 실수다...
여기선 오버로딩이 아니라 오버라이딩이다.
거의 등록하고 감지하고 일을 처리하고 등록하고의 패턴이 반복되기 때문에 익숙하면서도,
중간 중간 등장하는 잊고 있었거나 새로운 개념들이 등장하여 한번에 이해하기 어렵기도 하다.
어떻게든 제대로 이해해 보려고 이렇게 글을 작성하는 것이기도 한데, 결코 쉽지 않다.
이번 글의 WSASend()의 Thread Safe에 관한 내용은 아래의 글을 참고했다.
'Study > C++ & C#' 카테고리의 다른 글
[C++] SendBuffer (0) | 2023.06.29 |
---|---|
[C++] RecvBuffer (0) | 2023.06.28 |
[C++] Service (0) | 2023.06.23 |
[C++] IOCP Core (0) | 2023.06.21 |
[C++] Socket util 클래스 작성 (0) | 2023.06.20 |