Serialization 1
이전 포스팅에서는 패킷을 직접 만들어서 그것을 바이트로 변환해 보내는 작업을 했다.
이번 포스팅에서는 그 과정을 다뤄볼 예정이다.
직렬화는 네트워크에서만 사용하는 내용은 아니고 포괄적인 내용을 다루기는 한다. 세이브 파일을 만들거나 그럴때도 사용하긴 한다.
직렬화는 메모리상에 존재하는 데이터를 납작하게 압축해서 바이트로 저장, 전송하는 것이라고 보면 된다.
직렬화를 바로 해보진 않고 여러 패킷들을 직접 만들어 본 후 압축하는 과정을 거쳐서 어떤 과정이 필요할지에 대해 고민해보자.
먼저 PlayerInfoPeq, PlayerInfoOk라는 패킷이 있다고 해보자.
public class Packet
{
public ushort size;
public ushort packetId;
}
class PlayerInfoReq : Packet
{
public long playerId;
}
class PlayerInfoOk : Packet
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
나중에는 enum 값들도 다 자동화 해야할 것이다.
ArraySegment<byte> s = SendBufferHelper.Open(4096);
byte[] size = BitConverter.GetBytes(packet.size); // 2바이트
byte[] packetId = BitConverter.GetBytes(packet.packetId); // 2바이트
byte[] playerId = BitConverter.GetBytes(packet.playerId); // 8바이트
ushort count = 0;
Array.Copy(size, 0, s.Array, s.Offset + count, 2);
count += 2;
Array.Copy(packetId, 0, s.Array, s.Offset + count, 2);
count += 2;
Array.Copy(playerId, 0, s.Array, s.Offset + count, 8);
count += 8;
ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);
Send(sendBuff);
이런식으로 보내면 조금 더 간단히 보낼 수 있을 것 같다. 위는 PlayerInfoReq 패킷을 보내는 과정이다.
받는 입장에서는 어떻게 하면 편하게 받을지 고민해보자.
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;
}
Console.WriteLine($"RecvPacketId : {id}, Size = {size}");
}
이런식으로 받을 수 있을 것이다.
여기서 최적화를 한번 해보자. 패킷을 보내고 받는건 되게 중요하기때문에 여기서는 신경을 써야한다.
일단 맘에 안드는 부분은 패킷을 변환할때이다. BitConverter를 사용해서 GetBytes를 하는 순간 사실상 byte[] n = new byte[]를 하는 것이다. 효율성이 매우 떨어질 것이다.
이것을 해결하기 위해서는 여러 방식이 존재한다.
ArraySegment<byte> s = SendBufferHelper.Open(4096);
ushort count = 0;
bool sucesss = true;
//sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), packet.size);
count += 2;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.packetId);
count += 2;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.playerId);
count += 8;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);
if(sucesss)
Send(sendBuff);
TryWirteBytes를 이용해서 우리가 예약한 바이트에 직접적으로 집어넣을 수 있어서 효율적일 것이다. 그런데 유니티에서 사용할 수 있을지 없을지는 그 버전에 따라 다르다.
Serialization 2
이번에는 추가적으로 이 작업을 함수 형식으로 하면 좋을것 같다는 생각이 드니 이것을 해보자.
public abstract class Packet
{
public ushort size;
public ushort packetId;
public abstract ArraySegment<byte> Write();
public abstract void Read(ArraySegment<byte> s);
}
class PlayerInfoReq : Packet
{
public long playerId;
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
public override void Read(ArraySegment<byte> s)
{
ushort count = 0;
//ushort size = BitConverter.ToUInt16(s.Array, s.Offset);
count += 2;
//ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count);
count += 2;
this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
count += 8;
}
public override ArraySegment<byte> Write()
{
ArraySegment<byte> s = SendBufferHelper.Open(4096);
ushort count = 0;
bool sucesss = true;
//sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), packet.size);
count += 2;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), this.packetId);
count += 2;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), this.playerId);
count += 8;
sucesss &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
if (sucesss == false)
return null;
return SendBufferHelper.Close(count);
}
}
패킷 자체적으로 함수를 넣어 처리하는 방식이다.
Serialization 3
이번에는 고정적으로 바이트의 크기를 가지는 것이 아닌 가변적인 길이의 바이트를 가진 데이터를 처리하는 방식에 대해 알아보자.
만약 string을 보낸다고 생각해보자. string의 크기는 정확히 얼마일지 아무도 모른다. 그렇기 때문에 ushort로 string의 크기를 같이 보낸다면 해결할 수 있을 것이다.
class PlayerInfoReq : Packet
{
public long playerId;
public string name;
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
public override void Read(ArraySegment<byte> s)
{
ushort count = 0;
ReadOnlySpan<byte> sp = new ReadOnlySpan<byte>(s.Array, s.Offset, s.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerId = BitConverter.ToInt64(sp.Slice(count, sp.Length - count));
count += sizeof(long);
ushort nameLen = BitConverter.ToUInt16(sp.Slice(count, sp.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
}
public override ArraySegment<byte> Write()
{
ArraySegment<byte> s = SendBufferHelper.Open(4096);
ushort count = 0;
bool sucesss = true;
Span<byte> sp = new Span<byte>(s.Array, s.Offset, s.Count);
count += sizeof(ushort);
sucesss &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), this.packetId);
count += sizeof(ushort);
sucesss &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), this.playerId);
count += sizeof(long);
// string UTF-16
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, s.Array, s.Offset + count + sizeof(ushort));
sucesss &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
sucesss &= BitConverter.TryWriteBytes(sp, count);
if (sucesss == false)
return null;
return SendBufferHelper.Close(count);
}
}
nameLen을 뽑아내는것과 동시에 길이를 저장하고 그것을 패킷으로 보내는것이다.
Serialization 4
마지막으로 List를 직렬화 하는 방법에대해 알아보자. List가 단순한 고정 데이터를 가지고 있는 경우라면 List의 크기를 계산하기 쉬울것이다. 하지만 List안에 들어가 있는 데이터가 구조체나 class일 경우 조금 힘들어 질 것이다.
그러니 안에 있는 데이터를 꺼내서 그 사이즈를 판별해서 패킷을 만들어야한다.
class PlayerInfoReq : Packet
{
public long playerId;
public string name;
public struct SkillInfo
{
public int id;
public short level;
public float duration;
public bool Write(Span<byte> s, ref ushort count)
{
bool success = true;
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), id);
count += sizeof(int);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), level);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), duration);
count += sizeof(float);
return success;
}
public void Read(ReadOnlySpan<byte> s, ref ushort count)
{
id = BitConverter.ToInt32(s.Slice(count, s.Length - count));
count += sizeof(int);
level = BitConverter.ToInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
duration = BitConverter.ToInt32(s.Slice(count, s.Length - count));
count += sizeof(float);
}
}
public List<SkillInfo> skills = new List<SkillInfo>();
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
public override void Read(ArraySegment<byte> s)
{
ushort count = 0;
ReadOnlySpan<byte> sp = new ReadOnlySpan<byte>(s.Array, s.Offset, s.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerId = BitConverter.ToInt64(sp.Slice(count, sp.Length - count));
count += sizeof(long);
ushort nameLen = BitConverter.ToUInt16(sp.Slice(count, sp.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
ushort skilllen = BitConverter.ToUInt16(sp.Slice(count, sp.Length - count));
count += sizeof(ushort);
skills.Clear();
for (int i = 0; i < skilllen; i++)
{
SkillInfo skill = new SkillInfo();
skill.Read(s, ref count);
skills.Add(skill);
}
}
public override ArraySegment<byte> Write()
{
ArraySegment<byte> s = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> sp = new Span<byte>(s.Array, s.Offset, s.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), this.packetId);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), this.playerId);
count += sizeof(long);
// string UTF-16
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, s.Array, s.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
// Skill list
success &= BitConverter.TryWriteBytes(sp.Slice(count, sp.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach (SkillInfo skillInfo in skills)
{
success &= skillInfo.Write(s, ref count);
}
success &= BitConverter.TryWriteBytes(sp, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
지금 이러한 과정을 직렬화의 이론이라고 생각하면된다. 실제로 이를 자동화하지 않고 사용하는건 되게 실수를 많이 유발하고 불편할 것이다. 물론 직접 만드는 것이 더 좋을 때도 있긴하다.
다음 포스팅은 직렬화하는것을 일반화, 자동화해보자.
'Unity > 온라인 RPG' 카테고리의 다른 글
[게임 서버] Job Queue - 채팅 테스트 (0) | 2024.02.01 |
---|---|
[게임 서버] 패킷 직렬화 - Packet Generator (0) | 2024.01.30 |
[게임 서버] 네트워크 프로그래밍 - Connector, TCP? UDP?, Buffer와PacketSession (1) | 2024.01.22 |
[게임 서버] 네트워크 프로그래밍 - Listener와 Session (0) | 2024.01.19 |
[게임 서버] 네트워크 프로그래밍 - 네트워크 기초 및 소켓 프로그래밍 (0) | 2024.01.17 |