티스토리 뷰

개요


패킷을 보낼 때는 해당 패킷의 내용과 관계없이 send함수를 호출해서 패킷을 보내기만 하면 된다. 하지만 패킷을 받을 때는 상황이 좀 다르다. 받은 패킷이 어떤 패킷이냐에 따라 호출할 함수 또는 수행할 연산 등이 달라지므로, 그 때 그 때 적절한 동작을 수행할 수 있도록 만들어 주어야 한다.

고민을 하던 중 cocos2d-x 엔진의 KeyboardListener가 생각이 났다. 

KeyboardListener는 내 class의 instance를 등록시켜 놓으면 내 class에 있는 KeyboardListener함수로 callback이 되도록 구성되어있다.

덕분에 key event들을 한곳에서 처리하지 않고 필요한 객체에서 따로 처리할 수 있다.

SocketListener를 구현해 보자.

수신받은 패킷에서 꼭 필요한 부분(어떤 함수를 호출하는 패킷인지)을 분리해서 등록된 Listener함수로 넘겨주는 기능을 구현해보자.






Spider - SocketListener


네트워크 매니저인 Spider는 Listener들을 관리하기 위해 Node 리스트를 가진다.

* Node는 게임 내에서 사용되는 모든 오브젝트들의 최상위 클래스로 모든 오브젝트 클래스들은 이 Node를 상속받아야 한다.

WSAAsyncSelect로 FD_READ 이벤트를 받았다면 Spider::Broadcast()로 받은 패킷의 내용을 넘겨주며, Broadcast()는 패킷에서 필요한 부분을 분리해서 모든 Listener들에게 패킷을 알려준다.


SocketListener는 다음 같은 코드를 가능하게 해준다.


1
2
3
4
5
6
7
8
9
10
11
12
// 게임을 위해 임의로 만든 Tank class
void Tank::SocketListener(Packet packet){
switch(packet.func){
        case MOVE:
            // move에 적절한 대응.
            break;
        case HIT:
            // hit에 적절한 대응.
            break;
        }
}
 
cs


Node class를 상속받은 임의의 class가 자기자신을 Listener로 등록하고 상속받은 SocketListener함수를 통해 socket event를 받을 수 있도록 하기 위해서 Spider class에 다음 코드들을 추가한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Spider.h
 
class Spider{
private:
    std::list<Node*> _listeners;
 
public:
    void AddSocketListener(Node* listener);
    /** Select
    * 우리 엔진에서는 socket을 하나만 열고 사용한다는 가정을 하고있으므로,
    * 어떤 socket에서 발생한 이벤트인지는 구별하지 않는다.
    **/
    void Select(int);
    /** Recv
    * packet을 수신하여 모든 listener들에게 방송(broadcast) 한다.
    **/
    void Recv();
 
};
 
void Spider::AddSocketListener(Node* listener){
    _listeners.push_front(listener);
}
 
void Select(int msg){
    switch(msg){
        case FD_READ:
        Recv();
        break;
        case FD_CLOSE:
        break;
    }
}
 
void Spider::Recv(){
    int size = 0;
    Packet packet;
 
    size += recv(_socket, (char*)&packet, sizeof(packet), 0);
    // recv가 실패하면 종료.
 
    if(size < 0)
        return;
 
    // 바이트 순서를 바꿔준다.
    packet.func = ntohl(packet.func);
    packet.count = ntohl(packet.count);
 
    // broadcast to all listener
    for(std::list<Node*>::iterator it = _listeners.begin(); it != _listeners.end(); it++){
        (*it)->SocketListener(packet);
    }
}

cs


코드가 너무 길어져서 Recv()함수만 가지고 왔지만, udp통신을 위한 Recvfrom, SendTo 등 같은 원리로 동작하는 송수신 함수들이 있다.

Spider::Select는 누가 호출하는 것일까? 우리 엔진은 WSAAsyncSelect를 사용하기 때문에 윈도우 메세지 콜백함수로 이벤트가 발생한다.

이 이벤트를 받아서 사용할 것이다.


1
2
3
4
5
6
7
8
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam){
    switch(msg){
        case WM_SOCKET: // WM_USER+1
        Spider::Instance()->Select(WSAGETSELECTEVENT(lparam));
        break;
        // ...
    }
}
cs







마침


c++과 winAPI만으로 게임을 만들다가, 네트워크 동기화 기능을 넣고 싶어서 socket에 대해 공부를 해 보았다. 

생각보다 재미있어서 만들던 게임에 쉽게 적용할 수 있게 엔진 형태로 만들어보기로 했다. 엔진이라고 할 수 있을지도 의심스러운 모습이지만 오픈소스 게임 엔진인 cocos2d-x에서 스타일을 많이 가져와서 만들었고 나름 잘 사용하고 있는 엔진이다.


다음 글에서는 p2p게임에서 실제로 이 엔진을 사용해 위치와 속도 등을 udp로 동기화하는 예제 볼 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함