이번 포스트에서는 네트워크 통신에서의 직렬화에 대해 알아보겠습니다.
직렬화는네트워크 뿐만이 아닌 다른 상황에서도 보편적으로 사용하는 단어입니다.
그리고 네트워크 통신에서의 직렬화는 다음과 같이 사용할 수 있습니다.
1) 직렬화 : Send 버퍼안에 전송할 데이터를 압축하여 집어넣는 작업을 의미
대표적으로 OnConnected 에서 수행하는 로직이 해당합니다.
2) 역직렬화 : Recv 버퍼에 담겨진 압축된 데이터를 다시 원상태로 늘리는 작업을 의미
대표적으로 서버의 OnRecvPacket과 클라의 OnRecv에서 수행하는 로직이 해당합니다.
이 포스트에 소개해드릴 직렬화와 관련된 작업은 자동화 이전에
자동화 작업이 어떻게 이뤄질 지 간략하게 알아보도록 하겠습니다.
1) 클라이언트
1 - 1) Client
{
// 기존에 클라이언트에서 생성하던 패킷을 ServerSession 클래스에서 수행합니다.
// 따라서 메인 로직에서는 GameSession이 아닌 ServerSession을 생성합니다.
class Program
{
static void Main(string[] args)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new ServerSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
1 - 2) ServerSession
// 패킷의 인자를 설정하는 경우, 최대한 용량을 줄여주는 것이 좋음
class Packet
{
// 패킷의 용량
public ushort _packetSize;
// 패킷의 아이디
public ushort _packetID;
}
// PlayerInfoRequire의 멤버는 _packetSize, _packetID, _playerId = 12 바이트
class PlayerInfoRequire : Packet
{
public long _playerID;
}
// PlayerInfo의 멤버는 _packetSize, _packetID, _hp, _attack = 12 바이트
class PlayerInfo : Packet
{
public int _hp;
public int _attack;
}
public enum PacketID
{
PlayerInfoRequire = 1,
PlayerInfo = 2,
}
class ServerSession : Session
{
// 만약, 아래의 OnConnected 방식이 지원되지 않는 유니티 버전에서는 다음의 로직을 사용가능함
// TryWriteBytes를 사용하지 못하는 경우, unsafe를 사용하여 Raw 포인터를 사용할 수 있음
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($"[Connect from] : {endPoint}");
PlayerInfoRequire packet = new PlayerInfoRequire()
{
_packetSize = 4,
_packetID = (ushort)PacketID.PlayerInfoRequire,
_playerID = 1001,
};
// 서버로 전송
for (int i = 0; i < 5; i++)
{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
bool success = true;
ushort offset = 0;
// 아래의 읽어들인 패킷의 크기만큼의 배열을 생성하고,
// 이를 다시 SendBuffer에 기록하던 방식을
// 다음의 로직으로 축약할 수 있음
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), packet._packetSize);
offset += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + offset, openSegment.Count - offset), packet._packetID);
offset += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + offset, openSegment.Count - offset), packet._playerID);
offset += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), (ushort)offset);
/*byte[] size = BitConverter.GetBytes(packet._packetSize);
byte[] packetId = BitConverter.GetBytes(packet._packetID);
byte[] playerId = BitConverter.GetBytes(packet._playerID);
Array.Copy(size, 0, openSegment.Array, openSegment.Offset + 0, 2);
offset += 2;
Array.Copy(packetId, 0, openSegment.Array, openSegment.Offset + offset, 2);
offset += 2;
Array.Copy(playerId, 0, openSegment.Array, openSegment.Offset + offset, 8);
offset += 8;*/
ArraySegment<byte> sendBuffer = SendBufferHelper.Close(offset);
if(success)
Send(sendBuffer);
}
}
.
.
.
}
클라이언트에서 생성하는 ServerSession은 위와 같이 구성하였습니다.
서버로 패킷을 전송하는 OnConnected 로직에서 위와 같이 직렬화를 수행합니다.
그리고 기존에 사용하던 로직 대신 위와 같이 로직을 구성하여
생성과 기록을 한번에 처리하도록 하였습니다.
만약, 유니티의 버전에 따라 해당 로직을 사용하지 못하는 경우
ToBytes 메서드의 로직을 사용하면 됩니다.
2) 서버
2 - 1) Server
{
class Program
{
// Listen 소켓을 생성할 새로운 Listener 인스턴스 생성
static Listener serverListener = new Listener();
static void Main(string[] args)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// Listener의 Init 함수에 인자인 DNS의 IPEndPoint와 Accept 이벤트 발생시 호출할 콜백함수를 넘겨줌
serverListener.Init(endPoint, () => { return new ClientSession(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
Server 역시, 패킷을 설정하는 과정을 ClientSession에서 대신 처리합니다.
ClientSession에서는 Send되어 직렬화된 데이터를 다시 역직렬화하는 작업을 수행합니다.
2 - 2) ClientSession
// 패킷의 인자를 설정하는 경우, 최대한 용량을 줄여주는 것이 좋음
class Packet
{
// 패킷의 용량
public ushort _packetSize;
// 패킷의 아이디
public ushort _packetID;
}
// PlayerInfoRequire의 멤버는 _packetSize, _packetID, _playerId
class PlayerInfoRequire : Packet
{
public long _playerID;
}
// PlayerInfo의 멤버는 _packetSize, _packetID, _hp, _attack
class PlayerInfo : Packet
{
public int _hp;
public int _attack;
}
public enum PacketID
{
PlayerInfoRequire = 1,
PlayerInfo = 2,
}
class ClientSession : PacketSession
{
.
.
.
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort offset = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
offset += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + offset);
offset += 2;
switch((PacketID)id)
{
case PacketID.PlayerInfoRequire:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + offset);
offset += 8;
Console.WriteLine($"PlayerInfoReg : {playerId}");
}
break;
}
Console.WriteLine($"RecvPacket has Id : {id} and Size : {size}");
}
}
ClientSession의 OnRecvPacket 함수에서는 직렬화되어 전송된 데이터를 역직렬화합니다.
이때 패킷의 아이디를 참고하여 패킷에 따라 다른 역직렬화를 수행합니다.
최종 실행결과는 다음과 같습니다.
클라이언트는 총 12 바이트의 데이터를 직렬화하여 서버로 전송하고 있고,
서버는 총 12바이트의 패킷을 역직렬화하여,
필요한 데이터를 추출하고 있는 것을 확인할 수 있습니다.
지금까지는 직렬화하기 위한 과정을 일일이 설정해주고 있지만,
이제 위의 과정을 자동화하도록 해보겠습니다.
'C#' 카테고리의 다른 글
UTF-8과 UTF-16 (0) | 2025.01.06 |
---|---|
직렬화(Serialization) 2 (0) | 2025.01.01 |
PacketSession (0) | 2024.12.31 |
RecvBuffer와 SendBuffer (0) | 2024.12.30 |
TCP - UDP의 이해 (0) | 2024.12.25 |