#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를 읽어와 서버를 연다.

서버에서 수신 버퍼로 사용하기 위해 RingBuffer를 구현했다.

 

🔁 링 버퍼( Ring Buffer )

고정 크기의 원형 큐 형태의 자료구조로, FIFO( First-In-First-Out ) 방식으로 데이터를 저장한다.

 

📦 구조

구성 요소 설명
buffer[] 데이터를 담을 배열( 고정 크기 )
head 데이터를 읽는 위치
rear 데이터를 쓰는 위치

 

 

RingBuffer 클래스

#pragma once

#include <iostream>

using namespace std;

#define BUFFER_DEFAULT_SIZE	100001
#define BLANK				1

#ifdef RINGBUFFER
#define RINGBUFFER_DLL __declspec(dllexport)
#else
#define RINGBUFFER_DLL __declspec(dllimport)
#endif

// 원형 큐
class RINGBUFFER_DLL RingBuffer
{
private:
	char* _buffer;
	int _front;
	int _rear;
	int _bufferMaxSize;

	bool Init(int bufferSize);	
public:
	RingBuffer(void);
	RingBuffer(int bufferSize);
	~RingBuffer();

	int GetBufferSize(void);

	//현재 사용하고 있는 버퍼 크기 반환
	int GetUseSize(void);

	//남아 있는 공간 사이즈
	int GetFreeSize(void);

	// 한번에 enqueue 할 수 있는 사이즈
	int GetDirectEnqueueSize(void);

	// 한번에 Dequeue 할 수 있는 사이즈
	int GetDirectDequeueSize(void);

	// 데이터 넣기
	int Enqueue(char* data, int size);

	// 데이터 빼기
	int Dequeue(char* dest, int size);

	// 데이터가 있는지 확인
	int Peek(char* dest, int size);

	// rear 움직이기
	int MoveRear(int size);

	// front 움직이기
	int MoveFront(int size);

	// ringbuffer 초기화
	void ClearBuffer(void);

	// 비어잇는지 확인
	bool IsEmpty(void);

	// front 위치 반환
	char* GetFrontBufferPtr(void);

	// rear 위치 반환
	char* GetRearBufferPtr(void);

	// buffer 맨앞 반환
	char* GetBufferPtr(void);
};

 

'ChattingServer' 카테고리의 다른 글

[ChattingServer] WorkerThreadProc  (0) 2025.06.02
[ChattingServer] Start 함수  (0) 2025.06.01
[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29

AcceptThreadProc를 구현했다.

클라가 접속하면 session을 할당하고 저장하며 동기로 처리한다.

 

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

	if (instance != nullptr)
	{
		for (;;)
		{
			SOCKADDR_IN clientAddr;
			int addrLen = sizeof(clientAddr);

			// 클라 연결 대기 
			SOCKET clientSock = accept(instance->_listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
			if (clientSock == INVALID_SOCKET)
			{
				DWORD error = WSAGetLastError();
				std::cout << "accept failed : " << error << std::endl;
				break;
			}

			// 연결 수락 총 개수 증가
			instance->_acceptTotal++;
			instance->_acceptTPS++;

			// 세션 할당
			Session* newSession = new Session();			

			newSession->sessionId = ++instance->_sessionId;
			newSession->clientAddr = clientAddr;
			newSession->clientSocket = clientSock;

			// IOCP에 등록
			CreateIoCompletionPort((HANDLE)newSession->clientSocket, instance->_HCP, (ULONG_PTR)newSession, 0);

			instance->OnClientJoin(newSession);

			// sessions에 저장
			instance->_sessions.push_back(newSession);
		}
	}

	return 0;
}

'ChattingServer' 카테고리의 다른 글

[ChattingServer] Start 함수  (0) 2025.06.01
[ChattingServer] RingBuffer ( 원형 큐 )  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29
[ChattingServer] 사용자 버퍼  (0) 2025.05.29

클라가 서버에 접속하면 Session을 할당하고 클라의 정보를 입력한 후 저장한다.

 

Session 구조체

struct Session
{
	LONG sessionId = 0;
	SOCKET clientSocket;
	
	SOCKADDR_IN clientAddr;

	RingBuffer recvRingBuffer; 
	queue<Packet*> sendQueue; 

	OVERLAPPED recvOverlapped = {};
	OVERLAPPED sendOverlapped = {};	

	IOBlock* IOBlock = nullptr;
};

 

sessionId: Session을 구분할 id ( 서버에서 부여 )

clientSocket: 서버에 접속한 클라와 소통할 Socket

clientAddr: 서버에 접속한 클라 주소

recvRingBuffer: 클라가 보낸 데이터를 담아둘 원형큐

sendQueue: 클라가 전송할 데이터를 담아둘 직렬화 버퍼 큐

recvOverlapped: WSARecv 통지용

sendOverlapped: WSASend 통지용

IOBlock: Session에 I/O 작업이 남아 있는지 확인

 

 

IOBlock 구조체

struct IOBlock
{
    LONG64 IOCount = 0;
    LONG64 IsRelease = false;
};

 

IOCount: Session의 I/O 작업을 기록해둘 변수

IsRelase: Relase를 하는지에 대한 여부( = 연결 해제 )

 

IOBlock을 따로 구성해서 Session이 사용중인지 아닌지를 판별해준다.

'ChattingServer' 카테고리의 다른 글

[ChattingServer] RingBuffer ( 원형 큐 )  (0) 2025.06.01
[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29
[ChattingServer] 사용자 버퍼  (0) 2025.05.29
[ChattingServer] 패킷 구조  (0) 2025.05.29
직렬화 버퍼는 구조화된 데이터를 바이트 스트림으로 변환하거나, 반대로 바이트 스트림을 구조화된 데이터로 변환 하기 위해 사용하는 버퍼를 말한다.

 

📦 사용 목적

  1. 네트워크 전송: 구조체 -> 바이트 스트림으로 만들어서 전송
  2. 파일 저장: 바이너리 형태로 저장하기 위해 직렬화 필요
  3. 패킷 시스템: 커스텀 패킷 포맷 구성 시 필수

 

구조체 데이터나 일반 데이터를 바이트 스트림으로 패킷에 담아서 전송해야하기 때문에 직렬화 버퍼가 필요하다.

직렬화 버퍼를 담당하는 클래스를 만들어서 사용한다.

 

#pragma once

#ifdef PACKET
#define PACKET_DLL __declspec(dllexport)
#else
#define PACKET_DLL __declspec(dllimport)
#endif

#define PACKET_BUFFER_DEFAULT_SIZE	100000

// 패킷 클래스
class PACKET_DLL Packet
{
public:
#pragma pack(push,1)
	// 패킷 헤더
	struct EncodeHeader
	{
		unsigned char packetCode;

		unsigned short packetLen;

		unsigned char randXORCode;

		unsigned char checkSum;
	};
#pragma pack(pop)
protected:
	char _packetBuffer[PACKET_BUFFER_DEFAULT_SIZE];

	unsigned int _header;

	unsigned int _front;
	unsigned int _rear;

	unsigned int _bufferSize;
	unsigned int _useBufferSize;

	unsigned char _key;
public:
	Packet();
	~Packet();

	void Clear(void);

	unsigned int GetBufferSize(void);
	unsigned int GetUseBufferSize(void);
	void MoveRearPosition(int size);
	void MoveFrontPosition(int size);

	// 데이터 넣기
	Packet& operator = (Packet& packet);

	template<typename T>
	Packet& operator<<(T& value);

	template<typename T>
	Packet& operator<<(const std::pair<T, int>& value);

    // 데이터 빼기
	template<typename T>
	Packet& operator>>(T& value);

	template<typename T>
	Packet& operator>>(const std::pair<T, int>& value);

	int GetData(char* dest, int size);
	int GetData(wchar_t* dest, int size);

	// 헤더 설정
	void SetHeader(char* header, char size);

	// 패킷 인코딩
	bool Encode(void);
	// 패킷 디코딩
	bool Decode(void);
};

template<typename T>
Packet& Packet::operator<<(T& value)
{
	memcpy(&_packetBuffer[_rear], &value, sizeof(T));
	_rear += sizeof(T);
	_useBufferSize += sizeof(T);

	return *(this);
}

template<typename T>
Packet& Packet::operator<<(const std::pair<T, int>& value)
{
	*this << value.second;
	memcpy(&_packetBuffer[_rear], value.first, value.second);
	_rear += value.second;
	_useBufferSize += value.second;

	return *(this);
}

template<typename T>
Packet& Packet::operator>>(T& value)
{
	memcpy(&value, &_packetBuffer[_front], sizeof(T));
	_front += sizeof(T);
	_useBufferSize -= sizeof(T);

	return *(this);
}

template<typename T>
Packet& Packet::operator>>(const std::pair<T, int>& value)
{
	memcpy(value.first, &_packetBuffer[_front], value.second);
	_front += value.second;
	_useBufferSize -= value.second;

	return *(this);
}

 

연산자 <<를 오버로딩해서 데이터를 쉽게 직렬화버퍼에 담을 수 있도록 한다.

Encode를 통해 직렬화버퍼 안에 있는 데이터를 암호화하고 Decode를 통해 복호화한다.

 

Packet* newPacket = new Packet();

char* stringData = new char[10];
int data = 10;

*newPacket << data;
*newPacket << make_pair(stringData, 10);

 

위와 같이 사용하면 된다.

'ChattingServer' 카테고리의 다른 글

[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 사용자 버퍼  (0) 2025.05.29
[ChattingServer] 패킷 구조  (0) 2025.05.29
[ChattingServer] 채팅 서버 시작  (0) 2025.05.29

네트워크 통신에서 사용자 버퍼가 필요한 이유를 살펴보자.

✅ 비동기 I/O와 데이터 누락 방지

  • 네트워크는 데이터를 순식간에 전송하거나 수신하지 못할 수 있다.
  • OS에서 recv()나 WSARecv() 호출 시 받은 데이터의 양이 프로그램이 기대한 양보다 적을 수 있다.
  • 이런 경우 사용자 버퍼에 누적하여 전체 패킷을 구성해야 한다.

예를 들어

  1. TCP 스트림으로 100바이트를 클라이언트에서 전송
  2. 서버에서 recv() 에서 60바이트만 수신
  3. 나머지 40바이트는 다음 호출 때 수신, 이후 사용자 버퍼가 누락된 데이터를 기억해줘야함

✅ 부분 송수신 대응 ( TCP의 특성 )

  • TCP는 패킷 단위가 아니라 바이트 스트림
  • 한 번의 recv()로 하나의 패킷이 온전히 들어온다는 보장이 없음

✅ 수신 / 송신 처리 분리

  • 사용자 버퍼를 통해 앱 로직과 OS I/O 호출을 분리할 수 있다
  • 수신 데이터를 사용자 버퍼에 모아두고, 패킷 단위로 처리하거나 송신 데이터를 사용자 버퍼에 모아두고, OS 전송이 가능할 때 전송하게 설계 가능

✅ 패킷 조립 / 파싱 편의

  • 수신된 데이터를 사용자 버퍼에 저장하고, 패킷 헤더 확인, 패킷 길이 검사, 완전한 패킷인지 확인, 부족하면 더 받기 와 같은 과정이 가능하려면 임시 저장소가 필요

 

위와 같은 이유로 사용자 버퍼가 필요하다.

 

이번 채팅 서버에 사용할 사용자 버퍼는 원형큐버퍼로 일명 RingBuffer라 불린다.

 

✅ RingBuffer

메모리를 원형으로 사용하는 고정 크기 순환 버퍼다. 네트워크 통신, 스트림 처리 등에 사용되고 FIFO 구조를 가진다.

 

#pragma once

#include <iostream>

using namespace std;

#define BUFFER_DEFAULT_SIZE 100001
#define BLANK               1

#ifdef RINGBUFFER
#define RINGBUFFER_DLL __declspec(dllexport)
#else
#define RINGBUFFER_DLL __declspec(dllimport)
#endif

// 원형 큐
class RINGBUFFER_DLL RingBuffer
{
private:
	char* _buffer;
	int _front;
	int _rear;
	int _bufferMaxSize;

	bool Init(int bufferSize);	
public:
	RingBuffer(void);
	RingBuffer(int bufferSize);
	~RingBuffer();

	int GetBufferSize(void);

	//현재 사용하고 있는 버퍼 크기 반환
	int GetUseSize(void);

	//남아 있는 공간 사이즈
	int GetFreeSize(void);

	// 한번에 enqueue 할 수 있는 사이즈
	int GetDirectEnqueueSize(void);

	// 한번에 Dequeue 할 수 있는 사이즈
	int GetDirectDequeueSize(void);

	// 데이터 넣기
	int Enqueue(char* data, int size);

	// 데이터 빼기
	int Dequeue(char* dest, int size);

	// 데이터가 있는지 확인
	int Peek(char* dest, int size);

	// rear 움직이기
	int MoveRear(int size);

	// front 움직이기
	int MoveFront(int size);

	// ringbuffer 초기화
	void ClearBuffer(void);

	// 비어잇는지 확인
	bool IsEmpty(void);

	// front 위치 반환
	char* GetFrontBufferPtr(void);

	// rear 위치 반환
	char* GetRearBufferPtr(void);

	// buffer 맨앞 반환
	char* GetBufferPtr(void);
};

 

위처럼 RingBuffer를 구성했다. 내부적으로 buffer를 관리한다.

'ChattingServer' 카테고리의 다른 글

[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29
[ChattingServer] 패킷 구조  (0) 2025.05.29
[ChattingServer] 채팅 서버 시작  (0) 2025.05.29
채팅 서버를 구현하기 전에 서버와 클라이언트가 주고 받을 패킷 구조를 설계하려고 한다.

 

✅ 구조

패킷의 형태는 헤더 파일 + 데이터 형태로 구성되고

헤더 파일은 총 5바이트의 크기를 가지며, 아래와 같은 변수로 구성된다.

 

struct EncodeHeader
{
	unsigned char packetCode;

	unsigned short packetLen;

	unsigned char randXORCode;

	unsigned char checkSum;
};

 

  • packetCode: 1차적으로 패킷의 무결성을 확인하는 값으로 상수값으로 정해진다.
  • packetLen: 데이터의 총 길이를 나타낸다. ( 기본적으로 바이너리 데이터이기 때문에 길이가 꼭 필요하다 )
  • randXORCode: 패킷 데이터를 XOR 값을 이용해 암호화 시켜줄때 필요한 값이다.
  • checkSum: XOR로 암호화 시킨 데이터를 기준으로 삼아 checkSum을 구해 데이터 변조 여부를 판단한다.

'ChattingServer' 카테고리의 다른 글

[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29
[ChattingServer] 사용자 버퍼  (0) 2025.05.29
[ChattingServer] 채팅 서버 시작  (0) 2025.05.29

✅ 채팅 서버 

IOCP를 이용해 채팅 서버를 구현하려고 한다.

 

 채팅 서버의 구조

✅ AcceptThread

accept를 담당하여 서버에 접속한 클라들을 대상으로 Session 형식으로 저장한다.

✅ WorkerThread

IOCP의 WorkerThread로 클라가 보낸 모든 데이터들을 받고 해석해 처리한다.

 

 

'ChattingServer' 카테고리의 다른 글

[ChattingServer] AcceptThreadProc  (0) 2025.06.01
[ChattingServer] Session  (0) 2025.05.29
[ChattingServer] 직렬화 버퍼  (0) 2025.05.29
[ChattingServer] 사용자 버퍼  (0) 2025.05.29
[ChattingServer] 패킷 구조  (0) 2025.05.29
미리 컴파일된 헤더( Precompiled Header, PCH )는 컴파일 시간을 단축하기 위해 자주 변경되지 않는 헤더 파일을 미리 컴파일해두고, 이후 빌드 시 다시 컴파일하지 않고 재사용하는 기능을 말한다.

 

✅ 미리 컴파일된 헤더의 개념

  • c++ 프로젝트에서 <windows.h>, <iostream> 같은 무거운 헤더 파일들은 컴파일러가 파싱하는 데 상대적으로 시간이 오래 걸린다.
  • 이러한 헤더들을 한 번만 컴파일하고 결과를 캐시처럼 재사용하면 전체 컴파일 속도가 빨라진다.
  • 이를 위해 컴파일러는 .pch라는 파일로 헤더의 중간 결과물을 저장해 둔다.

 

✅ 사용 방법

// pch.h
#pragma once
#include <iostream>
#include <vector>
#include <Windows.h>

pch용 헤더 파일 생성 ( pch.h )

 

// pch.cpp
#include "pch.h"

pch를 실제로 컴파일할 cpp 파일 생성 ( pch.cpp )

 

 

프로젝트 속성 - C/C++ - 미리 컴파일된 헤더

미리 컴파일된 헤더를 사용(/Yu)

미리 컴파일된 헤더 파일을 pch.h로 설정하고 확인

 

이후 생성된 cpp파일들은 자동으로 pch.h를 포함한다.

✅ 장점

  • 빌드 속도 향상
  • 자주 바뀌지 않는 공통 헤더는 한 번만 컴파일

 

⚠️ 주의할 점

  • pch.h가 변경되면 다시 컴파일되어야 함 ( 자주 바뀌는 헤더는 pch에 포함하지 않는 것이 좋다. )
  • 컴파일 에러가 나면 디버깅 힘듬 ( 헤더의 순서와 포함 관계에 주의 )

✅ c++ 템플릿의 컴파일 원리

템플릿은 컴파일 타임에 인스턴스화된다.

 

c++ 에서 템플릿은 실제로 사용되기 전까지 코드가 생성되지 않는다. 템플릿 함수나 클래스는 일종의 코드 생성기로,

특정 타입이 주어졌을 때 컴파일러가 해당 타입에 맞는 코드를 만들어낸다.

예를 들어

template<typename T>
T add(T a, T b)
{
      return a + b;
}

 

add<int>(1, 2)가 호출될 때, int를 타입으로 가지는 add<int>라는 함수가 생성된다.

 

✅ 헤더에 정의하지 않으면 생기는 문제

만약 위 템플릿을 .cpp 파일에 정의하고, 헤더에는 선언만 해두면 아래와 같은 문제가 발생한다.

 ❗ 컴파일러가 템플릿 정의를 찾을 수 없음

  • 컴파일러는 어떤 타입으로 인스턴스화할지 알 수 없다.
  • 컴파일러가 .cpp 파일을 보지 않고 템플릿의 정의 전체를 알아야 해당 타입에 맞는 코드를 생성할 수 있기 때문

결과적으로 링커 에러가 발생한다.

undefined reference to 'int add<int>(int, int)'

 

✅ 해결 방법: 정의를 헤더에 포함

템플릿은 정의와 선언을 한 파일( 주로 헤더 )에 함께 작성해야 아래와 같이 동작한다.

  • 헤더는 cpp 파일에서 #include 되기 때문에 정의가 항상 접근이 가능
  • 컴파일러는 그 자리에서 템플릿 인스턴스를 직접 생성할 수 있다.
// add.h
template<typename T>
T add(T a, T b) {
    return a + b;
}

 

헤더에 선언과 정의를 동시에 구현

 

#include"add.h"
int result = add<int>(3, 4);

 

위처럼 add함수를 호출하는 순간 컴파일러가 add<int>를 생성한다.

 

 

'언어 > C' 카테고리의 다른 글

[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
[C] 미리 컴파일된 헤더  (0) 2025.05.29
[C] 2진수, 16진수 변환  (0) 2025.04.23
[C] 컴퓨터 덧셈 ( 반가산기, 전가산기 )  (0) 2025.04.23
[C] 게이트 회로  (0) 2025.04.23

+ Recent posts