공부/인프런 - Rookiss

Part 4-3-13. 네트워크 프로그래밍 : PacketSession

셩잇님 2023. 11. 8. 20:49
반응형

 

 

네트워크 프로그래밍

 

 지난 시간에는 SendBuff 클래스를 만들었다. 클래스 내부에는 버퍼로 활용해 줄 recvBuffer, 사용한 영역을 표기하는 usedSize, 남은 공간의 크기를 나타내는 FreeSize를 선언해준다. 또한 메서드로는 Receive와 같이 Open, Close 함수를 만들어었지만, Receive와는 다르게 Clean 함수는 만들어주지 않았다. 🤔

 

 이 후, TreadLocal을 이용한 SendBufferHelper를 만들어 SendBuff 클래스 내부를 건드리지 않고 사용하였다. 

 


 

 

패킷 전송하기 📤

 

 패킷을 전송하는 방법까지 알아보았지만, 정작 패킷만 보았을 때 패킷의 사이즈가 어느정도인지 아직은 알 수 없다. 따라서 보통 패킷의 첫 번째 인자로는 사이즈를 넣어주고, 두 번째 인자로는 패킷의 ID를 넣어 사용한다. 여기서 말하는 패킷의 ID란 해당 패킷이 어떤 패킷인지에 대한 구분 값이다. 예를 들어 패킷 ID가 1이라면 이동, ID가 2라면 스킬과 같은 형태로 나뉜다. 이 때 중요한 것은 기존처럼 int를 사용하는 것이 아니라 ushort를 사용하여 값을 표현한다는 것이다.

 

❓ 그냥 평소처럼 Int 쓰면 되는거아냐?

 이제는 서버와 전송을 하기 때문에 최대한 용량을 압축해서 보내야 한다. 예를들어 패킷을 보낼 때에도 int 형으로 Size와 Id를 만들경우 ushort 대비 4바이트를 더 사용하게 되기 때문이다. 이게 만약 10,000명이라면 40,000 byte를 낭비하게 되는 것이다. 따라서 패킷을 보낼 때에는 최대한 압축해서 보내는 것이 중요하다.

 

이제 진짜 작업을 해보자! 😤

 기존 Session Class에서 사용하던 OnRecv를 오버라이딩하여 사용할 것이다. Send 메서드를 실행할 때에도 size, Id 등과 같은 설정등의 변화가 있지만, 현재는 패킷을 받을 때 (OnRecv) 해당 패킷의 이상 여부를 검사하여 작업을 처리할 것이다.

 

ServerCore의 Session 클래스

    public abstract class Session
    {
        Socket _socket;
        int _disconnected = 0;

        RecvBuffer _recvBuffer = new RecvBuffer(1024);

        object _lock = new object();
        Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
        List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();

        public abstract void OnConnected(EndPoint endPoint);
        public abstract int OnRecv(ArraySegment<byte> buffer);
        // ⭐ OnSend를 오버라이딩하여 사용한다!
        public abstract void OnSend(int numOfBytes);
        public abstract void OnDisconnected(EndPoint endPoint);
        
        ...
    }

 

 

ServerCore의 Session 클래스 스크립트 내부에 새롭게 생성하는 PacketSession 클래스

 Packet 검사를 진행하고 이를 넘겨주어 처리하기 위한 Class를 생성해준다. 

 

 

 자세히 보면 oveeride 하는 OnRecv 함수에 sealed 키워드가 붙어 있는 것을 볼 수 있다. 해당 키워드는 PacketSession을 상속 받는 곳에서, 이를 다시금 오버라이드 할 수 없게 봉인해주는 역활을 한다. 따라서 PacketSession을 상속받은 객체는 OnRecv를 사용하지 못한다. 따라서 해당 함수를 이용하여 내가 원하는 패킷이 모두 도착했는지 확인하는 과정을 거친다.

 

예외처리 🚧

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

            while (true)
            {
                // 최소한 헤더는 파싱할 수 있는지 확인
                if (buffer.Count < HeaderSize)
                {
                    break;
                }

                // 패킷이 완전체로 도착했는지 확인
                ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
                
                // 패킷이 완전체가 아닌 부분적으로 온 상태
                if (buffer.Count < dataSize)
                {
                    break;
                }

                // 여기까지 왔으면 패킷 조립 가능
                OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
            }

            return 0;
        }

 

 먼저 예외 처리 1. 최소한의 헤더는 파싱할 수 있는지 확인한다. 이 후 패킷이 완전체로 도착했는지 ushort를 이용하여 검사한다.

 

 도착했음에도 불구하고 또 다른 예외처리를 진행한다. 예외 처리 2. 패킷이 완전체가 아닌 부분적으로 온 상태인지 확인한다. 위 두가지의 예외처리를 만족하면, buffer를 받아온 datasize 만큼 넘겨주어 첫 번째 패킷을 (현재 size, id만 담긴 값) OnRecvPacket 메서드에게 넘겨준다. 

 

// 별도로 보내주는 OnRecvPacket 인터페이스로 받아야 한다.
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
ArraySegment는 Sturct를 사용하여 Heap 영역에 할당되지 않기 때문에 new 키워드를 사용해도 문제가 되지 않는다.

 

 이 후, 기존 Server에서 Session을 상속받 부분을 PacketSession으로 변경하여 준다.

 

Server의 Program.cs

    class GameSession : PacketSession // 수정 ⭐
    {
        public override void OnConnected(EndPoint endPoint)
        {
            ...
        }

        ...
    }

 

 


 

 

Server의 Program.cs

 GameSession이 PacketSession을 상속받았기 때문에 abstract로 생성한 OnRecvPacket을 구현해주어야 한다. 다음과 같이 구현하여 BitConverter를 통해 받아온 인자 값이 정상적으로 잘 들어오는지 출력해보자.

 

        public override void OnRecvPacket(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}");
        }

 

다시 ServerCore의 Session.cs에 PacketSession 클래스로 이동하여 살펴보자. OnRecv로 돌아와서 processLength에 받음 받은 datasize 만큼 증가시키고, buffer에서는 사용한 부분 만큼을 제외하여, 다음 패킷으로 넘겨준다.

 

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

            while (true)
            {
                // 최소한 헤더는 파싱할 수 있는지 확인
                if (buffer.Count < HeaderSize)
                {
                    break;
                }

                // 패킷이 완전체로 도착했는지 확인
                ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
                
                // 패킷이 완전체가 아닌 부분적으로 온 상태
                if (buffer.Count < dataSize)
                {
                    break;
                }

                // 여기까지 왔으면 패킷 조립 가능
                OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
                
                // ⭐ 추가
                processLen += dataSize;
                buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
            }

            return 0;
        }

 

 


 

 

Dummy Client

 DummyClient 프로젝트 내 Progrm 스크립트로 이동하여 Send를 테스트하자. 기존에 테스트했던 패킷 클래스와 Server의 Program.cs Onconnect 내부에 있던 스크립트를 복사해서 붙여넣고 테스트를 해보자.

 

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            // 로그
            Console.WriteLine($"OnConnected : {endPoint}");

            Packet packet = new Packet() { size = 4, packetId = 7 };

            // 보낸다
            for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> openSegment = SendBufferHelper.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 = SendBufferHelper.Close(packet.size);

                Send(sendBuff);
            }
        }
        
        ...
    }

 

진행 순서 🔡

 

1. 클라이언트 연결 요청

2. 서버 승인
→ session.Start()를 통해 Recv 함수 동작

→ OnConnected() 진입 및 패킷 전송 (size, packetId)

3. Recv > Register > Complete > 서버의 OnRecv 동작하여 size, packetId 출력!

 

위와 같은 과정을 거치며 동작한다. 

 

결과 😎

 

 

임의로 넣어주었던 packetId 값인 7이 정상적으로 뜨는 것을 볼 수 있다. 어렵다 어려워!

 

 

 

반응형