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

네트워크 프로그래밍 - PacketSession

by 코딩하는 돼징 2023. 7. 31.
반응형

패킷작업을 위해서 뭐가 필요할까

기존 세션 코드에 기능을 추가해서 패킷이 들어가므로 수정이 필요하다.

패킷은 네트워크를 통해 전송되는 데이터의 단위이며 TCP와 같은 전송 프로토콜은 데이터를 일정 크기의 블록인 패킷을 분할하여 전송한다.

TCP의 경우 패킷이 완전체로 오지 않은 경우가 있으므로 완전체인지 아닌지 구분하는 작업이 필요하다.

 

패킷 통신을 위한 구조와 처리 과정

01 Packet 클래스 설정

size는 패킷의 크기, packetid는 이를 통해 번호에 따라서 패킷을 구분할 수 있다.

class Packet
{
    public int size;
    public int packetId;
}

02 LoginOfPacket

LoginOfPacket에는 캐릭터의 정보들을 리스트로 보내준다는 가정하에 이는 크기가 유동적으로 변할 수 있으므로 패킷의 크기를 알기가 어렵다. 그러므로 Packet클래스에 size가 필요하다.

class LoginOfPacket : Packet
{

}

 


03 short 사용

데이터를 송수신하고 처리하는 과정에서 최적하된 데이터 구조와 압축 방법을 사용하여 효율적으로 데이터를 처리하는 것이 중요하다. 그러므로 최대한으로 데이터의 사이즈를 줄이는 것이 좋다.

그러므로 int(4Byte)을 사용하는 것 보다 short(2Byte)를 사용하는 것이 좋다.

class Packet
{
    public short size;
    public short packetId;
}

패킷의 파싱 과정

Packet을 사용하는 부분은 따로 만든다.

public abstract class PacketSession : Session 
{
}

01 OnReceive

패킷 데이터가 수신되었을때 호출

sealed

다른 클래스가 PacketSession을 상속받은 다음에 OnReceive를 override할려고 하면 에러가 난다.

public sealed override int OnReceive(ArraySegment<byte> buffer)
{

}

그러므로 PacketSession을 상속받은 애들은 이 인터페이스가 아니라 아래를 사용해야한다.

 public abstract void OnReceive(ArraySegment<byte> buffer);

데이터 올때의 모습

OnReceive에서 패킷이 오면 parsing을 한다. size가 2byte가 왔는지 확인 후 만약에 안왔으면 데이터가 완전히 도착할 때까지 기다렸다가 처리한다.

public sealed override int OnReceive(ArraySegment<byte> buffer)
{
    int processLen = 0;
    while(true)
    {

    }

    return processLen;
}

processLen은 처리한 데이터의 길이를 누적하여 기록하는 변수이다.


02 Header 크기 설정

public static readonly int HeaderSize = 2;

03 최소한 Header가 파싱할 수 있는지 확인

무한 루프를 돌면서 패킷의 헤더를 파싱한다. 이는 패킷의 헤더 정보가 도착할 때까지 기다린다. 

while(true)
{
    if (buffer.Count < HeaderSize)
        break;
}

buffer.Count와 HeaderSize의 크기를 비교하면서 현재 수신된 데이터의 크기가 HeaderSize보다 작다면 아직 패킷의 헤더가 완전히 도착하지 않았다는 의미이다. 따라서 더 많은 데이터가 올 때까지 루프를 돌며 기다리게 된다.


04 패킷이 완전체로 도착했는지 확인

ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize)
    break;

buffer에서 읽어온 dataSize와 현재 수신된 데이터의 크기인 buffer.Count를 비교한다. 만약 현재 수신된 데이터 크기가 dataSize보다 작다면 아직 해당 패킷의 모든 데이터가 도착하지 않았다는 의미이다. 그러므로 다음 패킷 데이터를 수신하기 위해 기다린다. 


05 위의 상태들에 다 부합하지 않으면 패킷이 조립가능

// public abstract void OnReceivePacket(ArraySegment<byte> buffer); 사용
OnReceivePacket(new ArraySegment<byte>(buffer.Array, buffer.Offset,dataSize));

05 processLen크기 바꿔주기

processLen += dataSize;

현재까지 파싱한 데이터의 크기에 현재 패킷의 데이터 크기를 더해준다. 이를 통해 누적된 데이터의 크기를 업데이트한다.


06 다음 부분 집어주기

buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize,buffer.Count-dataSize);

현재까지 파싱한 데이터를 제외한 나머지 데이터를 새로운 버퍼로 만든다. 즉 현재 처리한 패킷의 데이터를 버퍼에서 제거하고 남은 데이터를 새로운 버퍼로 할당한다.

이는 다음 패킷의 파싱에 이전에 처리한 데이터가 포함되지 않고 새로운 패킷의 데이터만 처리한다.

 

버퍼를 새로 만들면 메모를 많이 잡아먹지 않을까?

ArraySegment 자체는 값 형식(struct)이기 때문에 Stack에 할당되고 ArraySegment가 참조하는 배열은 Heap에 할당된다. 그러므로 변수가 스택에 있더라도 그 변수가 참조하는 배열 데이터는 힙에 할당된 원본 배열을 공유하게 된다. 따라서 별도의 배열 데이터를 복사하지 않고 배열의 일부를 참조하는 방식으로 동작하므로 메모리 사용 측면에서 효과적이다.


07 처리한 바이트 수 뽑기

return processLen;

현재까지 누적된 데이터의 크기를 반환한다. 이 값은 누적된 데이터의 크기를 의미하며 다음 패킷 파싱을 위해 남은 데이터를 가리키는 buffer에는 processLen크기만큼 데이터가 제거된 상태로 할당된다.


패킷 사용하기

Server

OnReceive은 위에서 sealed처리하였으므로 지워준다.

class GameSession : PacketSession
{
    public override void OnConnected(EndPoint endPoint){ }
    public override void OnDisconnected(EndPoint endPoint){ }
    public override void OnSend(int numofBytes){ }
}

OnRecivePacket은 구현한다.

class GameSession : PacketSession
{
    public override void OnConnected(EndPoint endPoint){ }
    public override void OnDisconnected(EndPoint endPoint){ }
    public override void OnSend(int numofBytes){ }
    public override void OnReceivePacket(ArraySegment<byte> buffer){ }
}

public override void OnReceivePacket(ArraySegment<byte> buffer)
{
    ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
    ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
    Console.WriteLine($"RecvPacketid: {id}, Size {size}");
}

Client

namespace DummyClient
{
    class Packet
    {
        public short size;
        public short packetId;
    }
    public class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Packet packet = new Packet() { size = 4, packetId = 7 };
            Console.WriteLine($"OnConnected : {endPoint}");

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

                Send(sendBuff);
            }
        }
        ... 생략

결과 확인

 

 

 

 

 

 

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

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

 

 

 

 

 

 

 

반응형

댓글