패킷은 메모리상에 있는 녀석이었다.
이 녀석을 네트워크상에서 흘려보내주기 위해서 byte배열에 패킷을 밀이넣어주고 있었다.
그리고 센드버퍼에 위의 byte배열을 복사하여 Send를 해주는 작업을 해주었다.
이렇듯 우리가 했었던 위의 작업,
즉 메모리의 데이터를 네트워크로 흘려주기 위하여 버퍼에 밀어넣는 작업이 직렬화이다.
반대로 OnRecvPacket작업처럼 버퍼에 있는 값을 가져와서 읽는 것을 역직렬화 라한다.
🌞 클라이언트쪽에서 PlayerId 요청
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,
}
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
//for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = SendBufferHelper.Open(4096);
bool success = true;
ushort count = 0;
//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;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), packet.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);
if(success)
Send(sendBuff);
}
}
DummyClient쪽에 Main쪽에 있던 GameSession을 클래스로 분리해주었다. 여기서의 이름은 ServerSession인데 그 이유는 대리자는 현재 서버쪽에 있기 때문에 이를 표현하기 위해서 ServerSession으로 만들어 주었다.
전 시간까지는 그냥 size와 packetId만 가지고 있는 패킷을 만들어 보내봤는데, 이번 시간에는 데이터가 있는 패킷을 보내었다.
다른점은 크게 없었다.
이런 Session을 만들때는 최대한 최적화를 해야한다.
그래서 기존에는 새로운 byte를 할당하여 해당 byte에 size, packetId, playerId를 저장했다.
이렇게 되면 안정성은 있지만, 할당과 Copy를 하는 일이 나뉘어져 있어서 속도가 느릴 것이다.
그래서 여기서 Bitconver의 TryWriteBytes메소드를 이용하여 버퍼에 바로 쓰도록 만들어 줄 수 있었다.
이렇게 최적화를 해주는 방법에는 이 방법외에도 여러가지고 있다고 한다.
TryWriteBytes 메소드의 경우 오류발생시 bool 값으로 false를 리턴하기 때문에 이를 이용하여 버퍼 이상의 값을 받는 경우를 걸러낼 수 있다.
size값은 앞에 부분임에도 마지막에 설정을 해주었다. 그 이유는 데이터를 넣으면서 크기에 해당하는 count값을 알아야 size를 넣어줄수 있기 때문이다.
이렇게해서 보내진 패킷은 Server측의 ClientSession에서 처리를 한다.
🌞 서버는 패킷을 읽고 콘텐츠 리턴
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}");
}
OnRecvPacket 메소드로 흘러들어간 패킷은 스위치문을 통해서 보내진 packetId를 확인하여 어떤 데이터가 들어있는지를 확인하고, 이에대한 콘텐츠 처리를 한다.
🌞 패킷의 자동 생성
위의 코드에서는 Packet의 생성을 하나하나씩 해주고 있다. 하지만 이는 너무 비효율적이다. 이를 Packet 종류별 생성자를 만들어보자.
Packet 객체를 생성 할 때, packet의 size와 packetId를 지정하고, Write와 Read 메소드를 만들어서 패킷을 작성하거나 읽는 작업을 쉽게 만들어보자.
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(s.Array, s.Offset + count);
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);
bool success = true;
ushort count = 0;
//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), this.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), this.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
if( success == false )
return null;
return SendBufferHelper.Close(count);
}
}