6/11

채팅 서버를 스트레스 테스트 하기 위해 더미 클라이언트를 만들었다.

사용자로부터 입력을 받아 더미 클라이언트를 생성하고 채팅서버로 접속한 후 일정 시간 마다 정해진 문자열 데이터를 서버로 전송한다. 과부하를 주기 위해 채팅 서버에서는 입력받은 데이터를 접속중인 모든 더미 클라이언트한테 전달한다. 현재는 단순히 접속하고 데이터만 전송하지만 추후에 랜덤하게 접속을 끊고 서버에 다시 접속하여 더 많은 경우를 테스트하려고 생각중이다.

 

 

50개의 더미클라이언트를 접속하고 sendPacketTPS와 recvPacketTPS를 재어보니 아래와 같았다.

 

 


✅ 더미 클라이언트 접속 끊기

더미 클라의 접속을 끊으려고 하다보니 CoreNetwork에서 관리하는 sessions가 문제가 되었다. ( vector로 관리 중 )

 

지금까지는 sessions를 접근할때 어차피 끊는 경우가 없기 때문에 동기화가 필요 없었는데, 이제 연결을 끊어야 하니 동기화 객체가 필요해졌다. 문제는 sessions에 접근해서 session을 찾는 FindSession 함수의 호출 횟수가 매우 잦아서 때문에, 일일이 동기화 객체를 사용하면 오버헤드가 심할 것이라고 자체판단했다. 

 

vector 대신 배열을 사용하고, 배열에 접근하는 index를 따로 관리하는 방법으로 변경해야겠다.

 

이번 채팅서버는 채팅 채널을 생성하고, 해당 채널에 유저들이 접속하고,

해당 채널에 속한 유저들끼리 채팅하는 것을 기본으로 한다. 이에 맞는 프로토콜을 구성했다.

 

#pragma once

enum en_CHATTING_SERVER_PACKET_TYPE
{
	en_CHATTING_SERVER_PACKET = 0,

	//=======================================
	// 하트비트 요청 packet
	// short Type
	//=======================================
	en_CHATTING_SERVER_PACKET_C2S_HEARTBEAT = 1,

	//=======================================
	// 하트비트 응답 packet
	// short Type
	//=======================================
	en_CHATTING_SERVER_PACKET_S2C_HEARTBEAT = 2,

	//=======================================
	// 채팅 요청 packet
	// short Type
	// string Message
	//=======================================
	en_CHATTING_SERVER_PACKET_C2S_CHAT = 3,

	//=======================================
	// 채팅 응답 packet
	// short Type
	// string Message
	//=======================================
	en_CHATTING_SERVER_PACKET_S2C_CHAT = 4,

	//========================================================
	// 채팅채널 생성 요청 packet
	// short Type
	// string ChannelName
	//========================================================
	en_CHATTING_SERVER_PACKET_C2S_CREATE_CHATTING_CHANNEL = 5,

	//========================================================
	// 채팅채널 생성 응답 packet
	// short Type
	// string ChannelName
	//========================================================
	en_CHATTING_SERVER_PACKET_S2C_CREATE_CHATTING_CHANNEL = 6,

	//======================================================
	// 채팅채널 입장 요청 packet
	// short Type
	// string ChannelName
	//======================================================
	en_CHATTING_SERVER_PACKET_C2S_JOIN_CHATTING_CHANNEL = 7,

	//======================================================
	// 채팅채널 입장 응답 packet
	// short Type
	// string ChannelName
	//======================================================
	en_CHATTING_SERVER_PACKET_S2C_JOIN_CHATTING_CHANNEL = 8,

	//========================================================
	// 채팅채널 나가기 요청 packet
	// short Type
	// string ChannelName
	//========================================================
	en_CHATTING_SERVER_PACKET_C2S_LEAVE_CHATTING_CHANNEL = 9,

	//========================================================
	// 채팅채널 나가기 응답 packet
	// short Type	
	//========================================================
	en_CHATTING_SERVER_PACKET_S2C_LEAVE_CHATTING_CHANNEL = 10,

	//================================================
	// 채팅채널 삭제 요청 packet
	// short Type
	// string ChannelName
	//================================================
	en_CHATTING_SERVER_PACKET_C2S_DELETE_CHANNEL = 11,

	//================================================
	// 채팅채널 삭제 응답 packet
	// short Type
	// string ChannelName
	//================================================
	en_CHATTING_SERVER_PACKET_S2C_DELETE_CHANNEL = 12,

	//=======================================================
	// 채팅채널 목록 요청 packet
	// short Type	
	//=======================================================
	en_CHATTING_SERVER_PACKET_C2S_CHATTING_CHANNEL_LIST = 13,

	//=======================================================
	// 채팅채널 목록 응답 packet
	// short Type
	// string[] ChannelList
	//=======================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_LIST = 14,

	//==============================================================
	// 채팅채널 참여자 목록 요청 packet
	// short Type	
	//==============================================================
	en_CHATTING_SERVER_PACKET_C2S_CHATTING_CHANNEL_MEMBER_LIST = 15,

	//==============================================================
	// 채팅채널 참여자 목록 응답 packet
	// short Type	
	// string[] MemberList
	//==============================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_MEMBER_LIST = 16,

	//=============================================================
	// 채팅채널 참여자 추가 요청 packet
	// short Type
	// string ChannelName
	// string MemberName
	//=============================================================
	en_CHATTING_SERVER_PACKET_C2S_CHATTING_CHANNEL_ADD_MEMBER = 17,

	//=============================================================
	// 채팅채널 참여자 추가 응답 packet
	// short Type
	// string ChannelName
	// string MemberName
	//=============================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_ADD_MEMBER = 18,

	//================================================================
	// 채팅채널 참여자 제거 요청 packet
	// short Type
	// string ChannelName
	// string MemberName
	//================================================================
	en_CHATTING_SERVER_PACKET_C2S_CHATTING_CHANNEL_REMOVE_MEMBER = 19,

	//================================================================
	// 채팅채널 참여자 제거 응답 packet
	// short Type
	// string ChannelName
	// string MemberName
	//================================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_REMOVE_MEMBER = 20,

	//=======================================================================
	// 채팅채널 참여자 상태 변경 요청 packet
	// short Type
	// string ChannelName
	// string MemberName
	// int Status (0: offline, 1: online)
	//=======================================================================
	en_CHATTING_SERVER_PACKET_C2S_CHATTING_CHANNEL_MEMBER_STATUS_CHANGE = 21,

	//=======================================================================
	// 채팅채널 참여자 상태 변경 응답 packet
	// short Type
	// string ChannelName
	// string MemberName
	// int Status (0: offline, 1: online)
	//=======================================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_MEMBER_STATUS_CHANGE = 22,

	//=================================================================
	// 채팅채널 메시지 알림 응답 packet
	// short Type
	// string ChannelName	
	// string Message
	//=================================================================
	en_CHATTING_SERVER_PACKET_S2C_CHATTING_CHANNEL_GLOBAL_MESSAGE = 23,
};

 

Session이 가지고 있는 SendQueue는 STL의 큐를 사용하기 때문에 멀티 쓰레드에서 사용하려면, 동기화 객체를 사용해야한다.

앞서 작성한 SendPacket, SendPost, SendComplete 모두 멀티 쓰레드 환경에서 사용하면 위험하다.

 

내부적으로 CriticalSection을 관리하며 Queue에 데이터를 넣고 빼는 클래스를 따로 준비했다.

 

#pragma once
#include"pch.h"
#include"Packet.h"

#define SEND_QUEUE

#ifdef SEND_QUEUE
#define SEND_QUEUE_DLL __declspec(dllexport)
#else
#define SEND_QUEUE_DLL __declspec(dllimport)
#endif

class SEND_QUEUE_DLL SendQueue
{
private:
	queue<Packet*> queue;
	CRITICAL_SECTION cs;
public:
	SendQueue();
	~SendQueue();

	// packet 넣기
	void Push(Packet* packet);

	// packet 빼기
	Packet* Pop();

	// packet이 있는지 확인
	bool IsEmpty();

	// 내부 queue 청소
	void Clear();

	// queue에 있는 packet의 개수 반환
	size_t Size();
};

 

생성자에서 CriticalSection을 초기화 하고, 소멸자에서 삭제한다.

내부 큐에 Packet을 넣고, 뺄때 CriticalSection을 이용해 동기화시켜준다.

 

#pragma once

#include"../Packet.h"
#include"../RingBuffer.h"
#include"../SendQueue.h"

#pragma comment(lib, "..\\x64\\Debug\\NetworkLib.lib")

struct IOBlock
{
	// IO 작업 횟수를 기록해둘 변수 
	// Session을 사용하고 있는지에 대한 여부
	LONG64 IOCount = 0;
	
	// Release를 했는지 안했는지에 대한 여부
	LONG64 IsRelease = false;
};

struct Session
{
	LONG sessionId = 0;
	SOCKET clientSocket; // 클라이언트 소켓
	
	SOCKADDR_IN clientAddr; // 서버에 접속한 클라 주소

	RingBuffer recvRingBuffer; // recvBuf
	SendQueue sendQueue; // sendQueue	

	OVERLAPPED recvOverlapped = {}; // WSARecv 통지
	OVERLAPPED sendOverlapped = {};	// WSASend 통지

	IOBlock* IOBlock = nullptr; // Session이 사용중인지, 해제되었는지 확인
	
	LONG isSend; // 세션에 대해 WSASend 작업을 하고 있는지 안하고 있는지 여부

	vector<Packet*> sendPacket; // 전송할 패킷들을 담아둠
	LONG sendPacketCount; // 전송한 패킷의 개수를 기록
};

 

Session 구조체에서 queue<Packet*> sendQueue 를 삭제하고 SendQueue를 추가시켰다.

Session의 sendQueue를 사용하는 모든 코드에서 새로 작성한 클래스에 맞게 수정했다.

IOCP Send 완료 통지 함수인 SendComplete를 구현했다.

WSASend 완료 통지가 오면, 전송 완료된 패킷을 먼저 정리한다.

isSend을 0으로 바꿔 WSASend를 걸 수 있도록 한다.

 

isSend가 0으로 바뀌기 전에 큐잉된 packet들이 있을 수 있으므로 sendQueue의 크기를 재고 0보다 크면 SendPost를 호출한다.

void CoreNetwork::SendComplete(Session* sendCompleteSession)
{
	// 전송 완료된 패킷을 정리한다.
	for (int i = 0; i < sendCompleteSession->sendPacketCount; i++)
	{
		delete sendCompleteSession->sendPacket[i];
		sendCompleteSession->sendPacket[i] = nullptr;
	}

	sendCompleteSession->sendPacketCount = 0;

	// isSend를 0 으로 바꿔서 WSASend를 걸수 있도록 해준다.
	InterlockedExchange(&sendCompleteSession->isSend, 0);

	// 만약 위에서 바꾸기 전에 큐잉만 하고 빠질 경우
	// 여기서 크기를 검사해서 WSASend를 걸어준다.
	if (sendCompleteSession->sendQueue.size() > 0)
	{
		SendPost(sendCompleteSession);
	}
}

 

WSASend로 packet을 전송하는 과정은 2가지 과정으로 나뉜다.

 

우리가 작성하는 ChattingServer에서 Session은 WSASend를 한번만 걸 수 있다.

즉, 하나의 워커 쓰레드가 WSASend를 걸면 해당 WSASend가 완료될때까지 다른 워커 쓰레드에서는 WSASend를 걸 수 없다.

SendPacket은 여러 쓰레드에서 호출할 수 있기 때문에 SendPacket에서는 packet을 큐잉을 중점으로 구현한다. ( 물론 SendPost를 호출해 WSASend 걸기를 시도 한다 )

1️⃣ SendPacket

WSASend를 걸 session을 찾고, 전송할 packet을 큐잉하고, WSASend를 건다.

 

void CoreNetwork::SendPacket(__int64 sessionId, Packet* packet)
{	
	Session* sendSession = FindSession(sessionId);
	if (sendSession == nullptr)
	{
		return;
	}

	packet->Encode();

	// 패킷 큐잉
	sendSession->sendQueue.push(packet);

	// WSASend 등록
	SendPost(sendSession);

	ReturnSession(sendSession);
}

 

2️⃣ SendPost

WSASend를 걸기전 isSend를 1로 바꾸면서 진입하고, 다른 워커쓰레드에서 isSend가 false가 될때까지 WSASend를 걸지 못하도록 막는다. 보내려고 진입했으나 sendUseSize가 0일 경우 바로나가지 않고 sendQueue의 사이즈를 다시 한번 더 확인해서 SendPost를 다시 진행한다. 

 

이후 전송할 packet을 WSABUF에 담고 WSASend를 건다.

 

void CoreNetwork::SendPost(Session* sendSession)
{
	int sendUseSize;
	int sendCount = 0;
	WSABUF sendBuf[500];

	do
	{
		// Session의 isSend를 1로 바꾸면서 진입한다.
		// 다른 워커 쓰레드가 WSASend를 하지 않도록 막는다.
		if (InterlockedExchange(&sendSession->isSend, 1) == 0)
		{
			sendUseSize = sendSession->sendQueue.size();

			if (sendUseSize == 0)
			{
				InterlockedExchange(&sendSession->isSend, 0);

				if (!sendSession->sendQueue.empty())
				{
					continue;
				}
				else
				{
					return;
				}
			}			
		}
		else
		{
			return;
		}
	} while (0);

	// session이 보내야할 packet의 개수를 지정한다.
	sendSession->sendPacketCount = sendCount = sendUseSize;

	// 보내야할 패킷의 개수만큼 sendQueue에서 패킷을 꺼내서 WSABUF에 넣는다.
	for (int i = 0; i < sendCount; i++)
	{
		if (sendSession->sendQueue.size() == 0)
		{
			break;
		}

		InterlockedIncrement(&_sendPacketTPS);

		Packet* packet = sendSession->sendQueue.front();
		sendSession->sendQueue.pop();
		
		sendBuf[i].buf = packet->GetHeaderBufferPtr();
		sendBuf[i].len = packet->GetUseBufferSize();

		sendSession->sendPacket[i] = packet;
	}

	// WSAsend를 걸기 전에 한번 청소해준다.
	memset(&sendSession->sendOverlapped, 0, sizeof(OVERLAPPED));

	// IOCount를 1 증가시켜 해당 session이 release 대상이 되지 않도록 한다.
	InterlockedIncrement64(&sendSession->IOBlock->IOCount);
		
	int WSASendRetval = WSASend(sendSession->clientSocket, sendBuf, sendCount, NULL, 0, (LPWSAOVERLAPPED)&sendSession->sendOverlapped, NULL);
	if (WSASendRetval == SOCKET_ERROR)
	{
		DWORD error = WSAGetLastError();
		if (error != ERROR_IO_PENDING)
		{
			if (InterlockedDecrement64(&sendSession->IOBlock->IOCount) == 0)
			{
				ReleaseSession(sendSession);
			}
		}
	}
}

 

2025.06.01 - [ChattingServer] - [ChattingServer] RingBuffer ( 원형 큐 )

 

[ChattingServer] RingBuffer ( 원형 큐 )

서버에서 수신 버퍼로 사용하기 위해 RingBuffer를 구현했다. 🔁 링 버퍼( Ring Buffer )고정 크기의 원형 큐 형태의 자료구조로, FIFO( First-In-First-Out ) 방식으로 데이터를 저장한다. 📦 구조구성 요소

program-yam.tistory.com

 

앞서 포스팅한 RingBuffer( 원형 큐 )를 이용해 클라이언트로부터 데이터를 받는다.

원형큐이므로 연속으로 한번에 데이터를 넣을 수 있는 공간과 전체 남은 공간을 비교해서 링버퍼가 2개로 나뉘어있는지 확인해 WSABUF를 구성한다.

 

IOCount를 증가 시켜 지금 session이 사용중임을 기록하고 WSARecv를 건다.

 

void CoreNetwork::RecvPost(Session* recvSession, bool isAcceptRecvPost)
{
	int recvBuffCount = 0;
	WSABUF recvBuf[2];

	// recvRingBuffer에 한번에 넣을 수 있는 크기를 읽는다.
	int directEnqueueSize = recvSession->recvRingBuffer.GetDirectEnqueueSize();
	// 남아 있는 recvRingBuffer의 크기를 읽는다.
	int recvRingBuffFreeSize = recvSession->recvRingBuffer.GetFreeSize();

	// 만약 남아 있는 recvRingBuffer의 크기가 한번에 넣을 수 있는 크기보다 크다면
	// recvRingBuffer가 2개의 공간으로 나뉘어 있는 것을 확인할 수 있다.
	if (recvRingBuffFreeSize > directEnqueueSize)
	{
		recvBuffCount = 2;
		recvBuf[0].buf = recvSession->recvRingBuffer.GetRearBufferPtr();
		recvBuf[0].len = directEnqueueSize;

		recvBuf[1].buf = recvSession->recvRingBuffer.GetBufferPtr();
		recvBuf[1].len = recvRingBuffFreeSize - directEnqueueSize;
	}
	else
	{
		recvBuffCount = 1;
		recvBuf[0].buf = recvSession->recvRingBuffer.GetRearBufferPtr();
		recvBuf[0].len = directEnqueueSize;
	}

	// WSARecv를 걸기 전에 한번 청소해준다.
	memset(&recvSession->recvOverlapped, 0, sizeof(OVERLAPPED));
		
	if (isAcceptRecvPost == false)
	{
		InterlockedIncrement64(&recvSession->IOBlock->IOCount);
	}	

	DWORD flags = 0;

	int WSARecvRetval = WSARecv(recvSession->clientSocket, recvBuf, recvBuffCount, NULL, &flags, (LPWSAOVERLAPPED)&recvSession->recvOverlapped, NULL);
	if (WSARecvRetval == SOCKET_ERROR)
	{
		DWORD error = WSAGetLastError();
		if (error != ERROR_IO_PENDING)
		{
			if (InterlockedDecrement64(&recvSession->IOBlock->IOCount) == 0)
			{
				ReleaseSession(recvSession);
			}
		}
	}
}

IOCP Recv 완료 통지 함수인 RecvComplete를 구현했다.

 

64번을 반복하면서 클라이언트가 보낸 메세지를 확인한다.

 

packet을 처리할때 최소한 헤더 크기만큼은 데이터가 왔는지 먼저 확인한다.

이후 헤더를 뽑아보고 1차로 packetCode의 값을 확인한다.

데이터가 완벽하게 도착하지 않았으면 다음 RecvComplete를 기다리기 위해 반복문을 탈출한다.

이후 패킷을 꺼내서 Decode하고 통과하면 패킷을 처리한다.

 

void CoreNetwork::RecvComplete(Session* recvCompleteSesion, const DWORD& transferred)
{
	int loopCount = 0;
	const int MAX_PACKET_LOOP = 64;
	recvCompleteSesion->recvRingBuffer.MoveRear(transferred);

	Packet::EncodeHeader encodeHeader;
	Packet* packet = new Packet();

	while (loopCount++ < MAX_PACKET_LOOP)
	{
		packet->Clear();

		// 최소한 헤더 크기만큼은 데이터가 왔는지 확인한다.
		if (recvCompleteSesion->recvRingBuffer.GetUseSize() < sizeof(Packet::EncodeHeader))
		{
			break;
		}

		// 헤더를 뽑아본다.
		recvCompleteSesion->recvRingBuffer.Peek((char*)&encodeHeader, sizeof(Packet::EncodeHeader));
		if (encodeHeader.packetLen + sizeof(Packet::EncodeHeader) > recvCompleteSesion->recvRingBuffer.GetUseSize())
		{
			// 1차 패킷 코드인 52값이 아니라면 나감
			if (encodeHeader.packetCode != 52)
			{
				Disconnect(recvCompleteSesion->sessionId);
				break;
			}			
		}
		else
		{
			break;
		}

		InterlockedIncrement(&_recvPacketTPS);

		// 비정상적으로 너무 큰 패킷이 올경우 연결을 끊음
		if (encodeHeader.packetLen > PACKET_BUFFER_DEFAULT_SIZE)
		{
			Disconnect(recvCompleteSesion->sessionId);
			break;
		}

		// 헤더 크기만큼 front를 움직이기
		recvCompleteSesion->recvRingBuffer.MoveFront(sizeof(Packet::EncodeHeader));
		// 패킷 길이만큼 뽑아서 packet에 넣기
		recvCompleteSesion->recvRingBuffer.Dequeue(packet->GetRearBufferPtr(), encodeHeader.packetLen);
		// 헤더 설정
		packet->SetHeader((char*)&encodeHeader, sizeof(Packet::EncodeHeader));
		// 패킷 길이 만큼 rear 움직이기
		packet->MoveRearPosition(encodeHeader.packetLen);

		// 디코딩에 실패하면 연결을 끊음
		if (!packet->Decode())
		{
			Disconnect(recvCompleteSesion->sessionId);
			break;
		}	

		// 패킷 처리
		OnRecv(recvCompleteSesion->sessionId, packet);
	}
	
	delete packet;
}

 

IOCP에서 사용하는 WorkerThread를 구현했다.

기본적으로 GQCS( GetQueuedCompletionStatus )가 return 되기를 대기하고, return된 값에 따라 처리한다.

 

return 된 값이 0일 경우 fin 을 받은 것이므로 종료 처리하고, 0보다 클 경우 적절하게 recv 또는 send 처리한다.

 

unsigned __stdcall CoreNetwork::WorkerThreadProc(void* argument)
{
	CoreNetwork* instance = (CoreNetwork*)argument;

	if (instance != nullptr)
	{
		Session* completeSession = nullptr;
		while (1)
		{
			DWORD transferred = 0;
			OVERLAPPED* myOverlapped = nullptr;
			int completeRet;
			DWORD GQCSError;

			do
			{
				completeRet = GetQueuedCompletionStatus(instance->_HCP, &transferred,
					(PULONG_PTR)&completeSession, (LPOVERLAPPED*)&myOverlapped, INFINITE);
				if (myOverlapped == nullptr)
				{
					GQCSError = WSAGetLastError();
					wcout << L"MyOverlapped NULL " << GQCSError << endl;
					return -1;
				}

				// transferred가 0이라면 fin 패킷을 받은것이므로 종료처리한다.
				if (transferred == 0)
				{
					break;
				}

				// 전달받은 overlapped가 recvOverlapped라면 recv 완료 처리를 진행한다.
				if (myOverlapped == &completeSession->recvOverlapped)
				{
					instance->RecvComplete(completeSession, transferred);
				}
				// 전달받은 overlapped가 sendOverlapped라면 send 완료 처리를 진행한다.
				else if (myOverlapped == &completeSession->sendOverlapped)
				{
					instance->SendComplete(completeSession);
				}
			} while (0);

			// IO 처리가 하나 끝났으므로 IOCount를 감소시킨다.
			if (InterlockedDecrement64(&completeSession->IOBlock->IOCount) == 0)
			{
				instance->ReleaseSession(completeSession);
			}
		}
	}

	return 0;
}

보통 HANDLE을 CloseHandle을 이용해서 닫고 해당 HANDLE에 NULL을 넣는다. 

배울때 습관적으로 넣었는데, 왜 넣어야하는지 그 이유를 알고 싶어졌다.

 

✅ 이중 해제 방지

핸들을 NULL로 만들어 두면 이후 코드에서 if문으로 유효한지 확인하고 다시 닫는 실수를 방지할 수 있다.

 

✅ 사용 후 참조 방지

닫힌 핸들을 NULL로 바꿔두면 잘못된 참조나 오용을 방지할 수 있다.

그렇지 않으면 나중에 그 핸들을 유효한 것처럼 잘못 사용하여 예외나 오류가 발생할 수 있다.

 

✅ 디버깅 시 추적이 쉬움

디버깅 중 핸들이 NULL이면 해당 핸들이 '해제된 상태'라는 것을 쉽게 파악할 수 있다.

 

c++11 이상이라면 nullptr이 더 의미상 명확하고 권장된다.

하지만 HANDLE은 전통적인 C 스타일 핸들이므로, NULL도 문제 없이 동작한다.

다만 resource를 닫고 나서 '이 핸들은 무효하다'는 의도를 표현할 때는 nullptr이 의미적으로는 더 정확하다.

#include"pch.h"
#include"ChattingServer.h"
#include"../Utils.h"
#include<rapidjson/document.h>

ChattingServer gChattingServer;

int main()
{
	std::string jsonStr = Utils::LoadFile(L"ServerInfo.json");	

	rapidjson::Document doc;
	if (doc.Parse(jsonStr.c_str()).HasParseError()) {
		std::cerr << "JSON 파싱 실패\n";
		return 1;
	}
		
	std::string ipAddress = doc["ipAddress"].GetString();
	int port = doc["port"].GetInt();	

	gChattingServer.Start(Utils::Convert(ipAddress).c_str(), port);

   	_setmode(_fileno(stdout), _O_U16TEXT);	

	while (true)
	{
		wcout << L"===================" << endl << endl;
		wcout << L"ChattingServer" << endl << endl;
		wcout << L"acceptTotal : [ " << gChattingServer._acceptTotal << " ]" << endl;
		wcout << L"acceptTPS : [ " << gChattingServer._acceptTPS << " ]" << endl;
		wcout << L"===================";

		gChattingServer._acceptTPS = 0;

		Sleep(1000);

		system("cls");
	}

	return 0;
}

 

main에서 ChattingServer를 선언하고 json으로 ip와 port를 읽어와 서버를 연다.

+ Recent posts