C++로 개발된 게임 서버와 Unity 기반 클라이언트가 안정적으로 통신하려면 적절한 네트워크 프로토콜 설계가 필수적입니다. TCP는 신뢰성 있는 데이터 전송을 보장하지만, 실시간 게임에서는 지연(Latency) 문제를 고려해야 합니다.
본 기사에서는 C++ 서버와 Unity 클라이언트 간 TCP 기반 통신을 구축하는 방법을 설명합니다. 먼저 TCP 통신의 개념과 소켓 프로그래밍 기초를 살펴본 후, 게임 통신에 적합한 프로토콜 설계 원칙을 다룹니다. 또한, 데이터 패킷 직렬화, 오류 처리, 성능 최적화 기법 등을 포함하여 네트워크 통신을 효과적으로 구현하는 방법을 소개합니다. 마지막으로, C++과 Unity를 사용한 구현 예제와 테스트 방법을 통해 실용적인 통신 시스템을 구축할 수 있도록 안내합니다.
TCP 통신의 기본 개념
C++ 게임 서버와 Unity 클라이언트 간 데이터를 주고받기 위해서는 네트워크 프로토콜을 이해해야 합니다. 대표적인 네트워크 프로토콜 중 하나인 TCP(Transmission Control Protocol) 는 신뢰성 있는 데이터 전송을 보장하며, 게임 서버와 클라이언트 간 지속적인 연결을 유지할 수 있도록 합니다.
TCP의 특징
TCP는 연결 지향(Connection-Oriented) 방식으로 작동하며, 데이터 전송의 신뢰성을 보장하기 위해 다음과 같은 기능을 제공합니다.
- 패킷 순서 보장: 패킷이 전송된 순서대로 도착하도록 정렬합니다.
- 오류 감지 및 재전송: 데이터 손실이나 오류가 발생하면 자동으로 재전송합니다.
- 흐름 제어: 네트워크 트래픽을 조절하여 데이터 과부하를 방지합니다.
- 혼잡 제어: 네트워크 상태에 따라 전송 속도를 조절합니다.
이러한 특징 덕분에 TCP는 보안성과 신뢰성이 중요한 게임 통신에서 널리 사용됩니다.
게임 서버-클라이언트 통신 방식
게임 서버와 클라이언트 간 TCP 통신은 일반적으로 다음과 같은 과정으로 이루어집니다.
- 서버 소켓 생성: C++ 서버가 소켓을 생성하고 특정 포트에서 클라이언트의 연결 요청을 대기합니다.
- 클라이언트 접속: Unity 클라이언트가 서버의 IP 주소와 포트를 사용하여 연결을 요청합니다.
- 데이터 송수신: 클라이언트와 서버가 주고받을 데이터 패킷을 정의하고 송수신합니다.
- 연결 유지 및 종료: 클라이언트가 접속을 유지하는 동안 지속적으로 데이터를 주고받으며, 게임 종료 시 연결을 해제합니다.
TCP vs UDP: 선택 기준
게임 네트워크에서는 TCP와 UDP(User Datagram Protocol)를 비교하여 선택해야 합니다.
프로토콜 | 신뢰성 | 속도 | 사용 사례 |
---|---|---|---|
TCP | 높은 신뢰성 보장 | 상대적으로 느림 | 턴제 게임, 채팅, 데이터 동기화 |
UDP | 신뢰성 없음(패킷 손실 가능) | 매우 빠름 | 실시간 멀티플레이, 음성 채팅 |
TCP는 신뢰성이 중요한 게임(예: 카드 게임, MMORPG)에서 사용되며, UDP는 속도가 중요한 실시간 액션 게임(예: FPS, MOBA)에서 주로 사용됩니다. 본 기사에서는 TCP 기반 게임 서버-클라이언트 통신 프로토콜을 다룹니다.
TCP 소켓 프로그래밍 개요
TCP 통신을 구현하려면 클라이언트와 서버 간의 데이터를 주고받기 위한 소켓(Socket) 을 생성해야 합니다. 소켓은 네트워크를 통해 데이터를 송수신하는 기본 단위이며, 서버와 클라이언트가 서로 연결되기 위해 반드시 필요합니다.
이 섹션에서는 C++에서 TCP 서버를 구축하는 방법과 Unity에서 TCP 클라이언트를 구현하는 방법을 간단한 예제와 함께 설명합니다.
C++에서 TCP 서버 소켓 생성
C++에서는 POSIX 소켓(Unix/Linux) 이나 Windows 소켓(WinSock2) 라이브러리를 사용하여 TCP 서버를 구현할 수 있습니다. 아래는 POSIX 소켓을 이용한 기본적인 TCP 서버 코드입니다.
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define PORT 8080
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
socklen_t addrlen = sizeof(address);
char buffer[1024] = {0};
// 1. 소켓 생성
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "소켓 생성 실패" << std::endl;
return -1;
}
// 2. 주소 설정
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 3. 바인딩
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
// 4. 연결 대기
listen(server_fd, 3);
std::cout << "클라이언트 연결 대기 중..." << std::endl;
new_socket = accept(server_fd, (struct sockaddr*)&address, &addrlen);
// 5. 데이터 수신 및 전송
read(new_socket, buffer, 1024);
std::cout << "클라이언트로부터 받은 메시지: " << buffer << std::endl;
send(new_socket, "Hello from server", 17, 0);
// 6. 연결 종료
close(new_socket);
close(server_fd);
return 0;
}
위 코드의 주요 동작 과정은 다음과 같습니다.
- 소켓 생성 –
socket()
함수를 호출하여 TCP 소켓을 생성합니다. - 주소 설정 및 바인딩 –
bind()
함수를 사용하여 특정 포트에 서버를 연결합니다. - 연결 대기 –
listen()
함수를 사용하여 클라이언트의 연결을 대기합니다. - 클라이언트 연결 수락 –
accept()
를 호출하여 클라이언트의 연결 요청을 처리합니다. - 데이터 송수신 –
read()
와send()
를 사용하여 클라이언트와 데이터를 주고받습니다. - 연결 종료 –
close()
를 호출하여 소켓을 닫습니다.
Unity에서 TCP 클라이언트 구현
Unity에서는 C#의 System.Net.Sockets
네임스페이스를 활용하여 TCP 클라이언트를 만들 수 있습니다.
using System;
using System.Net.Sockets;
using System.Text;
class TCPClientExample {
static void Main() {
try {
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
// 서버에 메시지 전송
byte[] message = Encoding.UTF8.GetBytes("Hello from Unity Client");
stream.Write(message, 0, message.Length);
// 서버로부터 메시지 수신
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
Console.WriteLine("서버로부터 응답: " + Encoding.UTF8.GetString(buffer, 0, bytesRead));
// 연결 종료
stream.Close();
client.Close();
} catch (Exception e) {
Console.WriteLine("오류: " + e.Message);
}
}
}
이 코드에서는 Unity 클라이언트가 C++ 서버와 연결한 후, 간단한 문자열 메시지를 주고받습니다.
서버-클라이언트 실행 순서
- 먼저 C++ 서버를 실행하여 클라이언트의 연결 요청을 대기합니다.
- 그 후 Unity 클라이언트를 실행하여 서버에 연결을 요청하고 메시지를 전송합니다.
- 서버는 클라이언트의 메시지를 수신한 후, 응답을 보내고 연결을 종료합니다.
이제 TCP 소켓을 활용하여 C++ 서버와 Unity 클라이언트를 연결하는 기초적인 구조를 이해할 수 있습니다. 다음 섹션에서는 게임 통신 프로토콜을 설계하는 원칙에 대해 살펴보겠습니다.
게임 통신 프로토콜 설계 원칙
C++ 서버와 Unity 클라이언트 간 TCP 통신을 원활하게 하기 위해서는 효율적이고 확장 가능한 네트워크 프로토콜을 설계해야 합니다. 게임 통신 프로토콜을 설계할 때 고려해야 할 핵심 요소들을 정리합니다.
1. 패킷 기반 데이터 전송
TCP는 신뢰성 있는 전송을 보장하지만, 한 번의 send()
호출이 반드시 한 번의 recv()
로 대응되지 않는 문제가 있습니다. 따라서 게임 통신에서는 패킷 단위로 데이터를 주고받는 것이 중요합니다.
해결 방법:
- 패킷에 헤더(길이 정보 포함) + 데이터 바디 구조를 사용
- 패킷 크기를 지정하여 데이터 경계를 명확하게 구분
- 직렬화(Serialization) 기법을 적용하여 일관된 데이터 형식을 유지
예제 패킷 구조 (C++ 기준):
struct PacketHeader {
uint16_t packetSize; // 패킷 전체 크기
uint16_t packetType; // 패킷 유형
};
2. 명확한 패킷 타입 정의
서버와 클라이언트 간 다양한 데이터를 주고받기 위해 패킷 유형(Packet Type) 을 정의해야 합니다.
예제 패킷 타입 정의 (C++ 기준):
enum PacketType {
LOGIN_REQUEST = 1,
LOGIN_RESPONSE = 2,
PLAYER_MOVE = 3,
CHAT_MESSAGE = 4
};
패킷 유형을 미리 정의하면, 클라이언트와 서버가 같은 프로토콜을 유지하며 안정적으로 통신할 수 있습니다.
3. 데이터 직렬화 및 역직렬화
클라이언트와 서버는 서로 다른 환경(C++ ↔ C#)에서 실행되므로, 데이터 변환(Serialization)이 필요합니다.
JSON, Protobuf, FlatBuffers 등의 직렬화 방식을 사용할 수 있으며, 네트워크 전송에 최적화된 바이너리 직렬화(Binary Serialization) 를 사용하는 것이 성능 면에서 유리합니다.
예제: C++에서 직렬화된 데이터 송신
void sendPacket(int socket, const void* data, size_t size) {
send(socket, data, size, 0);
}
Unity C#에서 역직렬화하여 수신
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
PacketHeader header = ByteArrayToHeader(buffer);
4. 클라이언트 인증 및 보안
게임에서는 클라이언트가 서버를 속이거나 부정 접속을 시도하는 경우가 많으므로, 보안 프로토콜을 강화해야 합니다.
보안 프로토콜 구현 방법:
- 암호화: AES, RSA 등을 활용하여 데이터 암호화
- 인증 시스템: 로그인 시 토큰 기반 인증(JWT, OAuth 등) 사용
- 패킷 무결성 검증: 해커가 패킷을 조작하는 것을 방지하기 위해 HMAC(Hash-based Message Authentication Code) 적용
5. 효율적인 패킷 처리 방식
서버는 많은 클라이언트의 패킷을 처리해야 하므로, 성능을 최적화해야 합니다.
- 비동기 소켓 프로그래밍 사용 (Non-Blocking I/O, epoll, select)
- 멀티스레딩 또는 멀티플렉싱 적용하여 동시 접속 처리
- 패킷 큐 시스템을 사용하여 처리 순서를 관리
결론
효율적인 게임 네트워크 프로토콜을 설계하려면 패킷 구조, 직렬화 방식, 보안, 성능 최적화를 고려해야 합니다. 다음 섹션에서는 구체적인 패킷 구조 및 직렬화 구현 방법을 살펴보겠습니다.
패킷 구조 정의 및 직렬화
C++ 서버와 Unity 클라이언트 간 데이터를 주고받으려면 일관된 패킷 구조를 설계해야 합니다. 또한, 네트워크 전송을 위해 데이터를 직렬화(Serialization) 및 역직렬화(Deserialization) 하는 과정이 필요합니다.
이 섹션에서는 효율적인 패킷 구조 설계 원칙과 C++ 및 C#에서 직렬화 기법을 적용하는 방법을 설명합니다.
1. 패킷 구조 설계
패킷 구조는 헤더(Header) + 데이터 바디(Body) 로 구성됩니다.
패킷 설계 원칙
- 고정 크기 헤더 사용 (패킷 크기, 타입 등 포함)
- 변동 크기 바디 지원 (데이터 길이 포함)
- 바이트 정렬(Padding) 방지를 위해
#pragma pack(1)
사용 - 엔디안(Endian) 변환 고려 (네트워크는 빅 엔디안, 일부 시스템은 리틀 엔디안)
C++에서의 패킷 구조 예제:
#pragma pack(1) // 패딩 제거
struct PacketHeader {
uint16_t packetSize; // 전체 패킷 크기
uint16_t packetType; // 패킷 유형
};
예제: 로그인 요청 패킷
struct LoginRequestPacket {
PacketHeader header;
char username[20];
char password[20];
};
이제 패킷을 직렬화하여 네트워크로 전송할 수 있습니다.
2. C++에서 직렬화 및 전송
패킷을 네트워크로 전송하기 전에 바이너리 형식으로 변환(직렬화) 해야 합니다.
void sendPacket(int socket, const void* packet, size_t size) {
send(socket, packet, size, 0);
}
로그인 요청 패킷을 전송하는 예제:
LoginRequestPacket loginPacket = {};
loginPacket.header.packetSize = sizeof(LoginRequestPacket);
loginPacket.header.packetType = 1;
strcpy(loginPacket.username, "player1");
strcpy(loginPacket.password, "mypassword");
sendPacket(clientSocket, &loginPacket, sizeof(LoginRequestPacket));
서버는 클라이언트로부터 받은 데이터를 역직렬화하여 사용할 수 있습니다.
3. Unity(C#)에서 직렬화 및 전송
Unity에서 C#을 사용하여 동일한 패킷 구조를 구현하려면 BitConverter
를 사용합니다.
class LoginRequestPacket {
public ushort packetSize;
public ushort packetType;
public string username;
public string password;
}
바이너리 데이터로 변환(직렬화)
byte[] SerializeLoginPacket(LoginRequestPacket packet) {
List<byte> data = new List<byte>();
data.AddRange(BitConverter.GetBytes(packet.packetSize));
data.AddRange(BitConverter.GetBytes(packet.packetType));
data.AddRange(Encoding.UTF8.GetBytes(packet.username.PadRight(20, '\0')));
data.AddRange(Encoding.UTF8.GetBytes(packet.password.PadRight(20, '\0')));
return data.ToArray();
}
TCP 소켓을 통해 전송
NetworkStream stream = client.GetStream();
byte[] packetData = SerializeLoginPacket(loginPacket);
stream.Write(packetData, 0, packetData.Length);
4. 패킷 역직렬화
수신된 데이터는 다시 구조체로 변환해야 합니다.
C++ 서버에서 역직렬화
void receivePacket(int socket) {
char buffer[1024];
recv(socket, buffer, sizeof(buffer), 0);
LoginRequestPacket* packet = (LoginRequestPacket*)buffer;
std::cout << "Username: " << packet->username << std::endl;
}
C# 클라이언트에서 역직렬화
byte[] buffer = new byte[1024];
stream.Read(buffer, 0, buffer.Length);
ushort size = BitConverter.ToUInt16(buffer, 0);
ushort type = BitConverter.ToUInt16(buffer, 2);
string username = Encoding.UTF8.GetString(buffer, 4, 20).TrimEnd('\0');
결론
- 고정 크기 헤더 + 변동 크기 바디 구조 사용
- 직렬화하여 바이너리 형태로 전송
- C++ 서버와 C# Unity 클라이언트 간 패킷 구조를 일치
- 엔디안 변환, 패딩 문제 고려
다음 섹션에서는 패킷의 전송 흐름과 처리 방식을 설명합니다.
데이터 전송 및 패킷 처리
C++ 서버와 Unity 클라이언트가 데이터를 주고받기 위해서는 패킷을 생성하고 전송하는 과정이 필요합니다. 이 섹션에서는 패킷 전송 흐름, 송수신 처리 방식, 그리고 패킷 큐 시스템을 활용한 처리 방법을 설명합니다.
1. 패킷 전송 흐름
TCP 기반의 클라이언트-서버 모델에서 데이터 패킷 전송은 다음과 같은 흐름을 따릅니다.
- 클라이언트가 서버에 연결 요청 (
connect
) - 서버가 연결을 수락 (
accept
) - 클라이언트가 서버로 패킷을 송신 (
send
) - 서버가 패킷을 수신하여 처리 (
recv
) - 서버가 응답 패킷을 클라이언트로 송신
- 클라이언트가 응답을 수신하고 데이터 처리
2. C++ 서버에서 패킷 수신
TCP 연결이 유지되는 동안 서버는 클라이언트로부터 패킷을 지속적으로 수신해야 합니다.
void receivePacket(int clientSocket) {
char buffer[1024];
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
PacketHeader* header = (PacketHeader*)buffer;
std::cout << "패킷 수신 - 타입: " << header->packetType << std::endl;
// 패킷 타입에 따라 처리
switch (header->packetType) {
case LOGIN_REQUEST:
handleLoginRequest(clientSocket, buffer);
break;
case PLAYER_MOVE:
handlePlayerMove(clientSocket, buffer);
break;
}
}
}
핵심 개념:
recv()
를 사용하여 클라이언트에서 데이터를 읽어옴.PacketHeader
를 먼저 읽고 패킷 유형을 확인.- 적절한 핸들러 함수에서 데이터를 해석하고 처리.
3. Unity 클라이언트에서 패킷 송신
Unity(C#)에서는 NetworkStream.Write()
를 사용하여 데이터를 전송할 수 있습니다.
void SendPacket(NetworkStream stream, byte[] packetData) {
stream.Write(packetData, 0, packetData.Length);
stream.Flush();
}
로그인 요청 패킷 전송 예제:
LoginRequestPacket loginPacket = new LoginRequestPacket {
packetSize = (ushort)24,
packetType = LOGIN_REQUEST,
username = "Player1",
password = "pass123"
};
byte[] data = SerializeLoginPacket(loginPacket);
SendPacket(clientStream, data);
4. 서버에서 클라이언트로 응답 전송
서버가 클라이언트의 요청을 처리한 후 응답을 보낼 수도 있습니다.
void sendLoginResponse(int clientSocket, bool success) {
struct LoginResponsePacket {
PacketHeader header;
uint8_t success;
} response;
response.header.packetSize = sizeof(LoginResponsePacket);
response.header.packetType = LOGIN_RESPONSE;
response.success = success ? 1 : 0;
send(clientSocket, &response, sizeof(response), 0);
}
Unity에서 응답을 수신하는 방법:
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0) {
ushort packetType = BitConverter.ToUInt16(buffer, 2);
if (packetType == LOGIN_RESPONSE) {
bool success = buffer[4] == 1;
Debug.Log("로그인 " + (success ? "성공" : "실패"));
}
}
5. 패킷 큐 시스템 활용
멀티스레드 환경에서는 패킷을 큐(Queue)에 저장한 후, 메인 스레드에서 처리하는 방식이 일반적입니다.
std::queue<std::vector<char>> packetQueue;
std::mutex queueMutex;
void receivePacket(int clientSocket) {
char buffer[1024];
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
std::lock_guard<std::mutex> lock(queueMutex);
packetQueue.push(std::vector<char>(buffer, buffer + bytesReceived));
}
}
void processPackets() {
while (!packetQueue.empty()) {
std::lock_guard<std::mutex> lock(queueMutex);
std::vector<char> packet = packetQueue.front();
packetQueue.pop();
PacketHeader* header = (PacketHeader*)packet.data();
std::cout << "패킷 처리: " << header->packetType << std::endl;
}
}
장점:
- 비동기 방식으로 패킷을 처리하여 성능 향상
- 메인 게임 루프와 네트워크 처리를 분리하여 안정성 확보
결론
- TCP 기반 패킷 송수신을 위해 send/recv를 활용
- 패킷 큐 시스템을 사용하여 멀티스레드 환경에서 성능 최적화
- 클라이언트 요청을 처리한 후 응답 패킷을 반환
- Unity와 C++ 서버 간 일관된 패킷 구조 유지
다음 섹션에서는 TCP 연결 관리 및 오류 처리 방법을 살펴보겠습니다.
연결 관리 및 오류 처리
TCP 기반 C++ 게임 서버와 Unity 클라이언트 간의 원활한 통신을 위해서는 연결 유지, 재연결, 패킷 손실, 지연 문제 해결 등의 오류 처리가 필수적입니다. 본 섹션에서는 안정적인 TCP 연결을 유지하는 방법과 네트워크 오류에 대한 대응 전략을 설명합니다.
1. TCP 연결 유지 전략
게임 서버와 클라이언트 간 연결을 지속적으로 유지하려면 연결 유지(Keep-Alive) 및 주기적인 핑(Ping) 패킷을 사용하는 것이 중요합니다.
연결 유지 방법:
- TCP Keep-Alive 옵션 활성화
- 주기적인 핑(Ping) 메시지 전송
- 클라이언트가 일정 시간 응답하지 않으면 타임아웃 처리
C++에서 TCP Keep-Alive 설정
int keepAlive = 1;
int keepIdle = 10; // 연결 유지를 위한 유휴 시간 (초)
int keepInterval = 5; // Keep-Alive 패킷 간격 (초)
int keepCount = 3; // Keep-Alive 패킷을 보낼 최대 횟수
setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive));
setsockopt(socket_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle));
setsockopt(socket_fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(keepInterval));
setsockopt(socket_fd, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(keepCount));
이 설정을 사용하면, 서버가 클라이언트의 연결 상태를 감지할 수 있습니다.
2. 재연결 전략
네트워크 문제로 인해 연결이 끊어질 경우, 자동 재연결 기능을 구현해야 합니다.
Unity에서 자동 재연결 (C# 코드 예제)
async Task Reconnect() {
while (!client.Connected) {
try {
client = new TcpClient("127.0.0.1", 8080);
Debug.Log("서버 재연결 성공");
return;
} catch {
Debug.Log("서버 재연결 시도 중...");
await Task.Delay(3000); // 3초 후 재시도
}
}
}
서버가 다운되거나 네트워크 연결이 끊어지면 클라이언트가 3초마다 자동으로 재연결을 시도합니다.
3. 패킷 손실 및 지연 처리
TCP는 신뢰성 있는 전송을 보장하지만, 네트워크 상황에 따라 패킷이 지연될 수 있습니다. 이를 해결하기 위해 타임스탬프를 활용한 패킷 유효성 검사를 수행할 수 있습니다.
C++에서 패킷에 타임스탬프 추가
struct Packet {
PacketHeader header;
uint64_t timestamp; // UNIX 타임스탬프
};
Unity에서 패킷을 수신한 후, 지연 시간이 너무 크면 무시할 수 있습니다.
ulong receivedTime = BitConverter.ToUInt64(buffer, 4);
ulong currentTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (currentTime - receivedTime > 5000) {
Debug.Log("패킷 지연됨, 무시");
} else {
Debug.Log("정상 패킷 처리");
}
이 방식은 서버와 클라이언트 간 패킷 유효성 검사에 유용합니다.
4. 네트워크 장애 감지 및 복구
네트워크 연결이 불안정할 경우, 서버가 클라이언트의 연결 상태를 감지하고 적절한 조치를 취해야 합니다.
서버에서 비정상 종료 감지 (C++ 코드)
void checkClientConnection(int clientSocket) {
char buffer[1];
int result = recv(clientSocket, buffer, 1, MSG_PEEK);
if (result == 0) {
std::cout << "클라이언트 연결 끊김" << std::endl;
close(clientSocket);
}
}
클라이언트가 연결을 종료하면 recv()
호출이 0을 반환하므로 이를 감지하여 소켓을 닫을 수 있습니다.
5. 오류 처리 전략
네트워크 장애 발생 시, 클라이언트와 서버가 적절하게 대응할 수 있도록 예외 처리를 구현해야 합니다.
C++ 예외 처리 코드 (소켓 오류 감지)
if (send(clientSocket, data, dataSize, 0) == -1) {
std::cerr << "데이터 전송 실패, 클라이언트 상태 확인 필요" << std::endl;
}
Unity에서 네트워크 오류 감지 (C# 예제)
try {
stream.Write(data, 0, data.Length);
} catch (Exception e) {
Debug.Log("네트워크 오류 발생: " + e.Message);
StartCoroutine(Reconnect()); // 자동 재연결 실행
}
이러한 방식으로 네트워크 장애를 감지하고 빠르게 대응할 수 있습니다.
결론
- TCP 연결을 유지하기 위해 Keep-Alive 및 핑(Ping) 메시지 사용
- 연결이 끊어질 경우 자동 재연결 기능 구현
- 타임스탬프를 활용한 패킷 지연 감지
- 서버가 클라이언트 연결 상태를 실시간 감지하여 복구 조치
- 예외 처리를 통해 네트워크 오류에 강한 게임 서버 구현
다음 섹션에서는 성능 최적화 기법을 살펴보겠습니다.
성능 최적화 기법
C++ 게임 서버와 Unity 클라이언트 간 TCP 통신을 최적화하면 네트워크 지연 최소화, CPU 사용량 감소, 대역폭 절약 등의 효과를 얻을 수 있습니다. 이 섹션에서는 Nagle 알고리즘 조정, 패킷 크기 최적화, 멀티스레딩 적용, 비동기 I/O 활용 등의 최적화 기법을 설명합니다.
1. Nagle 알고리즘 비활성화
Nagle 알고리즘은 작은 패킷을 모아서 전송하여 네트워크 트래픽을 줄이는 기능이지만, 게임 통신에서는 즉각적인 데이터 전송이 필요하므로 이를 비활성화해야 합니다.
C++에서 Nagle 알고리즘 비활성화 (TCP_NODELAY 옵션 설정)
int flag = 1;
setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
Unity(C#)에서 Nagle 알고리즘 비활성화
client.NoDelay = true;
이를 적용하면 입력 지연(Latency)이 줄어들고, 즉각적인 패킷 전송이 가능해집니다.
2. 패킷 크기 최적화
네트워크 대역폭을 절약하려면 패킷 크기를 최소화해야 합니다.
최적화 방법:
- 불필요한 데이터 제거 (예:
float
대신uint16_t
사용) - 중복되는 데이터는 압축하여 전송 (Zlib, LZ4 등)
- 가변 길이 데이터는 압축 문자열(String Compression) 활용
C++에서 패킷 압축 (Zlib 사용 예제)
#include <zlib.h>
void compressData(const char* input, size_t inputSize, char* output, size_t* outputSize) {
compress((Bytef*)output, outputSize, (const Bytef*)input, inputSize);
}
Unity에서 압축 활용 (C# DeflateStream)
using System.IO;
using System.IO.Compression;
byte[] Compress(byte[] data) {
using (var output = new MemoryStream()) {
using (var gzip = new DeflateStream(output, CompressionMode.Compress))
gzip.Write(data, 0, data.Length);
return output.ToArray();
}
}
압축을 적용하면 패킷 크기를 최대 50% 이상 줄일 수 있어 네트워크 성능이 향상됩니다.
3. 비동기 I/O 활용
멀티플레이어 게임 서버는 여러 클라이언트의 요청을 동시에 처리해야 하므로, 비동기(Non-Blocking) I/O 를 적용하면 성능이 크게 향상됩니다.
C++에서 epoll을 사용한 비동기 소켓 처리 (Linux 환경)
#include <sys/epoll.h>
int epoll_fd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = clientSocket;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clientSocket, &ev);
while (true) {
struct epoll_event events[10];
int numEvents = epoll_wait(epoll_fd, events, 10, -1);
for (int i = 0; i < numEvents; i++) {
handleClient(events[i].data.fd);
}
}
Unity에서 비동기 소켓 사용 (C# BeginRead
/ BeginWrite
)
stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(OnDataReceived), stream);
비동기 I/O를 적용하면 CPU 사용률을 낮추고 대량의 클라이언트를 처리하는 서버 성능이 향상됩니다.
4. 멀티스레딩 적용
싱글스레드 서버는 한 클라이언트의 요청이 오래 걸리면 다른 요청도 지연되는 문제가 발생합니다. 따라서 멀티스레딩(Thread Pool) 방식을 적용해야 합니다.
C++에서 멀티스레드 서버 구현 (std::thread 활용)
#include <thread>
void handleClient(int clientSocket) {
std::thread clientThread([clientSocket]() {
processClient(clientSocket);
});
clientThread.detach(); // 백그라운드에서 실행
}
Unity에서 멀티스레딩 적용 (C# Task.Run
)
Task.Run(() => {
HandleNetworkEvents();
});
멀티스레딩을 적용하면 게임 서버의 응답 속도가 빨라지고, 동시 접속자가 많아도 안정적인 처리가 가능합니다.
5. 패킷 병합 및 배치 처리
작은 패킷을 너무 자주 보내면 네트워크 부하(Network Overhead) 가 발생하므로, 여러 패킷을 묶어서 한 번에 보내는 패킷 병합(Packet Batching) 기법을 사용하면 성능이 개선됩니다.
C++에서 패킷 병합 적용 예제
std::vector<char> packetBuffer;
void sendBatchedPackets(int socket) {
if (!packetBuffer.empty()) {
send(socket, packetBuffer.data(), packetBuffer.size(), 0);
packetBuffer.clear();
}
}
Unity에서 병합 패킷 처리 예제
List<byte[]> packetQueue = new List<byte[]>();
void SendBatchedPackets() {
foreach (var packet in packetQueue) {
stream.Write(packet, 0, packet.Length);
}
packetQueue.Clear();
}
패킷을 배치 처리하면 네트워크 트래픽을 최소화하여 성능을 최적화할 수 있습니다.
결론
- Nagle 알고리즘 비활성화로 네트워크 지연 최소화
- 데이터 압축 및 패킷 크기 최적화를 통해 대역폭 절약
- 비동기 I/O(epoll,
BeginRead
) 활용으로 CPU 사용량 감소 - 멀티스레딩 적용하여 동시 접속자 처리 능력 향상
- 패킷 병합 및 배치 처리로 네트워크 부하 감소
다음 섹션에서는 구체적인 구현 예제 및 테스트 방법을 살펴보겠습니다.
구현 예제 및 테스트
이 섹션에서는 C++ 서버와 Unity 클라이언트 간 TCP 통신을 직접 구현하고 테스트하는 방법을 설명합니다. 실제 네트워크 환경에서 동작하는 샘플 코드를 제공하며, 데이터 송수신 및 패킷 처리 테스트를 수행합니다.
1. C++ 서버 구현
기능:
- TCP 서버를 실행하여 Unity 클라이언트의 연결을 수락
- 클라이언트로부터 패킷을 수신하고, 응답을 전송
C++ 서버 코드 (POSIX 소켓 사용)
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define PORT 8080
struct PacketHeader {
uint16_t packetSize;
uint16_t packetType;
};
struct LoginRequestPacket {
PacketHeader header;
char username[20];
char password[20];
};
void handleClient(int clientSocket) {
char buffer[1024];
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
LoginRequestPacket* packet = (LoginRequestPacket*)buffer;
std::cout << "로그인 요청 수신: " << packet->username << std::endl;
// 응답 패킷 전송
PacketHeader response;
response.packetSize = sizeof(PacketHeader);
response.packetType = 2; // 로그인 응답 패킷 타입
send(clientSocket, &response, sizeof(response), 0);
}
close(clientSocket);
}
int main() {
int serverSocket, clientSocket;
sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
listen(serverSocket, 5);
std::cout << "서버 실행 중... 클라이언트 연결 대기" << std::endl;
while ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen))) {
std::cout << "클라이언트 연결됨" << std::endl;
handleClient(clientSocket);
}
close(serverSocket);
return 0;
}
2. Unity 클라이언트 구현
기능:
- C++ 서버에 접속하고 로그인 요청 패킷을 전송
- 서버로부터 응답을 수신
Unity C# 클라이언트 코드
using System;
using System.Net.Sockets;
using System.Text;
class TCPClientExample {
static void Main() {
try {
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
// 로그인 요청 패킷 생성
byte[] username = Encoding.UTF8.GetBytes("Player1".PadRight(20, '\0'));
byte[] password = Encoding.UTF8.GetBytes("pass123".PadRight(20, '\0'));
byte[] packet = new byte[4 + 20 + 20]; // 헤더(4바이트) + 데이터(40바이트)
BitConverter.GetBytes((ushort)packet.Length).CopyTo(packet, 0);
BitConverter.GetBytes((ushort)1).CopyTo(packet, 2); // 로그인 요청 타입
username.CopyTo(packet, 4);
password.CopyTo(packet, 24);
// 서버로 패킷 전송
stream.Write(packet, 0, packet.Length);
// 응답 수신
byte[] buffer = new byte[4];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
ushort packetType = BitConverter.ToUInt16(buffer, 2);
Console.WriteLine("서버 응답 수신: 패킷 타입 " + packetType);
stream.Close();
client.Close();
} catch (Exception e) {
Console.WriteLine("오류: " + e.Message);
}
}
}
3. 테스트 및 실행 방법
1단계: C++ 서버 실행
g++ server.cpp -o server
./server
출력 예시:
서버 실행 중... 클라이언트 연결 대기
클라이언트 연결됨
로그인 요청 수신: Player1
2단계: Unity 클라이언트 실행
dotnet run TCPClientExample.cs
출력 예시:
서버 응답 수신: 패킷 타입 2
4. 테스트 자동화
서버와 클라이언트의 네트워크 테스트를 자동화하려면, ping 테스트 및 대량 클라이언트 연결 테스트를 수행할 수 있습니다.
C++에서 다중 클라이언트 시뮬레이션
for (int i = 0; i < 10; i++) {
std::thread([]() {
system("./client"); // 여러 클라이언트 실행
}).detach();
}
Unity에서 다중 클라이언트 실행
for (int i = 0; i < 10; i++) {
Task.Run(() => StartClient());
}
이 방법을 사용하면 동시 접속자를 테스트하고 성능을 측정할 수 있습니다.
결론
- C++ 서버와 Unity 클라이언트 구현하여 직접 테스트
- TCP 패킷을 주고받는 실용적인 예제 제공
- 대량 클라이언트 접속 시뮬레이션 테스트 가능
- 네트워크 환경에서 안정적인 데이터 송수신 확인
다음 섹션에서는 전체 내용을 정리하는 요약을 제공합니다.
요약
본 기사에서는 C++ 게임 서버와 Unity 클라이언트 간 TCP 통신 프로토콜을 설계하고 구현하는 방법을 설명했습니다.
핵심 내용은 다음과 같습니다:
- TCP 통신 개념 및 게임 서버-클라이언트 간 데이터 전송 방식
- C++에서 TCP 소켓 서버 구현, Unity에서 TCP 클라이언트 연결
- 게임 통신 프로토콜 설계 원칙 (패킷 구조, 직렬화, 인증 보안)
- 효율적인 데이터 전송 및 패킷 처리 방법
- TCP 연결 관리 및 오류 처리 (Keep-Alive, 자동 재연결)
- 성능 최적화 기법 (Nagle 알고리즘 비활성화, 비동기 I/O, 패킷 압축)
- 구현 예제 및 테스트 (C++ 서버, Unity 클라이언트 실행 및 자동화 테스트)
이제 TCP 기반 네트워크 통신을 구현할 때 효율적인 패킷 설계, 연결 유지, 성능 최적화 기법을 적용하여 안정적인 게임 서버-클라이언트 통신 시스템을 구축할 수 있습니다.