// 메모리안에 있는 데이터를 압축해서 보낸다가 직렬화의 개념
using ServerCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace DummyClient
{
#region SendBuffer
//class Knight
//{
// public int hp;
// public int attack;
// // 고정 아닌 예
// public string name;
// public List<int> skills = new List<int>();
// // 가변적이여서 예측이 어려움
//}
#endregion
#region PacketSession
class Packet
{
// int 보다 4바이트 아낌
// 만약 10000명이면 40000바이트를 아낌
public ushort size;
public ushort packetId;
}
#endregion
#region Serialization
// 메모리안에 있는 데이터를 압축해서 보낸다가 직렬화의 개념
// 네트워크 뿐이 아닌 파일저장 등에서도 사용됨
// 정리를 하고 넘어간다.
// 패킷마다 고유 정보 아이디가 있을것
// 패킷을 구분할 수 있어야함
// 클라이언트 세션쪽에서도 알고 있어야 파싱이됨
class PlayerInfoReq : Packet
{
public long playerId;
}
class PlayerInfoOk :Packet
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
#endregion
// 서버의 대리자라는 의미
//
class ServerSession : Session
{
// ex TryWriteBytes가 유니티에서 사용할 수 없을 경우 사용할 수 있는 unsafe code
// C++의 포인터를 사용할 수 있다.
//static unsafe void ToBytes(byte[] array, int offset, ulong value)
//{
// fixed (byte* ptr = &array[offset])
// *(ulong*)ptr = value;
//}
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected: {endPoint}");
#region SendBuffer
//ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
// 버퍼사이즈를 어떻게 할까
// 버퍼사이즈 고정사이즈가 아닌 경우가 생김
// 무작정 크게 잡아 놓는것은 낭비임
// 개선하기 위해서 큰 덩어리를 만들고 차츰 차츰 사용해갈 수 있게 만듬
// [][][][][][][][][][][][][] 큰 덩어리를 잘라서 사용
//byte[] sendBuff = new byte[1024];
//byte[] buffer = BitConverter.GetBytes(knight.hp);
//byte[] buffer2 = BitConverter.GetBytes(knight.attack);
//[buffer][][][][buffer2 ]
//Array.Copy(buffer, 0, sendBuff, 0, buffer.Length);
//Array.Copy(buffer2, 0, sendBuff, 4, buffer2.Length);
//Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
//Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
//닫아줘
// 8바이트 컨펌
//ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
// if 100명
// 1 -> 100명
// 100 -> 이동 패킷 100 * 100 = 1만
//Packet packet = new Packet() { size = 4, packetId = 7 };
// 서버에 5번 메시지를 전송
//Send(sendBuff);
//Thread.Sleep(1000);
//Disconnect();
#endregion
PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
#region Serialization
// 보낸다
//for (int i = 0; i < 5; i++)
// 자동화할 예정이다
{
//byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
//Send(sendBuff);
ushort count = 0;
bool success = true;
ArraySegment<byte> s = SendBufferHelper.Open(4096);
// 실패 성공 여부가 불분명함
// 더 효율적
// 유니티에서 되는지는 확인해봐야한다.
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.playerId);
count += 8;
// 찜찜함
// 버퍼에 넣어주면 좋을 것 같다는 생각이 든다.
// 버전이 다양하게 존재한다.
//byte[] size = BitConverter.GetBytes(packet.size); //2
//byte[] packetId = BitConverter.GetBytes(packet.packetId);//2
//byte[] playerId = BitConverter.GetBytes(packet.playerId); //8
//// 몇바이트 넣었는지 추적
//Array.Copy(size, 0, s.Array, s.Offset+ 0,2/*size.Length*/);
//count += 2;
//Array.Copy(packetId, 0, s.Array, s.Offset + count,2/* packetId.Length*/);
//count += 2;
//Array.Copy(playerId, 0, s.Array, s.Offset + count,8 /*playerId.Length*/);
//count += 8;
// 사이즈는 마지막에 알 수 있다.
// 모든 작업이 끝나고 채워줄 수 있다,
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);
Send(sendBuff);
}
#endregion
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
//Knight knight = new Knight() { hp = 100, attack = 10 };
// SendBufferHelper를 사용하여 버퍼 할당
//ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
//byte[] buffer = BitConverter.GetBytes(knight.hp);
//byte[] buffer2 = BitConverter.GetBytes(knight.attack);
//// 데이터 복사 (각 필드의 위치를 고려하여 복사)
//Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
//Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
//// 사용한 크기만큼 Close() 호출하여 실제 전송할 버퍼 생성
//ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
//Send(sendBuff);
//Thread.Sleep(1000);
//Disconnect();
}
public override int OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"From Server: {recvData}");
return buffer.Count;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
}
using ServerCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Server
{
#region PacketSession
class Packet
{
// int 보다 4바이트 아낌
// 만약 10000명이면 40000바이트를 아낌
public ushort size;
public ushort packetId;
}
//class LoginOkPacket : Packet
//{
//}
#endregion
#region Serialization
// 정보를 맞춰줘야 한다.
// 공통적인 부분에 넣어주는게 좋을 것 같다는 생각을 하게된다.
class PlayerInfoReq : Packet
{
public long playerId;
}
class PlayerInfoOk : Packet
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
#endregion
class ClientSession :PacketSession
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected: {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
Send(sendBuff);
Thread.Sleep(1000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected: {endPoint}");
}
// Sealed로 묶어서 사용하면 안됨
//public override int OnRecv(ArraySegment<byte> buffer)
//{
// string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
// Console.WriteLine($"[From Client] {recvData}");
// return buffer.Count;
//}
#region PacketSession
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort count = 0;
// size와 id
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
// 버퍼 크기를 지속적으로 체크햐야함
switch((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
count += 8;
Console.WriteLine($"PlayerInfoReq: {playerId}");
}
break;
}
Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}
#endregion
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
}
// 일반적으로 데이터는 XML, JSON을 통해서 관리
직렬화(Serialization) 는 데이터를 메모리, 파일, 네트워크 전송 등에 적합한 형태(바이트 배열) 로 변환하는 과정입니다.
📌 활용 사례
✔ 네트워크 패킷 데이터 전송 (클라이언트 ↔ 서버)
✔ 게임 상태 저장 (세이브 데이터)
✔ JSON, XML, Protocol Buffers 등으로 데이터 변환
📌 예제
✔ 네트워크 전송 시
객체(PlayerInfoReq) → 직렬화(바이트 배열) → 네트워크 전송 → 역직렬화(객체 복원)
✔ JSON을 파일로 저장할 때
객체(PlayerInfoReq) → JSON 변환 → 파일 저장 → JSON 읽기 → 객체 복원
네트워크에서 데이터를 주고받을 때 작은 단위로 나눈 데이터 블록입니다.
네트워크 패킷의 기본 구조:
[패킷 크기] [패킷 ID] [데이터]
예제:
[2바이트] [2바이트] [실제 데이터]
class Packet
{
public ushort size; // 패킷 크기 (2바이트)
public ushort packetId; // 패킷 ID (2바이트)
}
size: 패킷의 전체 크기를 저장 (데이터 손실 방지) packetId: 패킷 종류를 식별 📌 패킷 ID 종류
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
PlayerInfoReq: 플레이어 정보를 요청하는 패킷 PlayerInfoOk: 서버가 응답하는 패킷 byte[] buffer = BitConverter.GetBytes(packet.size);
byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
📌 문제점
❌ BitConverter.GetBytes()는 매번 새로운 바이트 배열을 생성 (new byte[4])
❌ Array.Copy()를 여러 번 호출하면 불필요한 연산이 많아 성능 저하
T> 활용success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), packet.size);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.packetId);
count += 2;
📌 개선된 점
✅ Span<T> 를 사용하여 기존 버퍼에 직접 데이터 삽입 → 메모리 할당 최소화
✅ TryWriteBytes()를 사용하여 성공 여부를 체크하여 예외 방지
✅ count 변수를 활용하여 자동으로 오프셋 증가
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
switch((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
count += 8;
Console.WriteLine($"PlayerInfoReq: {playerId}");
}
break;
}
}
📌 포인트
✅ count 변수를 사용하여 자동으로 오프셋 증가
✅ switch-case 문을 활용하여 패킷을 식별 후 처리
ushort nameLen = (ushort)Encoding.Unicode.GetByteCount(this.name);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, segment.Array, segment.Offset + count);
count += nameLen;
📌 핵심 포인트
✅ 문자열 크기 (nameLen)를 먼저 저장
✅ Encoding.Unicode.GetBytes()를 활용하여 버퍼에 직접 복사 (메모리 최적화)
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
📌 핵심 포인트
✅ 문자열 크기(nameLen)를 먼저 읽은 후, 해당 크기만큼 GetString()을 사용하여 복원
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach (SkillInfo skill in skills)
success &= skill.Write(s, ref count);
📌 핵심 포인트
✅ List<T> 데이터는 크기(Count)를 먼저 저장 후 → 개별 데이터 직렬화
skills.Clear();
ushort skillLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
for (int i = 0; i < skillLen; i++)
{
SkillInfo skill = new SkillInfo();
skill.Read(s, ref count);
skills.Add(skill);
}
📌 핵심 포인트
✅ skills.Clear()를 호출하여 기존 데이터 제거
✅ for 문을 활용하여 개별 데이터를 역직렬화 후 List에 추가
📌 최적화 개선
✅ BitConverter.TryWriteBytes() 사용 → 동적 할당 최소화
✅ Span<byte> 및 Slice() 사용 → 메모리 복사 최소화
✅ ReadOnlySpan<byte> 사용 → 버퍼 초과 읽기 방지 (보안 강화)
📌 보안 강화
✅ 잘못된 nameLen 입력을 방지
✅ if (!success) return null; → 데이터 오류 시 전송 차단
✔ 패킷 직렬화는 네트워크 최적화에 필수적인 기술
✔ 메모리 할당을 줄이고 성능을 최적화하는 것이 핵심
✔ 다음 단계: Protocol Buffers & Flat Buffers 분석 및 적용 🚀
이제 직렬화의 모든 개념을 체계적으로 정리했으니, 다음 강의로 넘어가도 됩니다! 🎯