본문 바로가기
cs공부/네트워크프로그래밍

네트워크프로그래밍 - 패킷 자동화 처리(TryWriteBytes,c#에서 포인터 사용하는 법)

by 코딩하는 돼징 2023. 8. 2.
반응형

패킷작업하기

서버, 클라이언트에 이 정보가 다 있어야 한다.

class Packet
{
    public short size;
    public short packetId;
}
class PlayerInfoReq : Packet
{
    public long playerId;
}
class PlayerInfoOk : Packet
{
    public int hp;
    public int attack;
}
public enum PacketID
{
    PlayerInfoReq = 1,
    PlayerInfoOk = 2,
}

보내는거 설정

01 플레이어 정보 요청을 나타내는 패킷, PlayerInfoReq클래스의 인스턴스 생성

PlayerInfoReq는 클라이언트로부터 플레이어 정보를 요청하는 클래스이다.

PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq };

02 packet객체를 바이트 배열로 변환화여 네트워크로 전송할 준비

ArraySegment<byte> openSegment = SendBufferHandler.Open(4096);
byte[] size = BitConverter.GetBytes(packet.size);
byte[] packetId = BitConverter.GetBytes(packet.packetId);
Array.Copy(size, 0, openSegment.Array, openSegment.Offset, size.Length);
Array.Copy(packetId, 0, openSegment.Array, openSegment.Offset + size.Length, packetId.Length);
ArraySegment<byte> sendBuff = SendBufferHandler.Close(packet.size);

SendBufferHandler.Close

패킷의 크기를 전달받아 적절한 크시로 송신 버퍼를 잘라낸다.

sendBuff

이 byte배열은 네트워크를 통해 클라이언트로 전송되고 수신한 byte배열을 역직렬화하여 패킷을 복원하고 패킷의 내용을 파싱하여 서버 요청에 응답한다.


03 playerId 추가

 PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
 byte[] playerId = BitConverter.GetBytes(packet.playerId);
 Array.Copy(playerId, 0, openSegment.Array, openSegment.Offset + size.Length, playerId.Length);

자동화를위해서 어떤 작업이 필요할까?

위의 작업들에서는 데이터를 원하는 위치로 직접 복사한다. 이는 수동으로 주소를 계산하고 복사한다. 이를 직접 계산하지 않고 자동으로 위치를 계산하는 방식을 사용해보자


04 count 변수를 사용하여 위치를 자동으로 계산

ushort count = 0;
Array.Copy(size, 0, openSegment.Array, openSegment.Offset + count, 2);
count += 2;
Array.Copy(packetId, 0, openSegment.Array, openSegment.Offset + count, 2);
count += 2;
Array.Copy(playerId, 0, openSegment.Array, openSegment.Offset + count, 8);
count += 8;
ArraySegment<byte> sendBuff = SendBufferHandler.Close(count);

05 bytes[]을 TryWirteBytes로 수정

byte[]를 만들면서 보내는거는 효율성이 떨어진다.

왜냐하면 데이터를 생성한 후 해당 데이터를 새로운 byte배열로 복사한다. 이 경우 크기가 큰 데이터를 복사할 때 메모리에 새로운 배열을 할당하고 원래 데이터를 복사하는 데 많은 시간과 메모리가 소모된다. 

byte[] size = BitConverter.GetBytes(packet.size); // 2
byte[] packetId = BitConverter.GetBytes(packet.packetId); // 2
byte[] playerId = BitConverter.GetBytes(packet.playerId); // 8

06 TryWriteBytes를 사용하는 것으로 고쳐보자


TryWriteBytes알아보러 가기

 

C# - TryWriteBytes

TryWriteBytes 메모리에 새로운 배열을 할당하는 대신 이미 할당된 메모리 영역인 세그먼트 버퍼를 활용하여 데이터를 작성한다. 그러므로 Span를 사용하여 기존의 메모리를 참조한다. 이로인해 데

code-piggy.tistory.com


size, packetId는 ushort이라 2씩 더해주고 playerId는 long이라 8을 더해준다.

bool success = true;
ushort count = 0;
               
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), packet.size);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count-count), packet.playerId);
count += 8;

07 최종데이터 크기

아래 코드를 확인해보면 최종적으로 쓰이는 데이터의 크기가 size가 아니라 count변수에 의해 결정된다는 것을 확인할 수 있다. size는 ushort이라 2byte를 최종크기라고 생각했는데 count변수는 2 + 2 + 8 = 12가 최종크기가 된다. 그러므로 최종적으로 쓰이는 데이터의 크기는 12byte가 된다.

따라서 마지막으로 count값을 전테 데이터 크기로 설정해준다.

count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count-count), packet.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count),count);

08 c#에서 포인터 사용하는 법


설명 참조

 

C#- ToBytes(c#에서 포인터 사용해보기)

C#에서 포인터를 사용하여 ulong타입의 값을 바이트 배열에 변환하여 저장하는 것으로 예시 ToBytes static unsafe void ToBytes(byte[] array, int offset, ulong value) 매개변수 array : 변환할 데이터를 저장하는 바

code-piggy.tistory.com


static unsafe void ToBytes(byte[] array, int offset, ulong value)
{
    fixed (byte* ptr = &array[offset])
        *(ulong*)ptr = value;
}

08 success가 true인 경우에만 Send하기

if (success)
{
    Send(sendBuff);
}

받는거 설정

buffer배열에서 데이터를 역직렬화하여 packId와 해당 packet에 필요한 데이터를 추출

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.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 8;
        Console.WriteLine($"PlayerInfoReq: {playerid}");
    }
    break;
}

결과 확인

 

 

 

 

 

 

참고 :  본 내용은 MMORPG  PART4 강의를 수강하여 작성하였습니다.

https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4

 

 

 

 

 

 

반응형

댓글