VisualStudio/C#

[C#] Google Protobuf형식 기반 ProtoBuff-net 사용 방법

usingsystem 2023. 11. 2. 15:12
728x90

Protobuf 참고 사이트 및 공식 

https://github.com/protobuf-net/protobuf-net

 

GitHub - protobuf-net/protobuf-net: Protocol Buffers library for idiomatic .NET

Protocol Buffers library for idiomatic .NET . Contribute to protobuf-net/protobuf-net development by creating an account on GitHub.

github.com

https://dotnetcoretutorials.com/protobuf-in-c-net-part-1-getting-started/#google_vignette

 

Protobuf In C# .NET – Part 1 – Getting Started – .NET Core Tutorials

This is a 4 part series on working with Protobuf in C# .NET. While you can start anywhere in the series, it’s always best to start at the beginning! Part 1 – Getting Started Part 2 – Serializing/Deserializing Part 3 – Using Length Prefixes Part 4

dotnetcoretutorials.com

https://learn.microsoft.com/ko-kr/aspnet/core/grpc/protobuf?view=aspnetcore-7.0

 

.NET 앱에 대한 Protobuf 메시지 만들기

.NET 앱에 대한 Protobuf 메시지를 만드는 방법을 알아봅니다.

learn.microsoft.com

 

 

ProtoBuff-net 이란?

protobuf-net는 Protocol Buffers 데이터 직렬화 라이브러리로서, C# 환경에서 Protocol Buffers 형식의 데이터를 직렬화하고 역직렬화하기 위한 라이브러리입니다. 이 라이브러리는 Google의 Protocol Buffers 형식을 기반으로 하며, C#에서 Protocol Buffers 메시지를 정의하고 사용하는 데 도움을 줍니다.

protobuf-net의 주요 특징과 개념은 다음과 같습니다:

  1. Protocol Buffers 지원: protobuf-net은 Google Protocol Buffers 형식을 지원하며, .proto 파일을 사용하여 데이터 메시지를 정의하고 이를 C# 클래스로 변환할 수 있습니다.
  2. C# 클래스 매핑: .proto 파일에서 생성된 C# 클래스를 사용하여 데이터를 표현합니다. 이러한 클래스는 ProtoContract 및 ProtoMember 특성을 사용하여 Protocol Buffers 메시지 필드와 C# 클래스의 필드 또는 속성을 매핑합니다.
  3. 직렬화 및 역직렬화: protobuf-net은 C# 객체를 Protocol Buffers 형식으로 직렬화하고, 직렬화된 데이터를 C# 객체로 역직렬화하는 기능을 제공합니다.
  4. 효율성: protobuf-net는 효율적인 직렬화 및 역직렬화를 위해 최적화되었습니다. 이는 데이터 크기와 직렬화/역직렬화 속도 측면에서 이점을 제공합니다.
  5. 데이터 규모 변화 대응: Protocol Buffers 형식을 사용하면 데이터 스키마를 버전 간에 변경하는 데 유연성을 제공합니다. protobuf-net은 다양한 데이터 규모 변화에 대응할 수 있도록 설계되었습니다.
  6. NuGet 패키지: protobuf-net은 NuGet 패키지로 제공되어 프로젝트에 쉽게 추가하고 사용할 수 있습니다.

protobuf-net를 사용하면 C#에서 Protocol Buffers 데이터를 직렬화 및 역직렬화하는 데 효과적으로 활용할 수 있으며, 특히 네트워크 통신 또는 데이터 저장 및 교환 작업에 유용합니다. Google의 Protocol Buffers 형식을 사용하면 데이터를 효율적으로 관리하고 서로 다른 플랫폼 간에 데이터를 교환할 수 있는 강력한 도구가 됩니다.

 

ProtoBuff-net 설치

 

패킷구조 설명

  • Header [4Byte]
    • Size[2byte]
    • PacketID[2byte]
  • Body [동적Byte]

패킷 정의

[ ProtoContract ]

ProtoContract를 사용하여 C# 클래스를 Protocol Buffers 메시지로 정의합니다. 각 클래스 필드나 속성을 Protocol Buffers 메시지의 필드로 매핑합니다.

[ProtoMember(1)]

ProtoMember 특성을 사용하여 클래스 내의 필드 또는 속성을 Protocol Buffers 메시지 필드와 연결합니다. ProtoMember에는 각 필드에 대한 번호를 할당합니다.

    enum PacketID
    {
        CChat = 3,
        SChat = 4,
    }
    public interface IMessage
    {
    }
    [ProtoContract]
    public class S_Chat : IMessage
    {
        [ProtoMember(1)]
        public int objectId { get; set; }
        [ProtoMember(2)]
        public string chat { get; set; }
    }
    [ProtoContract]
    public class C_Chat : IMessage
    {
        [ProtoMember(1)]
        public string chat { get; set; }
    }

Send

  •   ProtoBuf.Serializer.Serialize(memoryStream, packet)
    • 정의해놓은 Packet을 직렬화한다.
  •  string msgName = packet.GetType().Name.Replace("_", string.Empty);
    • 정의한 Packet Class Name을 리플렉션을 통해가져 와 Enum Type의 Id값으로 변경하기 위해
      • ClassName : S_Chat, Enum PacketID : SChat 이기 때문에 _제거를 통해 동일한 이름으로 변경
  •  PacketID msgId = (PacketID)Enum.Parse(typeof(PacketID), msgName);
    • PacketID 값이 Header로 들어가는 부분 2byte
  • ushort size = (ushort)packetArray.Length;
    • Packet 직렬화한 사이즈
  • byte[] sendBuffer = new byte[size + 4];
    Array.Copy(BitConverter.GetBytes((ushort)(size + 4)), 0, sendBuffer, 0, sizeof(ushort));
    Array.Copy(BitConverter.GetBytes((ushort)msgId), 0, sendBuffer, 2, sizeof(ushort));
    Array.Copy(packetArray, 0, sendBuffer, 4, size);
    • 앞에서 정의한 size, id 및 직렬화한 packet을 하나의 byte로 합쳐준다.
        public override void OnConnecte(EndPoint endPoint)
        {
            Console.WriteLine("OnConnecte  : " + endPoint.ToString());

            S_Chat chat = new S_Chat() { objectId = 1, chat = "서버에 접속함." };
            Send(chat);
        }

        void Send(IMessage packet)
        {
            MemoryStream memoryStream = new MemoryStream();
            ProtoBuf.Serializer.Serialize(memoryStream, packet);
            byte[] packetArray = memoryStream.ToArray();

            string msgName = packet.GetType().Name.Replace("_", string.Empty);
            PacketID msgId = (PacketID)Enum.Parse(typeof(PacketID), msgName);
            ushort size = (ushort)packetArray.Length;
           
            byte[] sendBuffer = new byte[size + 4];
            Array.Copy(BitConverter.GetBytes((ushort)(size + 4)), 0, sendBuffer, 0, sizeof(ushort));
            Array.Copy(BitConverter.GetBytes((ushort)msgId), 0, sendBuffer, 2, sizeof(ushort));
            Array.Copy(packetArray, 0, sendBuffer, 4, size);

            Send(sendBuffer);
        }

Recive

먼저 데이터 size와 id값을 찾아와 id값에 따라 switch문에서 적절하게 패킷처리를 해준다.

  •  MemoryStream memoryStream = new MemoryStream(new ArraySegment<byte>(buffer.Array, buffer.Offset + 4, buffer.Count - 4).ToArray());
    • 클라이언트의 역직렬화를 위한 작업으로 정의한 프로토콜은 헤더가 4이기 때문에 offset을 4 만큼 더해주고 count를 4만큼 빼준다.
  • C_Chat chatPacket = Serializer.Deserialize<C_Chat>(memoryStream);
    •  C_Chat 클래스에 바로 역직렬화를 수행한다.
        public override void OnRecvedPacket(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;

            MemoryStream memoryStream = new MemoryStream(new ArraySegment<byte>(buffer.Array, buffer.Offset + 4, buffer.Count - 4).ToArray());

            PacketID packetId = (PacketID)id;
            switch (packetId)
            {
                case PacketID.CChat:
                    {
                        C_Chat chatPacket = Serializer.Deserialize<C_Chat>(memoryStream);
                        Console.WriteLine(chatPacket.chat);
                    }
                    break;
            }
        }

 

 

전체 소스코드

using ProtoBuf;
using ProtoBuf.Serializers;
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Linq.Expressions;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using static ProtoBuff_net.PacketSession;

namespace ProtoBuff_net
{
    enum PacketID
    {
        CChat = 3,
        SChat = 4,
    }
    public interface IMessage
    {
    }
    [ProtoContract]
    public class S_Chat : IMessage
    {
        [ProtoMember(1)]
        public int objectId { get; set; }
        [ProtoMember(2)]
        public string chat { get; set; }
    }
    [ProtoContract]
    public class C_Chat : IMessage
    {
        [ProtoMember(1)]
        public string chat { get; set; }
    }

    public class ClientSession : PacketSession
    {
        public override void OnConnecte(EndPoint endPoint)
        {
            Console.WriteLine("OnConnecte  : " + endPoint.ToString());

            S_Chat chat = new S_Chat() { objectId = 1, chat = "서버에 접속함." };
            Send(chat);
        }

        void Send(IMessage packet)
        {
            MemoryStream memoryStream = new MemoryStream();
            ProtoBuf.Serializer.Serialize(memoryStream, packet);
            byte[] packetArray = memoryStream.ToArray();

            string msgName = packet.GetType().Name.Replace("_", string.Empty);
            PacketID msgId = (PacketID)Enum.Parse(typeof(PacketID), msgName);
            ushort size = (ushort)packetArray.Length;
           
            byte[] sendBuffer = new byte[size + 4];
            Array.Copy(BitConverter.GetBytes((ushort)(size + 4)), 0, sendBuffer, 0, sizeof(ushort));
            Array.Copy(BitConverter.GetBytes((ushort)msgId), 0, sendBuffer, 2, sizeof(ushort));
            Array.Copy(packetArray, 0, sendBuffer, 4, size);

            Send(sendBuffer);
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine("OnDisconnected  : " + endPoint.ToString());
        }
        public override void OnRecvedPacket(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;

            MemoryStream memoryStream = new MemoryStream(new ArraySegment<byte>(buffer.Array, buffer.Offset + 4, buffer.Count - 4).ToArray());

            PacketID packetId = (PacketID)id;
            switch (packetId)
            {
                case PacketID.CChat:
                    {
                        C_Chat chatPacket = Serializer.Deserialize<C_Chat>(memoryStream);
                        Console.WriteLine(chatPacket.chat);
                    }
                    break;
            }
        }
    }
    public class Program
    {
        static Listener _listener = new Listener();
        static void Main(string[] args)
        {
            string name = Dns.GetHostName();
            IPHostEntry ipEntry = Dns.GetHostEntry(name);
            IPAddress address = ipEntry.AddressList[1];// 1번 인덱스 ipv4
            IPEndPoint endPoint = new IPEndPoint(address, 9999);
            _listener.Init(endPoint, () => { return new ClientSession(); });
            Console.WriteLine("Server On");

            while (true)
            {
                ;
            }
        }
    }

    public class Listener
    {
        Socket _listener;
        event Func<Session> _sessionFacktory;
        public void Init(IPEndPoint endPoint, Func<Session> sessionFacktory)
        {
            _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _sessionFacktory = sessionFacktory;

            _listener.Bind(endPoint);
            _listener.Listen(10);

            SocketAsyncEventArgs accepArgs = new SocketAsyncEventArgs();
            accepArgs.Completed += AcceptCompleted;
            RegisterAccept(accepArgs);
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null;
            try
            {
                bool pending = _listener.AcceptAsync(args);

                if (pending == false)
                {
                    AcceptCompleted(null, args);
                }
            }
            catch (Exception err)
            {
                Console.WriteLine(err);
            }
        }
        void AcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Session session = _sessionFacktory.Invoke();
                session.Start(args.AcceptSocket);
                session.OnConnecte(args.AcceptSocket.RemoteEndPoint);
            }
            else
            {
                Console.WriteLine(args.SocketError);
            }
            RegisterAccept(args);
        }
    }
    public abstract class PacketSession : Session
    {
        public static readonly int HeadSize = 2;

        public sealed override int OnRecived(ArraySegment<byte> buffer)
        {
            int processLen = 0;
            while (true)
            {
                if (buffer.Count < HeadSize)
                    break;

                ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
                if (buffer.Count < dataSize)
                    break;

                OnRecvedPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
                processLen += dataSize;
                buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
            }
            return processLen;
        }
        public abstract void OnRecvedPacket(ArraySegment<byte> buffer);
    }
    public abstract class Session
    {
        Socket _socket;
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        int _counnected = 0;
        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        List<ArraySegment<byte>> _pandinglist = new List<ArraySegment<byte>>();
        public abstract void OnConnecte(EndPoint endPoint);
        public abstract int OnRecived(ArraySegment<byte> buff);
        public abstract void OnDisconnected(EndPoint endPoint);

        public void Start(Socket socket)
        {
            _socket = socket;

            _recvArgs.Completed += RecvArgs_Completed;
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);
            _sendArgs.Completed += SendArgs_Completed;

            RegisterRecv();
        }
        public void Send(byte[] sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pandinglist.Count == 0)
                    RegisterSend();
            }
        }
        void RegisterRecv()
        {
            if (_counnected == 1)
                return;
            try
            {
                bool pending = _socket.ReceiveAsync(_recvArgs);
                if (pending == false)
                    RecvArgs_Completed(null, _recvArgs);
            }
            catch (Exception err)
            {
                Console.WriteLine(err);
            }
        }
        void RegisterSend()
        {
            if (_counnected == 1)
                return;

            while (_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                _pandinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
            }
            _sendArgs.BufferList = _pandinglist;

            bool panding = _socket.SendAsync(_sendArgs);
            if (panding == false)
                SendArgs_Completed(null, _sendArgs);
        }
        private void RecvArgs_Completed(object sender, SocketAsyncEventArgs args)
        {
            if (_recvArgs.SocketError == SocketError.Success && _recvArgs.BytesTransferred > 0)
            {
                try
                {
                    OnRecived(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
                    RegisterRecv();
                }
                catch (Exception err)
                {
                    Console.WriteLine(err);
                    DisConnect();
                }
            }
            else
            {
                DisConnect();
            }
        }
        private void SendArgs_Completed(object sender, SocketAsyncEventArgs e)
        {
            lock (_lock)
            {
                if (_sendArgs.SocketError == SocketError.Success && _sendArgs.BytesTransferred > 0)
                {
                    try
                    {
                        _sendArgs.BufferList = null;
                        _pandinglist.Clear();
                        if (_sendQueue.Count > 0)
                            RegisterSend();
                    }
                    catch (Exception err)
                    {
                        Console.WriteLine(err);
                        DisConnect();
                    }
                }
                else
                {
                    DisConnect();
                }
            }
        }
        public void DisConnect()
        {
            if (Interlocked.Exchange(ref _counnected, 1) == 1)
                return;

            OnDisconnected(_socket.RemoteEndPoint);
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
            _sendQueue.Clear();
        }
    }
}
728x90