더북(TheBook)

이번에는 서버 쪽 코드를 살펴봅시다. 이 코드는 이해를 돕기 위해 일부 의사 코드로 작성되었습니다. 실제로 작동하는 코드는 설치된 프라우드넷의 샘플 폴더(ProudNet/Sample/Chat)에 있습니다.

class MyGameServer 
{
  CNetServer* m_netServer;
  CriticalSection m_critSec;
  map<HostID, shared_ptr<RemoteClient> >
      m_remoteClients;
 
  OnClientJoin(clientInfo)
  {
      CriticalSectionLock lock(m_critSec, true);
      shared_ptr<RemoteClient> newRemote =
          shared_ptr<RemoteClient>(new RemoteClient);
      fill_something(newRemote);
      m_remoteClients.Add(clientInfo->m_hostID,
          newRemote);
  }
 
  OnClientLeave(clientInfo)
  {
      CriticalSectionLock lock(m_critSec, true);
      m_remoteClients.Remove(clientInfo->m_hostID);
  }
 
  Init()
  {
      m_netServer->OnClientJoin = OnClientJoin;
      m_netServer->OnClientLeave = OnClientLeave;
  }
}

Tip

프라우드넷 설치 프로그램을 얻으려면 프라우드넷 웹 사이트(http://www.proudnet.com)에서 개인 사용자 혹은 평가판 사용 신청을 하면 됩니다.

 

MyGameServer 서버 메인 클래스를 만듭니다. 이 클래스 안에는 서버에 접속한 클라이언트들의 목록이 있습니다. 기본적으로 NetServer는 멀티 스레드로 작동합니다.

NetServer에서는 RMI나 이벤트 함수 호출이 여러 스레드에서 실행됩니다. 따라서 클라이언트 목록 변수와 기타 데이터를 보호하는 mutex 혹은 크리티컬 섹션 객체를 갖고 있어야 합니다.

OnClientJoin() 함수에서는 새 클라이언트가 들어옴을 압니다. 이때 클라이언트 목록 변수에 들어온 클라이언트를 가리키는 데이터를 추가합시다.

반대로 OnClientLeave() 함수에서는 나간 클라이언트에 대한 데이터를 삭제합니다. RMI 수신 및 이벤트 콜백은 서로 다른 스레드에서 호출됩니다. 따라서 위에서 선언한 뮤텍스를 잠가야 합니다.

NetServer는 멀티스레드로 작동하도록 기본 설정되어 있습니다. 그렇지만 원한다면 싱글스레드로 작동하도록 설정할 수도 있습니다. NetServer를 싱글스레드로 작동시키면 뮤텍스를 사용하지 않아도 됩니다.

NetServer가 멀티스레드로 작동하면 같은 클라이언트에서 수신은 서로 다른 스레드에서 호출되지 않게 제어합니다. 이 제어가 없다면 같은 클라이언트에서 수신이 서로 다른 스레드에서 동시에 실행될지도 모르는데, 당연히 불편합니다.

이번에는 클라이언트에서 서버와 연결 및 수신 처리를 알아봅시다.

1. NetClient.FrameMove()를 호출합시다. 이때 이벤트 및 수신 처리를 하게 됩니다. 서버에서는 이것을 호출할 필요가 없습니다. NetServer로 제공되는 스레드 풀(1장 참고)에서 호출되기 때문입니다.

2. 서버와 연결하는 과정이 성공하거나 실패하면 OnJoinServerComplete()가 호출됩니다. 이 함수를 구현합시다.

3. 서버와 연결이 중도 해제될 경우 OnLeaveServer() 콜백이 발생합니다. 역시 이 함수도 구현합시다.

다음은 이를 구현한 예시 코드입니다.

class MyGameClient 
{
  CNetClient* m_netClient;
 
  OnJoinServerComplete(info, replyFromServer)
  {
      if (info.type = = OK)
      {
          do_success();
      }
      else
      {
          do_failure();
      }
  }
 
  OnLeaveServer(info)
  {
      do_leave();
  }
 
  Init()
  {
      m_netClient->OnJoinServerComplete =
          OnJoinServerComplete;
      m_netClient->OnLeaveServer = OnLeaveServer;
  }
 
  MainLoop()
  {
      while (true)
      {
          m_netClient->FrameMove();
          update_scene();
          render_scene();
      }
  }
}

서버에서 메시지를 수신하면 일을 처리하는 코드는 다음과 같이 개발합니다.

class RemoteClient
{
  string m_name;
};
 
MyGameServer:public MyGameC2S::Stub
{
  MyGameS2C::Proxy m_s2cProxy;
 
  MyGameC2S::Stub::Chat(senderHostID, rmiContext, text) // [**]
  {
      CriticalSectionLock lock(m_critSec, true);
 
      // 송신자 정보 가져오기
      shared_ptr<RemoteClient> sender =
          m_remoteClients.find(senderHostID).second;
 
      // ➊ 수신자 목록 만들기
      vector<HostID> sendTo;
      for (auto r : m_remoteClients)
      {
          if (r.first != senderHostID)
              sendTo.push_back(r.first);
      }
 
      // ➋ 멀티캐스트!
      m_s2cProxy.ShowChat(&r[0], r.size(),
          sender->m_name,
          text);
  }
}

RMI를 이용해서 채팅 메시지를 받았습니다([**]). 채팅 메시지를 받았으니 채팅받을 클라이언트들을 모읍시다(). 이렇게 모은 후에는 클라이언트 목록을 RMI의 매개변수로 넣어서 송신합시다. 이렇게 하면 여러 클라이언트에 원격으로 함수가 호출됩니다. 즉, 멀티캐스트를 합니다(). 물론 이렇게 하지 않고 각 클라이언트 하나하나에 RMI를 호출해 주어도 됩니다. 하지만 처리하는 데 드는 연산량은 앞 방식이 훨씬 빠릅니다.

신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.