PacketGenerator [총 정리]

Eunho Bae·2022년 5월 10일
0

솔루션 구조

크게 ServerCore, Server, DummyClient와 Tool로써 PacketGenerator 세 프로젝트로 구성된 솔루션이다.

PacketGenerator

PacketGenerator는 말 그대로 패킷을 생성시켜주는 프로젝트인데

  • PDL.xml에 패킷의 구조를 정의해두고
  • PacketManager의 Program의 Main 함수에서 PDL.xml에 있는 내용을 한줄씩 가져와 변수에 맞게 미리 정의된 포맷에 넣어주고 소스코드화 시켜준다.

 <long name="playerId"/>

위 코드에서 r이 PDL.xml의 하나의 라인(문자열)인데 name의 타입을 가져와서 switch로 분기한 다음 PacketFormat.cs에 정의해 놓은 소스코드에 PDL에서 가져온 문자열을 붙여놓는 것을 볼 수 있다.
예를들어, 현재 r이 라면 memberType은 long, memberName은 playerId가 될 것이다.

PacketFormat.cs에서 위와 같이 정의를 했으니 매개변수 순서대로 각각 {0}과 {1}에 들어가 최종적으로 memberCode에는 "public long playerId;"가 들어가게 된다.

이렇게 하면 패킷을 다르게 정의해도 그때마다 매번 소스코드를 작성하지 않아도 패킷 내에 정의된 변수들을 자동으로 가져와 소스코드에 붙여줌으로써 자동화시킬 수 있다.

PacketGenerator 솔루션을 배치파일을 작성했다.
첫줄에서 PacketGenerator 솔루션을 컴파일하면 exe파일이 만들어지는데, 그 exe파일을 실행시켜줌과 동시에 Main함수의 args로 PDL.xml의 경로를 전달해준다. 그렇게 해서 만들어진 세 개의 cs파일들을 지정된 경로에 전부 복사시켜주도록 했다.
그렇게 해서 맨 위 솔루션 이미지의 구조와 같이 Server와 DummyClient에 각각 Packet 폴더가 생성되도록 했다.

DummyClient

DummyClient는 플레이어 개인이라고 생각하면 되고 각자 서버에 연결을 도와주는 Connector라는 객체를 가지고 있다. 클라이언트들은 각자 Connector 객체를 생성하고, 객체에 정의된 Connect함수의 매개변수에 EndPoint 정보와 서버와 연결이 성공적으로 끝났으면 어떤 세션을 생성할지 전달해준다.

그렇게 소켓을 생성해주고 비동기적으로 연결요청(ConnectAsync)을 해주면 어느 순간 호출된 OnConnectCompleted에서 Session을 만들어준다. 이때 Connect 함수에서 args에 등록했던 정보를 다시 빼서 session의 OnConnected() 함수에 전달해주고, Start() 함수에도 연결된 소켓 정보를 전달해준다.

ServerSession 클래스의 OnConnected에 종단점 정보를 전달해주면 출력을 한번 해주고, PacketGenerator로 생성한 C_PlayerInfoReq에 플레이어 정보를 넣어준다. skill.attributes와 packet.skills은 전부 list인데 스킬과 속성 정보를 정해주고 list에 추가해준다.

그렇게 해서 하나의 packet이 만들어지면 생성된 패킷 내부에 정의된(PacketFormat)에서 사이즈를 넉넉하게 예약(reserveSize)을 한 후(쓰레드를 처음 생성했다면 ChunkSize만큼 할당받음) 패킷의 사이즈를 카운트하고 다양한 타입의 변수들을 BitConverter를 이용해 바이트화해서 쭉 붙인 다음 전체 버퍼에서 offset위치에서 카운트한 범위만큼 ArraySegment를 이용해 긁어서 반환해준다.


위에 보면 하나의 프로세스(DummyClient)에 Main Thread와 Worker Thread(1116996)이 존재하는 걸 볼 수 있다.
ThreadLocal을 사용하지 않는다면 여러 프로세스들이 생성하는 작업자 쓰레드가 CurrentBuffer를 공유 가능하다. (Session은 ServerCore에 위치함)


_buffer에 4096 * 100 만큼 (ChunkSize) 공간을 할당하고 4096을 예약(reserveSize)한 것을 볼 수 있다.


패킷을 한번만 썼을때 (Write) 패킷의 사이즈 카운트를 끝내고 Close할때 offset이 0인 것을 볼 수 있다.

이렇게 Write() 한 후 반환한 ArraySegment를 Session.Send() 함수의 매개변수로 전달하면 SendAsync를 통해 큐에 넣어뒀던 buff들을 한번에 소켓을 통해 전송할 수 있다.

Server

Server는 Listener를 사용하며 초기화할때 ClientSession을 생성해서 팩토리에 등록하도록 했다.
소켓을 통해 클라이언트와 연결이 되었을 때 ClientSession을 생성하고, 클라이언트에게 전송받기 위해 RegisterRecv() 함수를 호출한다. RecvBuffer에서 쓸 수 있는 공간을 ArraySegment로 긁어와서 받을 버퍼로 등록을 해준다. 그리고 비동기적으로 메시지를 받고 OnRecvCompleted에서 처리를 해준다음 RegisterRecv()를 다시 호출하는 것을 반복한다.

클라이언트로 부터 패킷이 도착하면 먼저 OnRecv()가 호출될 것이고 올바르게 받았는지 패킷 사이즈를 확인한다. 패킷이 잘 도착했으면 PacketManager의 OnRecvPacket()으로 buffer를 넘겨준다.


Server의 Main에서 PacketManager의 Register() 함수를 호출했기 때문에 Dictionary에 이미 PacketID에 따라 어떤 함수를 Invoke해줄지 정해두었기 때문에 TryGetValue로 id가 존재하는지 확인하고 MakePacket() 함수를 호출한다.
호출하면 Read() 함수를 통해 직렬화시킨 정보를 정해진 길이만큼 끊어서 정해진 타입으로 변환시키고 C_PlayerInfoReq타입의 pkt(패킷)을 생성한다. 이렇게 클라이언트에서 정의한 플레이어 정보와 스킬 등을 담은 패킷을 받아서 재조립 시킨다음 C_PlayerInfoReqHandler함수를 통해 출력시킨다.

profile
개인 공부 정리

0개의 댓글