Network Library - LanServer,LanClient

2022. 3. 3. 23:32게임서버/namespace univ_dev

320x100
안녕하세요 대학생 개발자입니다.

마지막 글을 올리고 나서 아주 오랜 시간이 흘렀습니다.

사실 이 테스트를 올릴까 말까 고민을 했었는데 말입니다...

이 내용은 새로운 지식이라기보다는 제가 만든 서버에 대한 구현만 있기 때문에 따로 올릴까 말까 고민했었는데 말입니다...

그래도 일단은 쓰기로 했습니다...

이번 글은 그냥 저 혼자 끄적이는 느낌이 강하고 따로 새로운 지식이 들어가는 부분도 아니기 때문에 크게 고민 없이 술술 읽으셔도 될 것 같습니다.

그리고 가장 중요한 점... 크게 유익 한글은 아닐 겁니다.

 

직전 글에서는

 IOCP에 대한 주의사항과 문제가 될 수 있는 점을 정리했었습니다.

그리고 한동안 그를 바탕으로 주의하며 에코 더미 테스트를 했었습니다.

추가로 서버와 서버 간 통신을 하기 위한 서버 간 통신 네트워크 라이브러리를 만들었습니다.

 

그리고 문제없는 코드를 만들기 위해서 수많은 테스트를 했었습니다.

그럼 시작하도록 하겠습니다.

 

 

에코 서버란 + 네트워크 라이브러리는...

우선 제 글을 이까지 쭉 읽어오셨던 분이라면 에코 서버를 모르시는 분은 없으실 거라고 생각합니다.

받은 데이터를 그대로 받은 클라이언트에게 돌려주는 서버 그게 에코 서버죠.

 

물론 원래 에코 서버라고 하면 받았던 데이터를 그 어떤 가공도 하지 않고 받은 버퍼를 그대로 꽂아서 돌려준다는 의미를 가지고 있습니다만...

 

에코 서버의 역할을 하는 클래스는 진짜 컨텐츠 쪽의 역할을 하게 될 것이고... 네트워크 라이브러리는 패킷 하나 단위로 뜯는 작업을 할 것이고... 네트워크 라이브러리에서 실제로 에코 서버로 올라가는 내용은 네트워크 라이브러리의 헤더를 제외한 퓨어 데이터만이 올라가게 될 겁니다.

 

과정은 이렇게 진행이 될 것 같습니다.

직렬화 패킷에 옮겨 담은 뒤 패킷이 도착했을 때 호출되는 OnRecv라는 라이브러리 내부 함수를 통해서 컨텐츠 영역으로 받은 패킷을 올려주게 될 겁니다.

그리고 WSASend호출을 하는 함수도 API로 제공되는 SendPacket이라는 함수를 통해 호출될 겁니다.

컨텐츠 영역에서 Packet이라는 구조체만 가지고 있다면 그냥 가상 함수 오버 라이딩만을 통해서 디커플링을 하게 될 겁니다.

 

즉... 쓸데없어 보이지만 온 패킷을 직렬화 버퍼에 담고 그 직렬화 버퍼를 컨텐츠 쪽으로 올린 뒤, 그 직렬화 버퍼를 마샬링 하고 그 값을 다시 새로운 패킷에 직렬화 시켜서 그 패킷을 send 하는 겁니다.

 

이유는 같은 L7상의 코드지만 네트워크 부분과 컨텐츠 부분을 분리하기 위함입니다.

L6레이어의 구현의 목적 정도로 봐주시면 될 것 같습니다.

이전의 MMO버전의 TCP fighter서버와 같은 경우에는 네트워크를 처리하는 부분과 게임 로직을 처리하는 부분이 하나로 묶여있었다면 이제는 네트워크를 처리하는 라이브러리는 더 이상 서버와 관련이 없는 게 되는 겁니다.

 

그리고 게임 서버는 소켓이나... 그 외 네트워크 레이어의 것들을 전혀 몰라도 상관이 없습니다.

그저 아래에서 알려준 SessionID가 하나의 연결 단위가 될 겁니다.

L4에서 소켓을 올려주는 것처럼 말이죠.

 

 

그럼 진짜 시작하겠습니다.

헤더는 단순하게 그냥 에코 한 바이트수가 담겨있는 unsigned 2바이트 값입니다.

여기서 에코는 당연히 네트워크 라이브러리가 아닌 실제 에코 서버가 받는 2바이트 제외한 데이터가 되겠습니다.

물론 실제 에코가 작동하는 에코 서버 클래스의 영역까지 2바이트는 올라가지 않을 겁니다.

 

물론 추후에 유저와 서버 간의 서버 클라 구조라면 메시지 헤더의 크기는 더 증가할 겁니다.

당장 생각나는 건 제가 항상 만들었던 헤더의 방식처럼 헤더의 코드와 그리고 데이터의 길이 그리고 간단한 체크섬 정도가 들어갈 것 같은 느낌이 드네요...

뭐 추후에는 대략 4바이트 5바이트 정도의 헤더가 나올 것 같습니다.

 

물론 서버와 서버와의 통신에서 사용되는 프로토콜 헤더는 length를 가지는 2바이트가 전부다 일 것입니다.

신뢰할 수 있는 네트워크 환경이고 절대로 타인의 조작 가능성이 없는 사설망 환경이기 때문입니다.

 

그리고 위의 바이트 수 같은 내용은 중요하지 않습니다.

중요한 건 컨텐츠와 네트워크 부분을 분리해주는 다른 레이어가 하나 추가된다라는 점 정도만 알아두시면 될 것 같습니다.

 

 

class CLanServer
{
private:
    struct LANPacketHeader;
public:
    ~CLanServer();
    CLanServer(...);
public:
    void SendPacket(...);
    bool GetNetInitializeFlag();
    DWORD GetCoreErrorCode();
    DWORD GetLastErrorCode();
    void DisconnectSession(...);
    void Run();

private:
    void RecvPost(...);
    void SendPost(...);
    void Release(...);
    virtual void OnRecv(...) = 0;
    virtual void OnErrorOccured(...) = 0;
    virtual bool OnConnectionRequest(...) = 0;
    virtual void OnClientJoin(...) = 0;
    virtual void OnClientLeave(...) = 0; // Release후 호출
};

위 코드는 서버 간의 연결 시에도 당연히 서버와 클라이언트가 구분이 되어야 되겠습니다.

그리고 이 코드는 그중 서버 코드가 되겠습니다.

 

이름은 좀 있어 보이길래 어느 정도 프라우드넷에서 따왔습니다...

그리고 멤버 변수들과 헬퍼 함수들, 디버깅을 위한 변수들은 다 빼놓고 적었습니다.

 

LANPacketHeader 구조체의 경우에는 2바이트의 length값만 담은 구조체입니다.

이전부터 계속 말씀드렸듯 TCP는 바이트 스트림이라 뭉쳐서 오거나 찢어져서 올 확률이 있기 때문에 하나의 데이터 단위당 크기가 필요했습니다.

 

사실 제가 만든 에코 서버는 저기 상속받아서 가상 함수만 만들어주면 됐기 때문에 사실상 네트워크 라이브러리만 도는 코드라고 보셔도 됩니다.

 

나중에 게임 서버에서는 따로 로직이 도는 스레드와 워커 스레드들로 나눠지겠습니다.

 

그리고 저는 간단히 멤버 함수에 대한 정리를 해드릴 테니 나중에 제 글을 보고 누군가가 서버 개발의 꿈을 꾼다면 보고 만드시면 도움이 될 것 같습니다.

 

우선 퍼블릭 영역에 있는 함수들은 라이브러리에서 호출하는 함수가 아닙니다.

SendPacket은 말 그대로 외부에서 패킷을 전달해주면 우리는 그 패킷을 전달받아서 전송을 보장해주는 역할을 하면 되겠습니다.

이때까지 정리했던 내용을 바탕으로 얘기를 하자면 WSABUF에 데이터가 들어있는 곳의 주소를 전달하시면 됩니다.

당연히 WSASend와 WSARecv는 함수를 호출한 함수가 리턴된 뒤에도 살아있는 메모리를 전달해야 될 겁니다.

 

Run함수는 메인 스레드가 멈추지 않게 하기 위한 함수입니다.

이전 글에서 말씀드렸던 이벤트 같은걸 이용해서 블락 걸어놓으시면 됩니다.

 

RecvPost, SendPost함수는 실질적인 WSASend, Recv함수 호출하는 부분입니다.

우리는 원할 때마다 Send를 하고 싶기 때문에 SendPacket이라는 함수에서 실질적인 WSASend함수를 호출하는 부분까지 쭉 이어져 있습니다.

하지만 WSASend 자체의 호출 속도는 매우 느리므로 우리는 모든 SendPacket마다 이 함수들을 호출하지 않습니다.

SendPacket함수에서 링 버퍼에 큐잉을 해놓고 단 하나의 스레드만이 저 자리에 접근해서 Send를 처리할 겁니다.

그리고 Send가 완료 통지가 떨어지기 전까지는 다른 스레드들이 Send를 할 수 없게 막을 겁니다.

주의할 점은 단 하나의 스레드만 접근한다는 것은 블락에 걸리는 것은 아닙니다.

 

Recv건은 의외로 간단하게 되어있습니다.

Recv는 동시에 2개의 스레드가 애초에 접근할 수 없습니다.

왜냐면 Recv는 시작과 동시에 걸려있고 Recv완료 통지가 떨어지고 완료 작업을 완수했을 때 또다시 WSARecv를 호출할 것이기 때문입니다.

 

OnRecv함수는 패킷이 도착할 때마다 알려주는 함수이지 실제 WSARecv단위가 아닙니다.

TCP는 바이트 스트림이기 때문에 당연히 패킷 하나가 두 개로 짤려 올 수도 있고 두 개가 하나로 뭉쳐올 수도 있습니다.

그 점에 대비해서 우리는 링버퍼라는 것을 이용해서 추가적인 버퍼로 저장하고 그 버퍼를 찢어지거나 뭉쳐진 패킷을 처리했었습니다.

OnRecv는 하나의 패킷이 만들어질 때마다 컨텐츠 레이어 쪽으로 올려주는 함수입니다.

실제 컨텐츠 레이어에서는 OnRecv를 오버 라이딩해서 메시지가 올 때마다 무슨 행동을 할지 정해줄 수 있을 겁니다.

그렇기 때문에 실제 받은 수는 1회라고 할지라도 OnRecv함수는 10번 100번이 호출될 수 있다는 거죠. 

 

 

OnClientJoin 함수는 실질적인 세션을 생성하는 함수입니다.

그럼 우리는 하나의 클라이언트가 연결을 시도했다면 어떤 작업을 해야 되는가입니다.

당연히 그 세션을 가지고 당장 무언가를 할 수는 없을 겁니다.

아직 하나의 플레이어가 되기에는 정보가 많이 부족하기 때문입니다.

대부분의 게임은 로그인이라는 과정을 거쳐야 되고 로그인 패킷은 거의 connect요청과 동시에 도착합니다.

하지만 서버를 개발할 때는 정상적인 경우만을 생각하고 만들지 않을 겁니다.

대부분의 경우 비정상적인 유저 혹은 경쟁회사 등 다양한 곳에서부터 연결 요청을 하고 쓸데없는 패킷을 쏟아댈 겁니다.

혹은 연결만 해둔 채로 아무런 패킷을 보내지 않는 경우도 존재하겠죠.

우리는 이경우를 대비해서 OnClientJoin함수에서 이 세션에 대한 TimeOut을 걸어줘야 할 겁니다.

그리고 이걸 확인해주는 폴링을 하시던... 아니면 뭐 3초마다 한번 확인하던 스레드가 하나 나와야 되겠죠.

TimeOut시간은 적당히 잡으시면 될 겁니다.

 

 

OnConnectionRequest 함수는 실질적인 Session이 만들어지기 이전에 호출되는 함수입니다.

들어오면 들어온 거지 이런 함수가 왜 필요한 거냐라고 물어보실 수 있습니다.

이 부분은 많은 개발자들이 필요성을 못 느낄 수도 있습니다.

아까 말했던 것처럼 timeout과 같은 것들은 OnClientJoin함수에서 처리해버리면 되는 부분이기 때문입니다.

그리고 실질적으로 세션이 생성되기 전에는 할 수 있는 게 아무것도 없기 때문입니다.

그나마 세션이라도 있으면 세션을 통해서 타임아웃을 시작하거나 할 수 있지만

진짜 연결이 시도되었을 때는 무엇을 할 수 있을까라는 점입니다.

이유는 패치와 점검 같은 건에서 나올 수 있겠습니다.

실제 서버를 패치하고 난 뒤 라이브 서버에서 잘 적용이 되어있는지 확인을 꼭 해야겠죠.

당연히 유저의 입장은 차단하되 사내에서 테스트하는 사람들은 정상적으로 들어갈 수 있어야 할 겁니다.

즉 혹은 화이트 아이피로 특정 아이피 대역을 사용하는 유저에 대한 사용허가를 해주기 위해서 필요한 함수라고 보시면 될 것 같습니다.

뭐... OnConnectionRequest함수는 이 정도까지고 어떤 경우에는 차단하겠다 어떤 경우에는 허가하겠다는 컨텐츠 쪽의 로직이므로 여기서 논할부분은 아니라고 판단됩니다.

혹은 확실한 핵 사용자 및 많은 트래픽을 유발하는 클라이언트가 아닌 연결에 대한 소프트웨어적인 제재를 할 수 있겠죠.

후자는 당연히 방화벽단에서 차단을 해야겠지만 방화벽 차단을 하기 위한 작업이 되기 전까지는 소프트웨어적인 방어가 필요하기 때문에 이 기능이 추가되었다고 보시면 될 것 같습니다.

 

 

OnClientLeave 함수는 우리의 세션이 삭제된 이후에 호출해주는 함수입니다.

뭐... 세션이 삭제됨과 동시에 호출하는 겁니다.

그것 말고는 따로 설명드릴 게 없겠네요.

 

 

마무리입니다.

음... 랜 클라이언트도 대충 구조는 비슷합니다만 랜 클라이언트는 당연히 연결을 요청하는 쪽이고요.

연결을 요청하는 쪽이다 보니 랜 서버보다는 조금 심플한 구조로 설계되어있습니다.

그리고 함수 몇 개만 수정하면 되는 부분이다 보니 안 올렸습니다.

 

사실 세부 구현 부분이나 나머지 문제가 있었던 건수들은 대부분 이걸 만드는 과정에서 있었던 문제를 해결하기 위한 것들이었고...

이건 제가 아무리 글로 적어봐야 큰 의미가 없습니다.

스스로 겪으면서 해결해 나가야 진짜로 본인 실력이 되는 거니까요.

 

어... 그리고 이제부터는 글이 올라오는 빈도수가 확연히 줄어들 겁니다...

지금부터는 이론보다는 개발이 거의 주가 되는 시점이기 때문인데요...

뭐... 사실상 블로그에 글을 쓰는 일이 얼마 남지 않았다고 말씀드릴 수 있겠네요...(어서 취업하고 싶다...)

 

째뜬 오늘도 긴 글 읽어주신 분들 감사합니다.

다음 글부터는 아마 포트폴리오 위주로 올라올 것 같습니다.

그리고 텀이 아주 길어질 것 같구요...

그럼 안녕히 계세요

320x100

'게임서버 > namespace univ_dev' 카테고리의 다른 글

DumpClass  (0) 2022.03.28
LockFree  (0) 2022.03.22
MMO_TCPFighter  (1) 2022.01.17
Chatting Server  (0) 2021.12.30
Red-Black tree delete  (2) 2021.12.23