Abstract
Network Manager 는 TCP 연결 하나를 채널이라는 단위로 감싸, "끊기지 않고 이어지는 바이트의 흐름"을 패킷 단위로 잘라 핸들러에 배달해 줍니다. 프레임워크가 정하지 않는 것은 단 하나 — 패킷이 어떻게 생겼는가입니다. 그 부분을 INetworkChannelHelper 라는 인터페이스로 비워 두었고, 그것을 채워야 채널을 만들 수 있습니다. 이 페이지의 본문은 구조를 설명하고, 이 Abstract 는 실제로 동작하는 가장 작은 헬퍼를 보여 줍니다.
TCP 에서 "패킷"은 직접 만들어야 한다
TCP 는 보낸 순서대로 도착한다는 것만 보장하고, 어디서 끊어 읽을지는 알려 주지 않습니다. "안녕"과 "하세요"를 따로 보내도 받는 쪽에는 "안녕하세요"가 한 덩어리로 오거나 "안"과 "녕하세요"로 쪼개져 올 수 있습니다. 그래서 모든 TCP 프로토콜은 메시지 앞에 "이 뒤로 몇 바이트가 한 메시지다"라는 머리말(헤더)을 붙입니다.
프레임워크는 이 절차를 이렇게 나눠 맡습니다.
단계 | 프레임워크 | 헬퍼 (내가 구현) |
① 헤더 받기 | PacketHeaderLength 바이트가 모일 때까지 기다린다 | 헤더가 몇 바이트인지 알려 준다 |
② 헤더 해석 | DeserializePacketHeader — 본문 길이를 읽어 돌려준다 | |
③ 본문 받기 | 그 길이만큼 모일 때까지 기다린다 | |
④ 본문 해석 | DeserializePacket — 바이트를 패킷 객체로 만든다 | |
⑤ 배달 | 패킷의 Id 에 맞는 핸들러를 메인 스레드에서 부른다 |
직접 해 보기 — 가장 작은 헬퍼
헤더는 8바이트(본문 길이 4 + 패킷 Id 4)로 정합니다.
① 헤더와 패킷
using System.IO;
using GameFramework;
using GameFramework.Network;
using UnityGameFramework.Runtime;
/// <summary>헤더 8바이트: [본문 길이 int32][패킷 Id int32]</summary>
public sealed class SimplePacketHeader : IPacketHeader, IReference
{
public int PacketLength { get; set; }
public int PacketId { get; set; }
public void Clear()
{
PacketLength = 0;
PacketId = 0;
}
}
public abstract class SimplePacket : Packet
{
public abstract void Write(BinaryWriter writer);
public abstract void Read(BinaryReader reader);
}
public sealed class ChatPacket : SimplePacket
{
public const int PacketId = 1;
public override int Id => PacketId;
public string Text { get; set; }
public override void Write(BinaryWriter writer) { writer.Write(Text ?? string.Empty); }
public override void Read(BinaryReader reader) { Text = reader.ReadString(); }
public override void Clear() { Text = null; }
}
C#
복사
② 핸들러 — 받은 패킷으로 무엇을 할지.
public sealed class ChatPacketHandler : IPacketHandler
{
public int Id => ChatPacket.PacketId;
public void Handle(object sender, Packet packet)
{
ChatPacket chat = (ChatPacket)packet;
Log.Info("받음: {0}", chat.Text);
}
}
C#
복사
③ 채널 헬퍼
public sealed class SimpleChannelHelper : INetworkChannelHelper
{
private const int HeaderLength = 8;
public int PacketHeaderLength => HeaderLength;
public void Initialize(INetworkChannel networkChannel)
{
networkChannel.RegisterHandler(new ChatPacketHandler());
}
public void Shutdown() { }
public void PrepareForConnecting() { }
// 이 예제는 하트비트를 보내지 않는다. false 를 돌려주면 누락 통지도 나가지 않는다
public bool SendHeartBeat() { return false; }
public bool Serialize<T>(T packet, Stream destination) where T : Packet
{
SimplePacket simple = packet as SimplePacket;
if (simple == null)
return false;
long start = destination.Position;
BinaryWriter writer = new BinaryWriter(destination);
writer.Write(0); // 본문 길이 자리. 본문을 쓴 뒤 채운다
writer.Write(packet.Id);
simple.Write(writer);
writer.Flush();
long end = destination.Position;
int bodyLength = (int)(end - start) - HeaderLength;
destination.Position = start;
writer.Write(bodyLength);
writer.Flush();
destination.Position = end;
return true;
}
public IPacketHeader DeserializePacketHeader(Stream source, out object customErrorData)
{
customErrorData = null;
BinaryReader reader = new BinaryReader(source);
SimplePacketHeader header = ReferencePool.Acquire<SimplePacketHeader>();
header.PacketLength = reader.ReadInt32();
header.PacketId = reader.ReadInt32();
return header;
}
public Packet DeserializePacket(IPacketHeader packetHeader, Stream source, out object customErrorData)
{
customErrorData = null;
SimplePacketHeader header = (SimplePacketHeader)packetHeader;
SimplePacket packet = CreatePacket(header.PacketId);
ReferencePool.Release(header);
if (packet == null)
return null;
packet.Read(new BinaryReader(source));
return packet;
}
private static SimplePacket CreatePacket(int packetId)
{
switch (packetId)
{
case ChatPacket.PacketId: return ReferencePool.Acquire<ChatPacket>();
default: return null;
}
}
}
C#
복사
④ 사용
NetworkComponent network = GameEntry.GetComponent<NetworkComponent>();
INetworkChannel channel = network.CreateNetworkChannel("Game", ServiceType.Tcp, new SimpleChannelHelper());
channel.Connect(System.Net.IPAddress.Parse("127.0.0.1"), 9000);
// NetworkConnected 이벤트를 받은 뒤
ChatPacket packet = ReferencePool.Acquire<ChatPacket>();
packet.Text = "hello";
channel.Send(packet); // 보낸 뒤 패킷은 프레임워크가 풀로 돌려놓는다
C#
복사
위 코드는 이 에디션에서 컴파일되는 것까지 확인했습니다. 실제 서버와 주고받는 시험은 하지 않았으므로, 서버 쪽 헤더 형식(바이트 순서 포함)과 맞추는 작업은 직접 해야 합니다. BinaryWriter 는 리틀 엔디언으로 씁니다.
실제 프로젝트로 키울 때
•
직렬화 라이브러리 붙이기. 패킷마다 Write / Read 를 손으로 쓰는 대신 Protobuf 나 MessagePack 을 씁니다. 바뀌는 곳은 Serialize 와 DeserializePacket 두 메서드뿐이고, 게임 코드의 channel.Send(packet) 는 그대로입니다
•
패킷 종류 등록 자동화. CreatePacket 의 switch 는 패킷이 수십 개가 되면 관리가 안 됩니다. 시작할 때 Id → 타입 딕셔너리를 만들어 ReferencePool.Acquire(type) 으로 꺼냅니다
•
하트비트. SendHeartBeat 에서 하트비트 패킷을 Send 하고 true 를 돌려줍니다. 서버 응답이 HeartBeatInterval(기본 30초) 안에 안 오면 NetworkMissHeartBeat 이벤트가 오고, 몇 번 연속이면 끊긴 것으로 보고 재접속합니다
•
재접속. 자동이 아닙니다. NetworkClosed / NetworkError 이벤트에서 직접 Connect 를 다시 부릅니다. 재접속 간격을 점점 늘리는(1초, 2초, 4초 …) 방식이 서버에 부담을 덜 줍니다
•
customErrorData. 헤더가 이상할 때(길이가 음수, 알 수 없는 Id) 이 out 인자에 무언가를 넣으면 NetworkCustomError 이벤트로 전달됩니다. 잘못된 서버나 버전 불일치를 잡아내는 용도입니다
본문을 읽을 때 주의할 점
•
기본 제공 헬퍼가 없습니다. 위와 같은 클래스를 직접 만들어야 CreateNetworkChannel 을 부를 수 있습니다.
•
서비스 타입은 TCP 두 가지뿐입니다. Tcp 는 수신을 비동기 콜백으로, TcpWithSyncReceive 는 매 프레임 Update 에서 동기로 읽습니다. 보통 Tcp 를 쓰고, 콜백 스레드를 쓸 수 없는 플랫폼에서 후자를 씁니다.
•
핸들러는 메인 스레드에서 불립니다. 받은 패킷은 내부 이벤트 풀에 쌓였다가 채널의 Update 에서 배달되므로, 핸들러 안에서 Unity API 를 써도 됩니다.
•
패킷 객체는 핸들러가 끝나면 풀로 돌아갑니다. 핸들러 밖에서 쓸 값은 복사해 두세요.
•
게임 코드는 본문의 NetworkManager 대신 GameEntry.GetComponent<NetworkComponent>() 를 쓰고, 이벤트는 EventComponent 로 NetworkConnectedEventArgs 등을 구독합니다.
Network Manager (네트워크 매니저)
관련 소스 파일
•
GameFramework/Network/INetworkChannel.cs
•
GameFramework/Network/INetworkChannelHelper.cs
•
GameFramework/Network/INetworkManager.cs
•
GameFramework/Network/IPacketHeader.cs
•
GameFramework/Network/NetworkManager.cs
목적 및 개요
Network Manager는 Game Framework의 핵심 컴포넌트로, 네트워크 통신을 처리합니다.
네트워크 채널 생성 및 관리, 연결 수립, 패킷 송수신, 네트워크 이벤트 처리를 담당합니다.
•
채널 기반 아키텍처 제공
•
TCP 연결 지원
•
하트비트(heartbeat) 메커니즘을 통한 연결 유지
※ 인터넷 파일 다운로드 관련 내용은 Download Manager 문서를 참고하세요.
시스템 아키텍처
Network Manager는 GameFramework의 모듈 아키텍처 패턴을 따릅니다.
•
GameFrameworkModule을 상속
•
INetworkManager 인터페이스 구현
여러 네트워크 채널을 관리하며, 각 채널은 원격 호스트와의 연결을 나타냅니다.
Network Manager 구성 요소
•
NetworkManager: 네트워크 채널 관리 및 네트워크 이벤트 제공
•
NetworkChannel: 원격 호스트와의 연결, 패킷 송수신 처리
•
NetworkChannelHelper: 패킷 직렬화/역직렬화, 기타 보조 작업 처리
•
Packet: 모든 네트워크 메시지 패킷의 기본 클래스
•
PacketHeader: 패킷 길이 등 패킷 정보를 담는 헤더
네트워크 채널 생성 및 관리
채널 생성
CreateNetworkChannel 메서드를 사용하여 네트워크 채널을 생성합니다.
필요한 인자:
•
이름
•
서비스 타입
•
NetworkChannelHelper
지원 서비스 타입:
•
ServiceType.Tcp: 일반 TCP 연결
•
ServiceType.TcpWithSyncReceive: 동기식 수신을 지원하는 TCP 연결
채널 관리
NetworkManager는 채널을 관리하기 위한 다양한 메서드를 제공합니다:
•
HasNetworkChannel: 채널 존재 여부 확인
•
GetNetworkChannel: 특정 채널 반환
•
GetAllNetworkChannels: 모든 채널 반환
•
DestroyNetworkChannel: 채널 파괴
채널을 파괴하면 이벤트 구독 해제, 종료, 딕셔너리에서 제거가 자동 처리됩니다.
네트워크 채널 동작
원격 호스트 연결
•
Connect(IP, Port) 메서드 사용
패킷 전송
•
Send(Packet packet) 메서드 사용
패킷 수신
수신은 채널에서 자동 처리됩니다. 흐름:
1.
소켓에서 데이터 수신
2.
NetworkChannelHelper를 통해 패킷 헤더 역직렬화
3.
헤더 정보를 기반으로 패킷 본문 역직렬화
4.
등록된 핸들러 또는 기본 핸들러에서 패킷 처리
하트비트(Heartbeat) 메커니즘
연결 유지 및 모니터링을 위한 하트비트 기능을 지원합니다.
주요 속성 및 동작:
•
HeartBeatInterval: 하트비트 패킷 간격
•
MissHeartBeatCount: 누락된 하트비트 개수
•
ResetHeartBeatElapseSecondsWhenReceivePacket: 패킷 수신 시 하트비트 타이머 리셋 여부
네트워크 이벤트 처리
Network Manager는 다양한 이벤트를 제공합니다:
이벤트 | 설명 | 인자 |
NetworkConnected | 연결이 수립될 때 | NetworkChannel, UserData |
NetworkClosed | 연결이 종료될 때 | NetworkChannel |
NetworkMissHeartBeat | 하트비트가 누락될 때 | NetworkChannel, MissHeartBeatCount |
NetworkError | 네트워크 오류 발생 | NetworkChannel, ErrorCode, SocketErrorCode, ErrorMessage |
NetworkCustomError | 커스텀 오류 발생 | NetworkChannel, CustomErrorData |
Network Channel Helper
INetworkChannelHelper 인터페이스는 채널을 돕는 역할을 합니다:
•
Packet Serialization: 패킷 → 바이너리 변환
•
Packet Deserialization: 바이너리 → 패킷 변환
•
Heartbeat Management: 하트비트 패킷 송신
•
Connection Preparation: 신규 연결 준비
개발자는 자신만의 패킷 직렬화/역직렬화 로직을 구현해야 합니다.
패킷 시스템
네트워크 통신의 핵심 구성 요소:
•
Packet: 모든 패킷의 기본 클래스
•
PacketHeader: 패킷 길이 정보를 제공하는 인터페이스
→ 개발자는 Packet을 상속하여 패킷 타입 정의, PacketHeader 구현을 통해 PacketLength 속성 제공 필요
Game Framework과의 통합
•
Update(): 매 프레임 호출 → 모든 네트워크 채널 업데이트
•
Shutdown(): 프레임워크 종료 시 호출 → 모든 네트워크 채널 종료
게임 실행 사이클과 네트워크 동작을 동기화합니다.
사용 예시
네트워크 매니저 사용 흐름 예시:
1.
커스텀 NetworkChannelHelper 구현
2.
NetworkManager를 통해 네트워크 채널 생성
3.
패킷 핸들러 등록
4.
원격 호스트에 연결
5.
패킷 송수신
6.
네트워크 이벤트 처리
7.
필요 시 연결 종료
→ 유연하고 안정적인 네트워크 통신 시스템을 구현할 수 있습니다.










