전체 코드


// 메모리안에 있는 데이터를 압축해서 보낸다가 직렬화의 개념
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을 통해서 관리

1. 직렬화(Serialization)란?

1.1 직렬화 개념

직렬화(Serialization) 는 데이터를 메모리, 파일, 네트워크 전송 등에 적합한 형태(바이트 배열) 로 변환하는 과정입니다.

  • 데이터를 전송할 때 고정된 형식으로 변환하여 일관성을 유지할 수 있음.
  • 역직렬화(Deserialization) 는 바이트 데이터를 다시 원래 객체로 복원하는 과정.

📌 활용 사례
✔ 네트워크 패킷 데이터 전송 (클라이언트 ↔ 서버)
✔ 게임 상태 저장 (세이브 데이터)
✔ JSON, XML, Protocol Buffers 등으로 데이터 변환

📌 예제
✔ 네트워크 전송 시

객체(PlayerInfoReq) → 직렬화(바이트 배열) → 네트워크 전송 → 역직렬화(객체 복원)

✔ JSON을 파일로 저장할 때

객체(PlayerInfoReq) → JSON 변환 → 파일 저장 → JSON 읽기 → 객체 복원

2. 네트워크 패킷 기본 구조

2.1 패킷(Packet)이란?

네트워크에서 데이터를 주고받을 때 작은 단위로 나눈 데이터 블록입니다.

  • 네트워크 패킷의 기본 구조:

    [패킷 크기] [패킷 ID] [데이터]
  • 예제:

    [2바이트] [2바이트] [실제 데이터]

3. 기본 패킷 클래스 구조

3.1 패킷 클래스 정의

class Packet
{
    public ushort size;      // 패킷 크기 (2바이트)
    public ushort packetId;  // 패킷 ID (2바이트)
}
  • size: 패킷의 전체 크기를 저장 (데이터 손실 방지)
  • packetId: 패킷 종류를 식별

📌 패킷 ID 종류

public enum PacketID
{
    PlayerInfoReq = 1,
    PlayerInfoOk = 2,
}
  • PlayerInfoReq: 플레이어 정보를 요청하는 패킷
  • PlayerInfoOk: 서버가 응답하는 패킷

4. 패킷 직렬화 (Serialization)

4.1 직렬화 - 기존 방식 (비효율적)

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()를 여러 번 호출하면 불필요한 연산이 많아 성능 저하


4.2 개선된 직렬화 - Span<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 변수를 활용하여 자동으로 오프셋 증가


5. 패킷 역직렬화 (Deserialization)

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 문을 활용하여 패킷을 식별 후 처리


6. 문자열(String) 직렬화

6.1 문자열 직렬화 (Write)

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()를 활용하여 버퍼에 직접 복사 (메모리 최적화)


6.2 문자열 역직렬화 (Read)

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()을 사용하여 복원


7. 리스트(List) 직렬화

7.1 리스트 직렬화 (Write)

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)를 먼저 저장 후 → 개별 데이터 직렬화


7.2 리스트 역직렬화 (Read)

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에 추가


8. 최적화 및 보안 강화

📌 최적화 개선
BitConverter.TryWriteBytes() 사용 → 동적 할당 최소화
Span<byte>Slice() 사용 → 메모리 복사 최소화
ReadOnlySpan<byte> 사용 → 버퍼 초과 읽기 방지 (보안 강화)

📌 보안 강화
잘못된 nameLen 입력을 방지
if (!success) return null;데이터 오류 시 전송 차단


9. 결론 및 다음 목표

패킷 직렬화는 네트워크 최적화에 필수적인 기술
메모리 할당을 줄이고 성능을 최적화하는 것이 핵심
다음 단계: Protocol Buffers & Flat Buffers 분석 및 적용 🚀

이제 직렬화의 모든 개념을 체계적으로 정리했으니, 다음 강의로 넘어가도 됩니다! 🎯

profile
李家네_공부방

0개의 댓글