Server에서 Client로, 또는 반대의 상황에서 정보를 주고 받는 방법을 살펴보자.
Packet을 상속받는 정보들이 있다. Packet에는 모든 아이들이 반드시 가져야 할 기본 정보인 size와 packetId, Write()와 Read()를 지니고 있다.
public abstract class Packet
{
public ushort size;
public ushort packetId;
public abstract ArraySegment<byte> Write();
public abstract void Read(ArraySegment<byte> s);
}
size는 말 그대로 정보의 크기다. packetId는 패킷을 구분해줄 장치다. Write는 패킷을 보낼 때 사용하고 Read는 패킷을 받을 때 사용한다.
Player의 정보를 갖고 있는 PlayerInfoReq라는 패킷을 만들 것이다. 여기에 추가할 정보는 playerId와 name이라는 정보가 추가된다. 그리고 우리는 Write와 Read에서 무슨 일이 일어나야 하는지 알려줘야 한다.
class PlayerInfoReq : Packet
{
public long playerId;
public string name;
}
✅ Read
패킷을 읽어올 방법은 패킷 정보를 순서대로 읽어나가기다. 우리는 패킷에 담긴 정보를 읽고 우리가 읽은 정보의 크기만큼을 count에 담을 것이다. 다음으로 읽어야할 정보는 패킷에서 count만큼 뒤에 담겨있는 정보다.
public override void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
}
Read를 호출할 때 받은 segment는 우리가 읽어야할 정보를 담고 있는 ArraySegment다. 우린 이 segment를 ReadOnlySpan으로 변경해 사용한다. 기존보다 활용성이 높다. ReadOnlySpan은 말 그대로 읽기 전용, 수정이 불가능하다. 배열을 안전하게 사용할 수 있도록 돕는다.
Packet에서 반드시 들어가는 정보인 size와 packetId는 둘다 ushort 타입이다. 즉, 크기가 2바이트 고정이다. size에 어떤 값이 들어있는지 고민할 필요 없이 count를 늘려줄 수 있다. size와 packetId는 우선 읽어올 필요가 없어서 count값만 늘려주고 따로 값을 저장하지는 않았다.
PlayerInfoReq 패킷에서 추가되는 두가지 정보 중 playerId는 long 타입이다. 8바이트 고정이다. count를 늘려준다.
name은 다르다. string은 정보에 따라 들어오는 길이가 달라진다. 우선은 string이 몇 바이트인지 알아내야 한다. 그것만 알면 다른 정보처럼 count를 늘리고 정보를 읽어내면 된다.
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
nameLen을 만들어서 s에서 size, paketId, playerId를 제외하고 남은 바이트의 크기를 저장한다. 이게 바로 name의 크기다. count에는 ushort의 크기를 추가한다. name을 읽을 때 정보의 크기를 넣을 때 nameLen을 넣어주면 name을 읽을 수 있다.
✅ write
Write도 Read처럼 순서대로 써나가면 된다. 우선 정보를 담아올 커다란 그릇을 지정하고 정보의 크기를 넣을 count와 Write의 성공여부를 판별할 success를 선언한다.
public override ArraySegment<byte> Write()
{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset + count, segment.Count - count);
}
정보를 써내려간다. 정보의 크기에 따라 count 값을 늘리고 다음 정보는 처음 시작한 위치에서 count만큼 뒤로 밀린 곳에 적는다.
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.packetId);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
여기서도 string은 먼저 크기를 알아낸 후, 그 크기만큼 뒤로 넘어간 위치에 정보를 적어야 한다. string을 적는 데에는 두 가지 방법이 있다. 하나는 Encoding.Unicode.GetByteCount를 사용하는 것이다.
ushort nameLen = (ushort)Encoding.Unicode.GetByteCount(this.name);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
Array.Copy(Encoding.Unicode.GetBytes(this.name), 0, segment.Array, count, nameLen);
count += nameLen;
GetByteCount는 말 그대로 바이트의 크기를 알려주는 함수다. 이를 통해 string의 크기를 알아내고 정보를 적어나가면 된다.
다른 하나는 GetBytes를 사용하는 방법이다.
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
둘다 결과는 같지만 뒤에 나온 방식은 Array.Copy처럼 new를 하는 부분이 없어 메모리 활용에 더 좋다.
정보를 모두 쓴 후에는 성공 여부를 판단해 실패한 경우 null을 리턴하고 Buffer 작성을 끝낸다는 표시를 한다.
결과적으로 패킷의 Read와 Write를 모두 작성한 Session의 모습이다.
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 string name;
public override void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
// string
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nemaLen
}
public override ArraySegment<byte> Write()
{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset + count, segment.Count - count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.packetId);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
// string
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
'공부 > 게임 서버' 카테고리의 다른 글
[게임 서버 / C# ] Socket Error (10061) : 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다. (0) | 2022.10.07 |
---|---|
[게임 서버] 게임 해킹에 대해 알아보자 (0) | 2022.09.30 |
[게임 서버/암호학] 대칭키와 비대칭키 (0) | 2022.09.30 |
[게임 서버] await 사용하기 (0) | 2022.07.11 |
[게임 서버] SetBuffer를 RecvBuffer, SendBuffer로 따로 빼내기 #2 (0) | 2022.06.21 |