상세 컨텐츠

본문 제목

게임 gamedevforever님의 네트워크 게임 튜토리얼 관련 모음

프로그래밍 관련/네트워크, 통신

by AlrepondTech 2020. 9. 17. 05:33

본문

반응형

 

 

=================================

=================================

=================================

 

 

 

 

 

 

 

출처: http://www.gamedevforever.com , http://www.gamedevforever.com/39

 

 

 

네트워크 게임 튜터리얼 1 - 워밍업

 

Posted by Rhea Strike

안녕하세요? 요양과 잉여짓으로 바쁜 Rhea Strike 입니다.
정작 당분간 하지 않을려했던
잉여 프로그램들은 짜고 있지만 가장 맘먹은 동인지 원고와 짤 제작, 여친 만들기 등이 늦어져 무척 가슴 아픈 나날입니다.

다들 쟁쟁한 주제들과 현업에서 연구하신 소재들로 막강한 화력을 자랑하시지만
실력이 딸려 제가 준비한 것은 네트워크 게임을 만드는 튜터리얼 연재입니다.
이 연재를 시작하기전 무슨 주제로 어떻게 이어갈까 고민을 잠시 했는데요,
잠시 이 링크 http://rhea.pe.kr/493 를 읽어주시고 연재글을 봐주시면 감사하겠습니다.

연재의 초기는 기본적인 채팅으로 시작될 것입니다.
이는 네트워크 프로그래밍에 익숙하지 않는 분들과 있을지도 모를 학생들을 위한 것이기도 합니다만,
매 연재 속에 점점 단순 채팅 프로그램이 게임과 아키텍쳐로 점차 커져가며
퇴근길 지하철에서 전체 게임을 위해서 한번 곱씹어 생각해볼만한 꺼리를 던져드리는게 제 목적입니다.

자, 그럼 지루한 서론을 마치고 우선은 모두들 알고 계신 내용이라도 복습의 의미로
기초부터 시작해보지요. ...역시 지루하군요.

 

 

 

짤에 별다른 의미는 없습니다만 이런 여친을 찾고 있습니다. 도움을 바랍니다.


1. 윈속의 역사
이미 게임을 하나 운영하고 있는 곳이라면 많은 것이 갖춰져 있습니다.
개발자 이외에도 DB, 서버, 패치 시스템, 그리고 그것을 돌려주는 여러 DBA, SE분들이 있습니다.
그러나 아무 것도 없는 곳에서 새로 온라인/네트워크 게임을 만든다면 맨땅에 헤딩을 해야 합니다.

우선 기획서가 나오고 DB라던가 여러 가지 인프라들이 먼저 있어야할 것이지만,
가장 단순하게 윈도우 서버와 클라이언트부터 만든다고 가정합시다.
자, 그럼 서버는 어떻게 만들어야 할까요?
서버를 만들기 전에 서버와 클라이언트에는 어떤 "Winsock Model"을 사용할런지부터 결정해야 합니다.

원래 소켓은 Windows 운영체제가 아니라 BSD UNIX에서 먼저 만들어졌습니다.
그래서 초창기 윈도우 3.1 같은 경우에는 소켓(http://www.joinc.co.kr/modules/moniwiki/wiki.php/man/12/BSD%20socket)
이 아예 존재하지 않았습니다.

이는 단순 소켓 지원 여부를 떠나 당시 마이크로소프트(앞으로 MS)의 중요한 의사 표방이기도 합니다.
소켓은 TCP/IP, UDP를 위한 라이브러리이고 TCP/IP, UDP같은 프로토콜은 바로 인터넷의 기반이 되는 프로토콜들이죠. 모든 것을 집어삼킨 HTTP 역시 TCP/IP 80번 포트를 이용한 텍스트기반 프로토콜 아니겠습니까?

다시 말해, MS는 현재의 인터넷이 이리 사용될지 몰랐습니다, 무려 90년대 중반까지도요!
MS는 소켓과 TCP/IP가 아닌 자신들의 프로토콜로 MS-Network라는 현재의 인터넷과 비슷무리하지만 다른 개념을 만들고 싶었습니다.

그 망상은 Windows 2000 이 등장하면서 공식적으로 사라졌는데 \\로 접근하던 다른 PC접근과 네트워크 공유를 NetBIOS(NetBIEU) 설치 없이 TCP만 설치하면  되도록한 것이 그 증거(?)라고 볼수 있습니다. 정확하게 말하면 NetBIOS over TCP/IP 이란 개념인데 아뭏든 90년대에도 MS는 인터넷도, TCP도, BSD 소켓도 반대! 그냥 MS껄로 다 정ㅋ벅ㅋ할꺼야!!였었습니다.

그래서 Win 3.1에서 Netscape로 웹서핑을 하던 우리들은 Win 3.1에는 없는 TCP/IP 프로토콜을 쓰기 위해
트럼펫 윈속(http://www.trumpet.com.au/index.php/downloads.html) 을 따로 설치해 썼었었습니다.
진짜 일반 사용자가 쓸수 있는 MS표 소켓이 나오는 것은 Win95 때부터입니다.

추억 돋는 화면 하나 더! http://www.loblolly.net/~rddecker/helppages/win31trmwns.htm



....................




...........





.....


..


그러나 어쩌겠습니까?
모두가 알다시피 인터넷(다시 말해 TCP/IP과 그것을 쓰기 위한 BSD Sockets)이 대세가 되었는데요.


 

 

이 당시 빌횽의 마음...









할수 없이, MS는 BSD 소켓과 똑같은 윈도우용 소켓 라이브러리를 제작합니다.
BSD 소켓 4.3에 기반해 1993년 WinNT 3.51과 함께 Winsock 1.1을 공개합니다.
즉, Winsock은 WINdows + SOCKet의 약자지요.

 

 

정말로 이때 빌횽은 전세계 개발자들에게 이말을 들었습니다.




이 윈속 1.1은 놀랄만큼 BSD 소켓과 동일합니다.
고인이신 스티븐슨의 UNIX Network Programming에 있는 소스 대부분이 헤더 파일만 약간 바꾸면 그대로 돌아갈 정도입니다.

 

 

네트워크 게임계의 프로메테우스, 古 리차드 스티븐슨

이는 다이렉트X 초창기의 모습과 유사합니다. DOS 게임 개발자들에게 복잡한 Windows Procedure와 콜백을 잘 몰라도 DOS 게임을 그대로 포팅할수 있도록 한 배려와 비슷한 모습입니다.

하지만 이때까지도 MS는 TCP/IP의 가능성을 잘 몰랐습니다.
...그래서 Winsock 1.1을 사용하는 Windows 서버는 ....상당히 구렸습니다.
윈도우는 서버로는 절대 안된다는 말이 이때부터 흘러나왔죠.

그러나 1995년 WinNT 4.0의 등장과 함께 선보인 Winsock 2.0부터 상황은 달라지기 시작합니다.
아키텍쳐부터 싹 달라졌습니다.
1.0과의 호환성은 100% 유지하면서 Windows에 최적화된 진짜 Winsock이 등장한 것입니다.
(달라진 아키텍쳐로 "개인방화벽" 프로그램이 만들어질 수 있었습니다. 자세한건 "SPI"로 검색을 ^^)

 

 

오오미 윈속이랑께~!!

현재 우리가 주력으로 사용하는 Winsock은 16비트 모드가 제거되어 1996년에 발표된 2,2를 사용하고 있으며
Win8이 등장할때 Winsock에 Registered IO extensions 이 추가된다고 하는데 간만에 Winsock에 새로운 기능이 추가되는 것이라 기대가 큽니다.

2. 윈속 모델

 

 

모델의 좋은 사례, Winsock에게도 공식 모델이 필요합니다.


드디어 오늘 기초의 핵심인 윈속 모델 이야기를 하겠습니다.
안타깝게도 윈속이 버전업이 되었다고 해서 윈속의 성능이 공짜로 올라가는 것은 아닙니다.


네트워크 프로그래밍에서 첫쨰 난관은 언제 "1) 누가 서버에게 Connect를 해 올줄 모른다"는 점입니다. 그리고 이때 클라이언트가 Connect를 해오는 순간, 아주 빠르게 accpet()를 해줘야 합니다. 접속이 수천개가 되면 이 작업 역시 딜레이가 걸리고 컨텍스트 스위칭이 걸립니다.
그런 다음, 일단 양쪽이 연결이 되면 "2) 서버든 클라든 언제 상대방의 메시지를 듣기 위해 recv()를 해야할지 모른다"는 점입니다(아시겠지만 연결을 끊는 close() 역시 사실은 상대방에게는 recv()입니다.)

 

 

언제 이벤트가 오는지 모르는 싯점을 체크해봤습니다.

이미지 출처 : http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=%2Frzab6%2Frzab6connectionor.htm

사실 게임 클라이언트처럼 소켓 5개 정도에서는 큰 차이가 없습니다만,
서버는 한 프로세스가 수백~수천개의 접속을 관리해야 합니다.
이를 위해 단일 thread를 돌리는 방법으로는 고작 수십명이면 금방 한계에 찹니다.
아무리 서버장비가 좋아도 똑같습니다.
왜 단일 thread는 안될까요?
thread라는게 마법은 아닙니다. 단순히 CPU 한계치까지 일을 시킬 뿐, 계속 루프를 돌며 찌질하게 검사하는 역활 밖에 할 수 없습니다.

좋은 서버란 가장 적은 thread와 CPU를 사용해 가장 많은 접속을 빠르게 처리해줄수 있어야 하며 이를 위해 두가지 개념을 먼저 알아야 합니다.

우선 비동기(asynchronous )라는 개념이 들어갑니다.
쉽게 말해 계속 루프를 돌며 검사하는게 아니라 클라이언트에게 접속요청이 왔거나 메시지가 왔을때에만
해당 소켓에게 알맞은 작업을 해주는 것입니다.

그래서 비동기 Winsock을 위해 4가지 모델이 고려되었습니다.







 

 

이중 알맞은 Winsock Model 네명을 고르시오.






...는 아니고...





 모델명 특징 기타
 Window Message Select
      (WSAAsyncSelect)
소켓 이벤트를 윈도우 메시지를 이용해 알려준다.  윈도우 메시지를 이용하므로 반드시 윈도우가 생성되어야 하며 MFC의 CSocket 클래스가 바로 이 모델.
 Kernel Event Select
     (WSAEventSelect)
소켓 이벤트를 커널 이벤트로 알려준다.  한 객체당 최대 64개의 이벤트 밖에 못쓰므로 서버로 쓰기 위해서는 코드가 쓸때없이 복잡해짐.
 Overlapped I/O
    (Overlapped)
소켓 이벤트를 비동기 읽기/쓰기를 위한 오버랩 구조체를 이용해 알려준다. 위의 둘과 달리 Overlapped와 IOCP는 Win95, 98에서는 안됩니다.

이 기법은 파일에 비동기로 읽기쓰기를 같이 하기 위함이었으나 소켓이든 파일이든 둘다 같은 HANDLE 이잖아요? ^^

역시 MS의 꽁수의 결정체라 불릴만 합니다.
 Overlapped I/O + IOCP
    (IOCP)
위의 방식에 더해 커널 큐잉을 통해 소켓 이벤트를 통보받는다. 사실상 윈됴에서 서버는 이넘으로 정해져있다. C#에서 비동기 선언해도 IOCP가 만들어진다.

결론은 서버는 IOCP가 정답입니다!

...이런 모델들입니다.


두번째는 넌블로킹(non blocking)개념입니다.
단일 thread가 아닌 비동기 서버를 짰다고 합시다.
이때 어떤 이벤트가 일어나면 비동기로 OS가 서버에게 알려줍니다.
그러나 send(), recv() 같은 함수는 블로킹(blocking)함수 입니다.
send(), recv() 작업이 끝날 때까지 서버 프로그램은 다음 일을 할수 없습니다.
이때 WSASend(), WSARecv같은 넌블로킹 함수로 대체한다면, 작업이 끝나기 전에 서버는 다음 작업으로 진행될 수 있습니다.

아마 넌블러킹 함수는 네트워크 프로그래밍에서 처음 보신 분들도 있을 것이지만,
수많은 I/O관련 함수들의 고급은 전부 넌블러킹으로 흘러가게 됩니다.
넌블러킹 함수는 어쩔수 없이 Callback 함수가 반드시 뒤따라갑니다.
그래서 처음 접하면 조금 어렵습니다............................................가 아니라 실은 많이 어렵습니다,
거기에 Overlapped나 IOCP같은 비동기 통보까지 더해지면 아주 복잡해집니다.

다행히 클라이언트에서 주로 쓰이는 WSAAsyncSelect 모델과 WSAEventSelect 모델에서는 넌블로킹이 그리 중요한 개념은 아닙니다. 유선 인터넷이 하도 빨라서요... 
클라이언트에서 넌블로킹을 잘못 쓰면 소켓 응답 오기 전까지 게임 프로그램 자체가 위험해질수도 있고
그 사이 화면 UI처리같은 다른 할일들도 신경을 써야 합니다(<-이거 은근 고급 토픽입니다.).

그렇다고 클라이언트=블로킹이 절대 아닙니다.
최근 스마트 디바이스에서 SNS클라이언트와 SNG와 같은 게임에서는 불안한 3G 네트워크 때문에
요즘은 클라이언트도 비동기 + 넌블로킹이 대세입니다.

3G가 워낙 느리고 자주 끊기니, 일단 클라이언트에서는 send()한 것으로 치고
유저에게 다음 작업을 할수 있도록 해주는 것입니다.
혹시 안된다면 나중에 에러 메시지가 뜨며 다시 하라고 알려줍니다.

SNG를 안하시는 분들도 트위터같은 앱에서 자주 보셨을 것입니다.
재미있게도 이런 처리 방식은 트위터 웹페이지에서도 확인할 수 있습니다.
물론 저도(사실은 밑에 팀원 갈궈서 하게 했습니다... >_<;;; ) SNS 만들때 이렇게 했고요.

 

 

만약 단순 블로킹이라면 이런 상황 자체를 볼수 없을 것입니다.


이는 We Rule같은 게임이 네트워크가 불안정할때 쉽게 확인될수 있습니다. 단순해 보이는 SNG도 3G라는 현실로 제법 복잡한 네트워크 구현 기술을 알아야 합니다.

...이슬이...드립 치고 싶습니다.



3. 오늘의 소스
오늘의 소스는 간단한 클라이언트/서버 소스입니다.
클라이언트는 MFC로 만들었고 Winsock Model 역시 CSocket, 즉 윈도우 메시지 방식입니다.
서버는 boost::asio로 짰습니다. 기본 TCP 서버를 그대로 썼습니다.
단, 한 파일로 설명된 소스를 보기 좋게 클래스별로 나눠놨습니다.
이 정도면 네트워크 프로그래밍에 익숙하지 못한 초보분이라도 약간의 노력이면 다음 시간부터 따라오실수 있을 것입니다.

이 간단한 프로그램들로 네트워크 게임을 위한 첫 삽질이 시작될 것입니다.

 

1.zip
0.12MB


4. 더 생각해볼꺼리

1) 심심하고 정말정말 할일이 없는 날, 단일 thread 방식의 채팅 서버와 윈도우 메시지 방식의 채팅 서버를 만들어 최대 몇개의 클라이언트에게 원할하게 메시지를 돌려주는지 직접 재어보세요.
2) 클라이언트가 블로킹으로 작동되어야할 경우와 넌블로킹으로 작동되어야할 경우를 생각해보세요.
3) 클라이언트가 블로킹과 넌블로킹, 모두가 동시에 필요하다면 소켓 라이브러리를 어떻게 만들어야할까요?
   (힌트 : MFC에서 CSocket 와 부모 클래스인 CAsyncsocket를 살펴보세요.)
4) 실제 게임회사의 면접에서 IOCP의 동작 메커니즘을 구두로 답변하기는 무척 어렵습니다.
    그래서 저는 이렇게 묻습니다, 
    "왜 IOCP 소스에는 recv() 함수가 두번 적혀있나요?"
    여러분은 머라고 대답하시겠습니까?



내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/ )를 명시해주세요.

 

--------------------------------------------------------------------------------------------------------------------------------------------

 

고생 하셨습니다.

에... 모델로는 상단의 왼쪽 두번째에 네표 올인!

스티븐스 아저씨 정말 대단하신 분이시지요
그리고, 소스북 만들어서 팔겠다고 사기치고 가셨지요
10년이 넘게 기달렸는데...

네트워크 프로그래밍에서 손뗀지 오래 되었지만
오랫만에 보니 추억 돋네요

답을 달아야 할 거 같은...

1. 단순 쓰레드 라는 말의 의미를 이해 못했어영, 윈도우의 비선점형 특성 때문에 발생하는 문제를 말하는 건가요?

접속이 되면 쓰레드를 생성해서 자식 쓰레드에게 처리를 맏기고 부모 쓰레드는 다시 접속을 기다리면 되는 상황이라면 - accept() 다음에 쓰레드 생성해서 자식 쓰레드가 select() 이후를 처리하게 하고 다시 accept()로 돌아가면 되는 상황이라면 블럭 방식이라고 해도 수백개 정도는 처리 되지 않나요???
제가 Unix 소켓에만 익숙해서... 윈도 쪽은... 헤헷...

응? 그러고 보니... 쓰레드를 쓰는데 저기에 왜 select()가 들어가 있지요? 그냥 차일드 쓰레드에게 파일 디스크립터 넘기면 되는거 아닌가요???

accept를 블러킹 방식으로 쓰는데... 왜 select()가 필요한 거지요??? 손뗀지 오래되서 완전... 혼란의 연속이네요 ㅡ,.ㅡa

아... 혹시... 단순 쓰레드 = 단일 쓰레드 인건가요?

2. 이전 데이터를 받아서 다음 데이터를 처리해야 하는 경우에는 블러킹 방식을, 이전 데이터와 상관없이 데이터 처리가 되는 경우에는 넌 블러킹 방식을 씁니다.

에... 암호통신을 하는 경우에 상대방이 보낸 시드를 이용해서 보내는 메시지를 암호화 해서 보내는 경우가 블러킹 통신의 예가 될 수 있겠네요.

비 동기 통신은 웹 브라우저에서 쓰지요, 화면에 표시되는 각각을 따로 요청-수신 해서 화면에 뿌리지용

3. 에... ... ... ....
모르겠습니다. 고민을 많~~~이~~~ 해봐야 겠네요

4. 선빵을 날렸으면 답례로 한방 맞아 줘야지요
ㅋㅋㅋ

  • Rhea君 2012.01.04 10:48    
  • 아, 일단 IOCP처럼 thread에 뭔가를 하지 않았고(?),
    쓰레드 하나만 돌린다는 이중적 의미였습니다만,
    (말씀하신 두가지 다 맞는 상황입니다.)
    단일 쓰레드라는게 문맥에 더 잘 맞겠습니다.
    수정하였습니다.

    3에 대해서는 실제 소켓 옵션을 바꿀 것인지 안바꿀 것인지에 대한 논의입니다. ^^

    답변감사합니다.
  • 죠쉬 2012.01.04 10:54    
  • 옷! 바람같은 댓글!
    감사합니닷!

 

  1. 좋은 글 감사합니다~ ^^
    네트워크 플밍 안한지가 너무 오래되어서 머릿속에 띄엄띄엄 기억만 남아있는데... 쉽게 잘 써주셔서 많은 도움이 될 것 같습니다.
    스티븐스 책사진을 보니... 소식대롱, 알맹이, 껍데기 그런단어가 생각나네요. ㅎㅎ
    • Rhea君 2012.01.04 11:38    
    • Advanced Programming in the Unix Environment...그 책이졐ㅋㅋㅋㅋ
      참 여러가지 생각하게 하는 번역떡밥이죠.
      그게 또 그것을 좋아하시는 분들도 많았드랬죠.

      아 정말이지 스티븐슨 아저씨가 살아계셨다면 꼬옥 만나뵙고 싶은 분입니다.

      학계의 높은 분들만 갖고 놀수 있는 소켓과 TCP/IP를
      일반 개발자들에게 알려주신 프로메테우스시죠...
  2. 죠쉬 2012.01.04 10:49      
    • Rhea君 2012.01.04 11:42    
    • 네, BSD Sockets기준으로 말씀하신 함수가 맞습니다.
      그런데 문제는 이 함수를 통하면 전체 I/O 방식이 바뀐다는 점입니다.
      게임 클라이언트에서 블로킹과 넌블로킹을 동시에 쓴다면
      매번 이를 바꾸는 것이 위험하리라 예상됩니다.

      그럴 경우, 어떤 방법이 클라이언트 컨텐츠 개발자에게 안전하고 투명성이 제공될 것인가...그런 논의죠.

      MFC의 사례를 보면,
      넌블로킹으로 만들어진 CAsyncsocket의 상속을 받는
      CSocket은 블로킹 소켓 클래스입니다.

      어떻게 이렇게 되었을까요?
      그리고 이 방식의 장점과 단점은 어떤 것이 있을까요?

      ...에 대한 토론을 하고 싶었습니다. ^^

      PS) 의외로 엄청 간단하게 처리되어 있습니다.
  3. 훔... MS 윈도우는 조금 다른지 모르겠습니다만

    send(), recv() 혹은 write(), read() 함수도 사용되는 파일디스크립터를 fcntl()이나 ioctl() 함수를 이용해서 넌 블러킹 방식으로 바꾸면 넌 블러킹 방식으로 동작하는 걸로 기억합니다.

    설마, 시스템 레벨의 함수가 10년전과 지금은 다르다거나 하지는 않겠지요?

    추: 다시 생각해 보니, 설명을 짧게 하기 위해 일부러 생략하신 것 이겠네요, 사진에 있는 스티븐스 아저씨의 책에 있는 내용이니까요.

 

 

 

  1. 잘 봤습니다. 윈속 역사쪽은 전혀 모르던 내용들인데 재미있네요 ㅋㅋ 다음 번 글에서도 이런 류의 사이드 지식들도 간간히 적어주시면 좋을 것 같아요~

    2, 3번에 대해서 약간 딴 소리지만, 나중을 생각할 때 엔간하면 그냥 네트워크는 전부 비동기로 처리하되, 필요한 경우 별도의 논리적인 레이어에서 순서를 보장하는 식의 처리를 하는게 적당할 것 같습니다.

    요즘 멀티 쓰레딩 동네에서 작업을 배분하는 추세가 subsystem 단위에서 task 단위로 가는 경향이 있는데, 여기에선 하나의 작업이라도 블러킹 될 수 있다면 프레임워크 단에서 그걸 커버하기 위해 치러야 하는 비용이 작지 않으니...

    다른 영역들을 봐도 scalability쪽 이슈가 대두되는 웹쪽 프레임워크나 완전히 새롭게 구조를 일신한 WinRT 등에서는 아예 모든 I/O를 비동기로만 다루게 하고 있더군요. (심지어는 파일까지도!) 이런 부분에 대해서도 고민을 해보면 좋을 것 같습니다.

    다만 코루틴이 없는 C++의 경우는 콜백 천국이 되어 버리는 문제가 있긴 할 듯 합니다. 사실 제일 좋은건 C++ 언어 차원에서 코루틴을 지원해서 비동기 작업을 완벽하게 래핑해주는건데 표준 위원회 아저씨들은 이 쪽엔 별 관심이 없는 듯 orz
    • Rhea君 2012.01.05 12:51    
    • 네 그것도 좋은 방법입니다.
      예컨데 CSocket은 비동기소켓인 CAsyncsocket에게 상속받지만,
      소켓 옵션이 아닌 Send(), Receive() 함수에서 메시지처리를 따로하여 동기소켓을 흉내냅니다.

      이와 같은 처리를 통해 말씀하신대로 상황에 맞는 처리를 해주는게 좋겠지요.

      마지막 콜백문제에 대해서는 정말 간절한데 콜백함수 자체가 운영체제 의존성이 깊은 것이라,
      과연 표준안에 쉽게 적용되긴 힘들 것 같습니다. ㅠㅠ

      PS) 역사...같은 것은 늙덕의 증거죠, 그런거 제가 겪은 세대의 이야기가 아니었으면 좋겠습니다. ㅠㅠ
  2. 크로스 2012.01.04 16:34      
    • Rhea君 2012.01.05 12:38    
    • 먼저 크로스 님이 적으신
      http://bluekms21.blog.me/10126685276 에 기대가 큽니다.

      만드시는 졸작을 연재하시면 제 강좌보다 훨씬 나을 것 같습니다. 레알임.

      답변은...
      1) 공식적으로 정리할 생각은 없습니다.
      리플에서 토론하는 정도면 충분하다고 생각되며
      특별한 것이 있으면 제 블로그에 따로 한 꼭지 적겠습니다.
      2) 네, 됩니다.
      3) 네, 정확합니다.

      그런데 아래 댓글에 39가 무슨 의미인가요?
      (72면 몰라도...)
  3. 오옷 +ㅂ+
    모든 고급 토픽들은 렌더링 위주네.. 라는 저의 불만을 단 한번에 깨뜨려주는 멋진 서버 프로그래머용 글이네요!
    매우 감동하며 잘 읽었습니다.

    1) 더 생각해 볼 거리에 대해서는 혹시 다음 강에 답을 제시해 주시나요??

    2) 또, 이것을 원문과 출처 그대로 제 블로그에 보존하고 싶은데 허락해 주실 수 있으신지요?

    3) 마지막으로 더 생각해 볼 거리 4번에 대해서는...
    부족한 학문입니다만 recv()를 마지막에 대주고 빠져나와 스레드가 IOCP로 돌아가 응답을 기다리도록 하여, 유휴 스레드가 비동기적으로 처리하도록 하기위해 라고 생각하는데.. 맞을까요.??

 

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.com/ ,http://www.gamedevforever.com/62

 

 

네트워크 게임 튜터리얼 2 - 아키텍트 돋는 첫걸음

 

본의 아니게 업계에 알려진 Rhea君의 건강은 지금은 꽤나 좋아졌습니다.
각종 디스크 상담이 쇄도하고 있습니다.
그러나 아직 아래 사진과 같은 선물을 줄 여자친구를 만들지 못했으므로 조금은 더 쉬어야합니다.

 

 

진정한 남자로 선택을 받는 시간이 한달도 채 남지 않았습니다.

많은 게임 업체분들이 백수인 저를 위해 사심없고 우정 어린, 밥과 술을 사주셨는데,
현재 삼성동 사옥 돋는 N모사만 밥과 술과 향응을 제공하지 않았습니다.
여전히 성장기라 배가 고픕니다. 참고로 저는 소고기보다 물고기류를 더 좋아합니다(삼성동 오징어세상에서 오징어 통찜이나.).
그리고 백수를 갈취한 선릉역 N모사와 오리역 N모사 직원느님을 고발합니다(...뻥).

1. 네트워크 아키텍쳐
익히 알려져 있는대로, 소켓 속에는 주요 함수가 몇개 없습니다.
클라이언트를 기준으로 하면 연결을 위한 connect(), 주고받는 send()/recv(), 연결해제를 위한 close()뿐입니다.
그러나 많은 게임들은 이 몇안되는 단순한 함수들로 클라이언트/서버(이하 C/S)과 Peer-To-Peer(이하 P2P)를 오가는
마법과 같은 네트워크들을 보여줍니다.

1.1 모든 것은 서버로 C/S
C/S 모델은 100% 서버 지향 네트워크 아키텍쳐입니다.
사용자는 게임 클라이언트 실행시 서버에 의해 게임 플레이가 결정됩니다.

가장 좋은 예는 MMORPG와 맞고/포커같은 카드류 게임입니다.
C/S 모델은 MMORPG가 발전한 우리 나라에서 크게 발전된 모델입니다(기술뿐만 아니라 운영도 포함해서).

C/S 모델의 장점은 보안에 강력하다는 점입니다.
모든 결정이 서버에 의해 판가름나므로 해킹에 강합니다.
만약 해킹을 통해 어떤 짓을 했다고 하더라도 다음날 아침이면 들통납니다.
대부분의 온라인 게임 회사들일 경우, 출근하자마자 볼수 있는 것이 어제 급격히 레벨이 오른 유저의 전적입니다.
당장 제재가 가해지지는 않지만 어김없이 들키고 맙니다.
...나머지는 서버개발자가 아닌, 이름은 친절하지만 사실은 무서운 아저씨들인 고객만족팀에서 해결해주시곘죠.

여기서 한가지 명확하게 짚고 넘어갈 것이 있습니다.

우선 "클라이언트/호스트"와 "클라이언트/서버"의 개념입니다.
우선 우리가 익숙한 PC통신이나 UNIX를 생각해봅시다.
PC통신화면이나 텔넷으로 접속한 UNIX 화면은 100% 서버에서 던져주는 TEXT정보입니다.
이 당시에는 "서버"라는 용어 대신 "호스트(host)"라는 용어를 썼으며 이때는 반드시 로그인(Log in)이라는 용어를 썼습니다.

이 의미는 클라이언트에는 0.1%의 할일도 없으며 모든 것을 서버에서 전해주는 것을 받아 그 안에서 작업한다는 것입니다.
돌이켜보면 PC통신에 사용된 프로그램을 가르켜 단말기 예뮬레이터(Terminal Emulator)라고 불렀습니다.
PC가 아닙니다. 단말기라는 것은 PC가 아니라 Host에 접속해서 Host가 보내주는 화면과 텍스트 기반의 요청을 보내는 것 뿐입니다.

따라서 클라이언트는 Host 속으로 들어간다는 의미에서 로그인(Log in)이라 불렀습니다.

 

 

저승 로긴하시는 마미성님


그리고 단말기는 사라지고 PC가 흔해진 시대에 들어서며 클라이언트/서버의 시대가 왔습니다.
클라이언트/서버의 의미는 클라이언트와 서버가 "평등"하다는 의미이며 하나의 작업을 처리하기 위해
클라이언트 컴퓨터와 서버 컴퓨터가 협력해서 작업을 한다는 의미입니다.

그래서 로그온(Log on)이라 부릅니다.

윈도우 역시 Log on/Log off 라는 표현을 씁니다.
이는 커널이라는 서버와 애플리케이션이라는 클라이언트가 협업한다는 의미입니다.
오래전 윈도우95가 등장할때, "본격 32비트 클라이언트/서버 OS"라고 했는데
일부 사람들은 윈도우95에 소켓이 내장되어 그렇게 부른다고 했지만 그게 아니라 커널과 애플리케이션이 통신한다는 의미입니다.

네트워크에서 클라이언트/서버 역시 마찬가지입니다.
웹페이지를 볼까요?
흔히보는 홈페이지 가입할때, 특정 항목을 제대로 적지 않았으면 다시 적으라고 알려줍니다.
이는 서버에서 검사할 수도 있고, 클라이언트에서 자바스크립트로 검사할 수도 있습니다.
어느 쪽이든 가능합니다.

하지만 서버 측에서 검사할려면 일단 데이터를 서버로 넘겨야 합니다.
트래픽 측면을 본다면 자바스크립트로 처리하는게 효율성에서 맞습니다.
그런데 또 문제가 있습니다.

비쥬얼 스튜디오같은 자바스크립트 디버깅 툴만 있으면 자바스크립트에서 처리하는 양식 검사 따위는 아주 쉽게 무시해 버릴수 있습니다!
이렇게 되면 효율성을 무시하고 서버에서 검사를 해야 합니다.
도대체 뭐가 맞는 것일까요?

홈페이지 이야기를 하니 헛갈린다면 게임으로 넘어옵시다.
C/S기반의 슈팅 게임을 있다고 할때 공격 성공 판정을 서버에서 해야할까요, 클라이언트에서 해야 할까요?
서버에서 하자니 매 키입력마다 네트워크를 타야 합니다.
클라이언트에서 하자니 검증이 불가능합니다.

정답은 그때그때 다르다이고 서버에서 처리할 것과 클라이언트에서 처리할 것을 알맞게 나눠야 한다는 것입니다.
앞으로 게임을 만들며 이 문제를 실전에서 다시 짚을 것입니다.

1.2 우리끼리 놀자 P2P
P2P는 유저들끼리 데이터를 주고 받으며 게임을 진행하는 방식입니다. P2P의 대표적인 게임이라면 스타크래프트와 FPS게임들일 것입니다.
그러나 MMORPG 위주의 우리 나라에서는 꽤나 오랜 시간 P2P 기술이 홀대를 받았습니다.
보안에 취약하고 지금처럼 FPS가 유행하기 전까지는 P2P에 어울리는 게임이 없었기 때문입니다.

P2P의 장점은 로비를 제외하면 대용량 서버를 구축할 일이 없다는 것입니다.
단점은 클라이언트가 결국 서버와 클라이언트, 모두를 맡아야 하므로 네트워크 측면에서 만들기가 좀더 어렵다는 점입니다.
특히 P2P는 (플레이어수-1)((플레이어수-1)-1) /2 만큼 연결을 가져야 하므로 사용자가 늘어날때마다 디버깅이 힘들어집니다.
그리고 절대 잊을수 없는 문제가 있지요, 바로 NAT(Network Address Translator)이라고 불리는 공유기 문제입니다.
공유기 아래에 있는 Peer는 서버로 작동할 수 없습니다. 그래서 홀펀칭(hole punching)이라는 방법이 거론됩니다.

그러나 현실적으로 P2P만으로 게임 서비스를 만들기는 불가능합니다.
친구들을 모으고 채팅을 하며 어떤 방이 있는지 목록을 보기 위해서는 반드시 중앙에 서버가 있어야 합니다.
스타크래프트로 치면 바로 배틀넷(battle.net)이죠.

구분 장단점 주 프로토콜 게임 스타일
C/S 장점 : 보안에 유리
단점 : 높은 트래픽, 대용량 서버 기술 필요
TCP가 주로 사용되나 UDP도 사용 온라인 게임
P2P 장점 : 대용량서버 필요없음
단점 : 복잡성 증가, NAT 홀펀칭 필요
서버연결은 TCP,
P2P에는 UDP가 주로 사용
네트워크 게임

<표1. C/S와 P2P 정리>

온라인 게임과 네트워크게임의 차이도 이번에 짚고 넘어가죠.
온라인 게임은 사용자의 대부분의 MMORPG처럼 접속이 없어도 서버 시간이 흐르는 게임을 의미합니다. 내가 서버에 접속하지 않아도 서버는 이벤트가 진행되고 밤낮이 바뀝니다. 네트워크 게임은 방을 만들어 진행하는 단판승부 위주의 게임을 의미합니다. 스타크래프트, 맞고 같은 게임이 대표적이죠.
이 강좌의 제목은 아무 생각없이 네트워크 게임 튜터리얼이라 지었는데, 어떤 예제가 나오더라도 그냥 무시해주시길 바랍니다. ㅠㅠ

2. 네트워크 아키텍쳐의 실전 사례 분석

주의!
이 포스트에 언급된 게임들은 반드시 그렇게 작동된다고 책임지지 않습니다.
특히 해당 IP와 Port로 접속을 거는 것도 결코 책임지지 않습니다.

오래 전부터 네트워크를 분석하는 툴은 많았습니다.
기본 프로그램인 netstat도 훌륭한 도구이며(그 유명한 TCP/IP Illustrated 도 netstat로 실습하고 있습니다.)
약간의 관심만 가진다면 어떤 유명 게임이라도 클라이언트 상에서 어떤 과정을 거치는지 쉽게 확인할수 있습니다.
악용하지 않는다면 정말 좋은 공부가 됩니다.

이런 도구들이 있는데 유명 게임들의 네트워크 아키텍쳐를 전혀 모른다는 말은 개발자로써 관심이 없다는 말일 겁니다.
가끔 면접 볼때 무슨 게임에 몇년을 투자했다는 열정에 대한 자랑을 듣지만,
그 게임의 라이팅이나 네트워크를 짐작도 못하고 있으면 공돌이로썬 안타깝게 느껴집니다.

 

 

진짜 울고 싶은건 질문하는 사람이라구!!


이 강좌에서는 비쥬얼스튜디오 이외에 앞으로 두가지 툴을 자주 쓰겠습니다.

1) TCPView
http://technet.microsoft.com/en-us/sysinternals/bb897437
오래전 MS에게 인수된 sysinternals의 TCPView입니다.
netstat과 거의 흡사하지만 GUI라서 보다 편리합니다.
예전에 날잡아 서버 개발자 PC를 검사하며 이것이 안 설치되어 있으면 갈구시던 어떤 실장님이 생각나네요(제가 아닙니다.).
그외 FileMon, RegMon도 클라이언트 플랫폼 개발자들에게는 필수 도구입니다.

2) WireShark
http://www.wireshark.org/
pcap 라이브러리를 사용하는 패킷 검사툴입니다.
사실 이것과 노가다만 있으면 어떤 게임이라도 프리서버를 만들어 내는 일은 가능합니다.
불가능 하다고요?
우리가 게임을 만들듯, 수백명이 한사무실에 모여 각자 팀별로 패킷만 분석한다면 어떨까요?
대륙의 판타지는 이렇게 만들어집니다.

2.1 C/S 모델
여기서 말하고픈 것은 C/S 아키텍쳐라도 절대로 서버 1대로만 접속하지 않는다는 점입니다.
MMORPG도 그 수많은 종류만큼 다양한 네트워크 아키텍쳐가 있습니다만, 간단한 카드류 게임으로 서버에 어떻게 접속하는지 보겠습니다.

 

 

로비에 접속

 

 

로비서버로 짐작되는 XXX.XXX.XXX.214에 접속

 

 

 

로비에서 게임룸으로 들어왔습니다.

 

 

 

게임서버로 짐작되는 XXX.XXX.XXX.172에 접속


이렇듯 C/S 모델이라도 최소한 두개의 서버를 번갈아 접속했다는 사실을 알수 있습니다.
그리고 TCP 이외에 UDP도 사용하고 있고요.
UDP는 연결지향이 아니니까 책에서 본대로 접속은 이뤄지지 않았습니다.

물론 서버는 이 두개 뿐만이 아닙니다. 
직접 해보시면 서버 주소가 다를 것이고 테스트해볼때마다 다를 수도 있습니다.
이 말은 실제 각 로비서버와 각 게임서버는 여러 대가 있다는 의미입니다.
그리고 다른 서버로 접속했다 하더라도 같은 방에 있어야 합니다.

이를 위해서는

1) 서버간 통신을 해주는 서버가 필요
2) 어떤 서버로 연결할 것인지 알려주는 서버가 필요

라는 결론이 나옵니다. 그 구조를 클라이언트에서 알수는 없습니다만, 아키텍쳐는 나온 것이죠.
이러한 유추를 통해 각기 다른 월드나 지역으로 구분하는데도 유용하게 사용할수 있습니다.

2.2 P2P 모델

P2P의 대표 모델인 스타크래프트 배틀넷을 분석해보죠.
스타크래프트의 P2P 모델은 DirectPlay의 P2P 모델과 일치합니다.
DirectPlay를 그대로 가져다 쓴 것이라 여겨집니다.

P2P에서는 크게 자신이 세션(방)을 만드는 것과 다른 사람이 만든 세션을 검색해서 들어가는 두가지로 나뉩니다.
각 단계에서 소켓에서는 어떤 함수가 불려지는지 정리해보지요.

1) 내가 방을 만들어 접속하는 경우
1-1) TCP 소켓 생성

 

 

Winsock은 TCP/IP 이외에 다른 프로토콜도 사용가능합니다. Battle.net을 선택하는 순간 TCP/IP를 위한 준비를 합니다.

pSocket->Create(TCP); 와 같은 형태로 함수가 호출될 겁니다.

1-2) Connect()

 

 

로긴

버전업 체크 서버와 버전을 확인을 한후 로긴을 합니다.
이때 pSocket->Connect(배틀넷, 스타크래프트_확장팩); 형식으로 접속을 하게 됩니다.

1-3) recv()

 

 

배틀넷 서버에 접속


배틀넷 서버에 접속했습니다.
recv() 함수가 작동되며 공지사항이 날라옵니다.

1-4) 로컬에 서버 소켓 생성

 

 

방을 만들면...

 

 

 

P2P를 위해 로컬에 서버가 만들어집니다.

세션이 만들어졌습니다.
이때가 무척 중요합니다.
내가 세션을 만드는 것도 중요하지만 다른 유저들이 내가 만든 방을 검색하기 위해서
배틀넷 서버에게 내 세션의 정보를 알려줘야 합니다.
가장 중요한 것은 "서버인 나의 IP"입니다.

1-5) 세션 호스트로써 게임 시작
게임을 시작합니다.
이때 배틀넷과 접속을 끊습니다.
P2P답게 다른 유저들과 접속이 이뤄지고 있을 뿐 배틀넷과의 접속은 의미가 없기 때문이죠.

 

 

UDP 막 날라다님


만약 세션 호스트(방장)이 게임에서 나가게 되면 세션 마이그레이션(session migration)이라는 과정을 거치며
P2P 세션 호스트의 자리를 다른 사용자에게 물려주게 됩니다.
안정적으로 이것을 수행하는 것이 꽤나 난이도 있는 일이며 홀펀칭과 함께 P2P 라이브러리가 해주는 중요한 역활입니다.
스타크래프트에서 일반 유저보다 방장이 나갈때 잠시 랙이 걸리는 이유입니다.

1-6) P2P close(), 배틀넷 connect()
방을 나간 사람은 P2P 세션에서 나가며 다시 배틀넷에 Connect를 하게 됩니다.
이때 이겼다, 졌다의 정보를 갖고 배틀넷에 send()를 합니다.
이때가 P2P가 보안에 취약할 때입니다. 실제 이겼는지 졌는지 배틀넷은 모릅니다.
져놓고서 이겼다는 패킷을 날려도 배틀넷은 알지 못합니다.

2) 다른 사람의 만든 방에 접속할 경우
2-1) 세션 목록 recv()
1-3) 까지는 똑같습니다.
가장 중요한 것은 배틀넷이 현재 접속을 요청하는 세션 목록을 나타내는 것입니다.

 

 

P2P 서버가 해주는 가장 중요한 일


2-2) 세션에 참여 P2P connect
배틀넷과 접속을 끊고 해당 세션 호스트의 IP로 접속을 하게 됩니다.

 

 

배틀넷과 접속을 끊고 P2P 세션에 connect()


2-3) 게임 시작
세션 호스트라는 것이 아니면 네트워크 상으로는 똑같습니다.

 

 

마땅한 짤이 없어...지만 이건 스타2인데...



2-4) P2P close(), 배틀넷 connect()
게임을 나가게 되면 P2P 세션을 닫고 배틀넷으로 접속합니다.
이 역시 1-6)과 같습니다.

3. 홀펀칭
P2P는 세션 마이그레이션 이외에도 NAT이 가장 골치꺼리입니다.
NAT 뒤에 있는 IP는 실제 IP가 아니기 때문에 밖에서 들어올 수 없는 것이죠.
공유기에 따라 특정 IP를 밖으로 노출 시키는 경우도 있고 아닌 경우도 있습니다.
어찌되었건 게임 서비스를 하는 입장에서는 NAT 특성에 관계없이 연결을 만들어줘야 합니다.

다행히도 이러한 홀펀칭에 대해서는 denoil님께서 멋지게 정리를 해주셔서 (http://gamedevforever.com/47)
해당 포스트를 참조하시면 될 것 같습니다. ^^

홀펀칭 역시 제 블로그(http://rhea.pe.kr/)에서도 2-1 편으로 다시 정리하겠습니다.
UDP 예제도 이번 포스트 소스로 넣어야겠군요...

자, 이번 포스트를 통해 소켓 사용법 이외에 네트워크 게임 개발자로써 고민해야할 1차적인 아키텍쳐를 이야기해봤습니다.
강조하고픈 것은 아키텍쳐에 게임을 맞추지 말고,
게임에 따라 유연한 아키텍쳐를 생각해내고 업데이트에 따라 유연하고 쉽게 바뀔 수 있는 자세를 가져야 한다는 것입니다.
좋아하는 게임이 있으면 어떤 네트워크 아키텍쳐를 갖고 있는지 최대한 유추해보시길 바랍니다.
그리고 PC게임 뿐만 아니라 스마트폰 게임이나 콘솔게임기를 추적해보는 것도 좋은 공부가 됩니다.

다음 시간에는 서버와 절대로 떼어놓을 수 없는 Database를 살펴보겠습니다.
아직도 게임을 만들 시기는 아닙니다.

4. 더 생각해볼꺼리
1) 오늘 예제로 등장한 C/S게임과 스타크래프트의 네트워크 연결 순서도를 그려보세요.
물론 소켓 함수도 함께 적어보세요.

2) 자세히 보시면 스타크래프트1의 배틀넷은 DirectDraw surface가 아니라 일반 Dialog입니다.
(커서가 윈도우 커서로 바뀌죠?)
저도 그랬지만 Surface 위에 Dialog 나 윈도우 stuff들을 올리는 것은 쉽지 않았습니다.
이는 지금도 마찬가지인데 각자 UI 컨트롤을 위해 노가다 헀던 경험담들을 풀어봅시다.
선수끼리 숨기지말고요~

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/ )를 명시해주세요.

 

읽다보니 보안에 관한 - 클라이언트, 서버, 통신 보안에 관한 글이 좀 필요하지 않을까 하는 생각이 들기도 하네요. 보통 C/S 를 구성할 때 보안처리는 어떻게 하는지 아시는 분~~~!!!

  • Rhea君 2012.01.20 12:00    
  • 보통 SSL을 이용합니다. 그러나 보안이 들어가면 그만큼 느려지므로 로긴시나 주요 부분에서만 쓰입니다.
    평소 패킷은 세션키를 유지하는 방법으로 보안을 합니다.
  • 죠쉬 2012.01.20 13:09    
  • Rhea君 // 그렇군요... UO에서 하던 것 같이 모든 데이터를 코드화 하는 방식을 사용하지는 않는 모양이네요

    다시 생각해 보니 게임 서버의 경우 로그인시에 보안유지 하는 것 이상은 필요가 없을 지 도 모르겠다는 생각이 들기는 하네요

    플레이 데이터를 암호화 해봐야 얻을 수 있는 이익이 전무할지도 모르겠습니다. 봇... 도 못 막겠네요
  • Rhea 2012.01.20 13:59    
  • 물론 일반 패킷의 경우에도 XOR 기반의 키는 들어갑니다.
    업데이트 할때마다 이 키가 바뀌는 경우도 있지요.

    실제 패킷 암호화로만 100% 보안을 유지한다는 것은 불가능하다 생각합니다.
    모든 패킷에 PKI 기반의 인증서를 달수는 없으니까요.

    나중에 여러 게임의 Login방식에 대해 정리할 필요가 느껴집니다.

    국내 게임들은 대부분 웹에서 실행된다는 것도 보안에 있어 중요한 요소입니다.
    로긴을 게임 자체가 아닌 웹서버로 맡기는 것이죠.
  • 죠쉬 2012.01.20 14:36    
  • 엑? 업데이트 할 때 XOR에 사용되는 키가 바뀐다구요? 그럼 궂이 XOR로 데이터를 숨길 필요가 있나요?
  • Rhea 2012.01.20 17:44    
  • 단순 XOR일 경우, 비밀키가 클라이언트도에 있으니까 자주 바꾸는 것을 말한겁니다.

 

    1. 재미있게 잘 읽었습니다. 옛날 기억이 새록새록~  정말 잘 정리해주시는 거 같습니다. 잘 풀어낸다는 건 역시 내공이겠죠~ 멋져요 +_+

      한가지, 스타1에서 1-5) 세션 호스트로써 게임 시작 시에 배틀넷과의 접속은 끊지 않을거 같습니다. 확인해본바는 아니고, 지금 스타1을 깔지도 않을거지만  왜냐하면, 게임 중에 '배틀넷과의 접속이 끊어졌습니다' 라는 메시지를 한번씩 봤거든요~ 이미 배틀넷과의 접속을 끊고 게임을 시작했다면 저런 메시지를 굳이 뿌리지도 않을것이며, 끊어졌다는 이벤트도 발생하지 않을거 같아요~

  1. 좆뉴비 2014.02.26 16:38      
  2. 궁금한게 있는데요.
    스타에서 게임이 시작되면 베넷과의 연결이 끊긴다고 하셧는데, 실제 게임중에 보면 친구들와 대화 "/f w 안녕" 이러면 서로간에 대화가 가능하고 그렇습니다.
    이건 P2P 설명을 위해서는 끊겼다 하신거지, 실제로는 서버랑 연결이 계속 지속되고 있는것 맞겠죠

 

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.comhttp://www.gamedevforever.com/92

 

 

으음, 본의 아니게 5일이나 늦었습니다.
백수 주제에 아파치 웹서버 설정 문제로 골치를 앓아 시간을 소비했습니다.
대신 오늘 포스트는 가장 실용적인 포스트와 예제가 될 것입니다.

여친 만들기는 여전히 노력조차 못하고 있는데, 도대체 왜 이렇게 사나... 생각중입니다.
최근 고등학교때 동기, 후배분, 선생님을 뵙고 오니 더 복잡한 생각이 듭니다.

 

 

이번엔 다리 휘었다는 댓글은 안달리길 바라며.

오늘의 주제는 데이터베이스(Database)와 그것을 이용한 프로젝트입니다.
그런데 이상하게도 어느 게임 관련 서적에도 DBMS에 대한 이야기는 거의 나오질 않습니다.
먹고 사는데 중요한 정보는 쉽게 노출되지 않아서 일까요?
하지만 게임 업계에 근무하시는 분들은 이 DBMS야말로 핵심 요소라는 것을 잘 알고 계실 겁니다.

최근 KGC에서 N문사의 강산아님이 DBMS와 SQL 최적화에 대한 이야기를 해주시고 있는 것은,
그런 의미에서 아주 좋은 발전이라 생각합니다(서평을 바라신다면 한권 보내주시길 바랍니다. ^^).

 

 

열심히 읽어드리겠음.


자, 그럼 DBMS는 게임에서 어디에 쓰일까요?

1) 멤버쉽 전체 - 가입, 탈퇴, 로긴, 친구, 길드, 아바타...
2) 게임 컨텐츠 - 경험치, 레벨, 아이템, 공격력, 전적...
3) 몹정보 - 몹의 경험치, 레벨, 아이템, 공격력, 전적...
4) 사용자 패턴 분석
5) 그외 "정보"라고 할수 있는 모든 것을 담을 때

네, 다시 말해 게임과 서비스의 모든 것을 담고 있는 곳이 DBMS입니다.

또한 DBMS는 다양한 서버들에 있어 가장 쉬운 서버간 다리(brige)와 동기화 객체 역활을 해줍니다.

보통 회원 가입은 웹페이지(웹서버)에서 받습니다만, 게임 정보는 게임 서버를 이용합니다.
별것 아닌 단순한 형태이지만, 이미 한 DB에게 웹서버와 게임서버라는 역활이 다른 두개의 서버가 연결이 되었고
아무 쉬운 방법으로 두 서버가 한 사용자에 대한 정보를 공유한 것입니다.

그리고 이 웹서버와 게임 서버를 각각의 쓰레드 객체라고 생각하고 DB를 한 유저에 대한 유저 객체라고 놓고 본다면,
유저 객체에서 트랜잭션(transaction)을 이용해 쓰레드 동기화를 구현하고 있는 셈입니다.
이런 관점이 아키텍트로 나가는 첫 시점이라 생각합니다.

1. DBMS 기초
DB에 대해 익숙하신 분도 있고, 익숙하지 않은 분들도 많을 것입니다.
특히 학생 때에 DB에 대해 관심을 가지는 분들이 적습니다. 아마 비쥬얼한 것이 없고 DB를 사용하는 프로젝트도 없기 때문일 것입니다.
그래서 설명을 하긴 해야하는데...




.....




..................





......네, 코드도 재활용하는데 글이라고 재활용 못하겠습니까?
억지로 찾은 아래의 링크로 DB와 DBMS를 이용하는 법에 대한 내용은 날로 먹겠습니다.

 

 

날로 먹으니 더 맛있네요~

1편 http://www.zdnet.co.kr/news/news_view.asp?artice_id=00000039130809
2편 http://www.zdnet.co.kr/news/news_view.asp?artice_id=00000039131033
3편 http://www.zdnet.co.kr/news/news_view.asp?artice_id=00000039131490

그러고 보니;;;;;;;;;;


날로 먹은 위의 글은, 당시 테마가 MFC라서 MFC로 되어져있지만, MFC가 아니더래도 똑같습니다.

만약 여기에 더 추가한다면 ADO.NET과 요즘 유행(?)하는 NoSQL에 대한 이야기를 적어볼까 합니다.
그러나 ADO.NET은 DataSet을 비롯한 강력한 기능에도 불구하고 닷넷 프레임워크를 필요로 한다는 점이 결정의 걸림돌이 됩니다.
즉 ADO.NET을 이용한다면 서버도 C#을 써야 합니다. Managed C++ 따윈 잊으십시요.

또한 NoSQL은 트랜잭션 처리와 보안과 안정성에 있어서 연구(그리고 직접 뜯어고치는 재빌드 필수)가 더 필요하다고 봅니다.
BLOB을 대체하기 위한 방법으로는 좋으나 마냥 NoSQL이 진리다~!! 라고 생각하는 것은 아니라고 봅니다.
주위에 NoSQL이 뜬다고 우리도 걍 NoSQL로 개발해~ NoSQL로 뒤짚어~~라는 분 있으면 이 문장을 보여주세요.
그래도 말 안들으면 딴 회사 가세요~~~
참고로 저는 mongoDB을 DB계의 UDP라고 생각합니다. 현존 관계형 DBMS가 TCP라면 mongoDB은 딱 UDP 같은 느낌입니다.

본격 Native C++ 강좌인 이 연재에서는 ADO를 이용하여 DB를 연결하겠습니다.

2. Winsock과 ADO를 이용한 아바타 채팅 시스템
간만에 실전 코드로 돌아가봅시다.
우리가 만들 것은 소켓과 DB를 이용한 로그인 + 아바타 채팅 시스템입니다.
로긴에 성공하면 DB에 저장된 자신의 아바타가 화면에 나오며 상대와 채팅을 할때면 대화를 보낸 사람의 아바타가 표시됩니다.

프로젝트 명세서

1) DB에는 ID와 PWD, 그리고 사용자 이름과 아바타가 있다.
2) 클라이언트에는 서버에게 ID와 PWD를 보낸다.
3) 서버는 클라이언트가 보낸 ID와 PWD를 DBMS에 질의 한다. 이때 DB 연결 방식은 ADO를 이용한다.
4) 로긴에 실패하면 실패 패킷을, 성공하면 사용자의 추가 데이터(이름, 아바타)를 보낸다.
5) 클라이언트가 보내는 채팅 메시지에는 아바타 정보가 포함된다.


명세서가 나왔으니 가장 먼저 DB를 만들어 봅시다.
MS-SQL Server 같은 것이 있으면 좋겠지만 대부분 무리일 것이니 위의 강좌처럼 MS-Access를 이용하겠습니다.
Access는 Excel에게 밀려 Office를 설치하며 잊으시는 분도 많을 것입니다.
그러나 Access는 MS-SQL Server를 대신하여 공부하기에 정말 좋은 개인용 DBMS입니다.
MS-SQL Server 로 마이그레이션도 아주 훌륭합니다.

이런 이야기를 왜 드리냐면, 수많은 게임 회사들은 전담 DBA가 없는 경우가 많기 때문입니다.
초기 개발단계나 규모가 작은 개발사라면 서버 개발자가 DBMS를 만질 수 밖에 없는 안타까움이 있습니다(...가끔 SE질도 합니다.).
신입 서버 개발자라면 이력서에 자신있는 DBMS 하나를 꼭 적어보시길 바랍니다.

2,1 Database 설계
넹, 딸랑 한 테이블입니다.
JOIN이고 머고 필요없습니다. 이 강좌가 SQL 강좌는 아니니까요.

 

 

꼴랑 필드 4개

 

 

 

ID와 PWD를 소켓으로 받아 NAME과 AVATRA를 내려보냅니다.


별다른 설명이 없어도 어떻게 사용될지 보입니다.
서버에서 요청할 SQL문은 SELECT NAME, AVATAR FROM USER WHERE ID='test1' AND PWD ='aaaa' 뿐입니다.
BOF랑 EOF가 같다면 당연히 로긴 실패가 되는 것이고요.
회원 가입과 아바타 설정은 웹프로그래머분이 웹서버에서 받아주세요~

아, 그리고 가급적 DBMS에 질의는 생SQL문이 아니라 반드시 Stored Procedure를 써서 접근하는 습관이 중요합니다.
그런데 저는 이미 습관화 되어 있으므로 이번 예제에는 걍 생SQL문을 날렸습니다.
이는 단순히 SP가 빠르다는 이유가 아니라,
정기적으로 DBMS의 성능을 테스트(Query Profile)하는데 생SQL문을 쓰면 제대로 분석하기 힘들기 때문입니다.

2.2 ADO를 이용한 DB접근
이번 회 예제에는 가장 기본적인 형태의 ADO 클래스가 들어있습니다.
이미 다이렉트X를 날고 기는 독자분들에게 COM 컴포넌트 인터페이스 얻어오는 법은 장난일꺼라 생각되어 회원정보를 질의하는 부분만 보겠습니다.

 

if (S_OK == rad.RAConnect(_T("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=..\\DB\\MyDB.mdb;"))) {
    oRs = rad.RAGetDisconnectedRs(strQueryLogon.c_str());
    CComVariant val;
    if (oRs) {
        while (!oRs - > EndOfFile) {
            // Get Name
            val = oRs - > Fields - > Item[_variant_t(_T("NAME"))] - > Value;
            if (val.vt != (VT_NULL)) {
                printf("NAME : %s\n", (char * ) _bstr_t(val));
                m_strName = (char * ) _bstr_t(val);
            }

            // Get Avatar
            val = oRs - > Fields - > Item[_variant_t(_T("AVATAR"))] - > Value;
            if (val.vt != (VT_NULL)) {
                printf("AVATAR : %s\n", (char * ) _bstr_t(val));
                val.ChangeType(VT_I4);
                m_iAvatar = val.lVal;
            }
            bIsFind = TRUE;

            oRs - > MoveNext();
        }
        oRs - > Release();
        oRs - > Close();
    }
}

 

 

참 쉽죠?

ADO 연결 문자셋으로 MDB 파일에 접근한 후, Recordset 객체를 다시 만들어 SELECT의 결과값을 획득합니다.
이때 귀찮은 것이 COM 변수를 일반 변수로 변환하는 것일 겁니다.
NAME은 문자형이므로 _bstr_t를 그대로 변환하였는데 AVATAR는 INT형이라 CComVariant 에서 제공하느 ChangeType()으로 변환했습니다.
COM 변수를 변환할때는 atoi()같은 고전 함수보다 COM에서 제공하는 방법을 사용하는 것이 안전합니다.

...사실 요즘 COM 공부하시는 분들 별로 없으리라 생각됩니다.
예전에 게임 아카데미에서 COM 강의를 해보니 학생 분들 모두 전멸을 하여 아주 행복했습니다.
하지만 윈도우용 함수는 크게 WIn32 API와 COM으로 구분됩니다. SH로 시작하는 쉘함수들 역시 COM의 한 부분입니다.
닷넷 프레임워크 역시 COM과 XML의 조합입니다.
이중 디스패치 구현해가며 COM 컴포넌트를 직접 만드는건 시대에 뒤떨어지는 일이라 하더라도
기존 COM 컴포넌트를 이용하는 것이 어려우면 막강한 윈도우 OS를 반 밖에 사용하지 못하는 것이라 생각됩니다.

2.3 패킷 설계
DB와 ADO를 극복하고 가장 재밌는 부분으로 돌아왔습니다.
먼저 이 프로젝트에 사용될 패킷을 적어봅시다.

 

//
// MyPacket.h
//
#define MSG_LOGIN 1000
#define MSG_LOGINFAIL 1001
#define MSG_LOGINSUCC 1002
#define MSG_MESSAGE 1003

// 헤드 검출용 C, S
class CGENERIC {
    public:
        DWORD head;
};

// 로긴 패킷 C->S
class CLOGIN: public CGENERIC {
    public: char id[20];
    char pwd[20];
};

// 로긴 실패 S->C
class CLOGFAIL: public CGENERIC {
    public:
};

// 로긴 성공 S->C
class CLOGSUCC: public CGENERIC {
    public: char id[20];
    DWORD avatar;
    char name[20];
};

// 메시지 C->S->C
class CMESSAGE: public CGENERIC {
    public: char id[20];
    DWORD avatar;
    char message[500];
};

 

전부 고정 패킷입니다. 아직 string이나 vector를 그래도 송수신하게 해주는 패킷 생성기를 만들지 않았으니까요.
누군가 패킷 생성기 대신 포스팅 해주실 분 없나요?
아직 패킷수도 많지 않으니까 패킷 클래스 이름에 송신용, 수신용, 알림용 네이밍 규격을 붙이지 않았습니다.

 

 

점점 포스트가 읽기 어려워지고 있습니다.

모든 패킷은 CGENERIC을 상속받고 있습니다.
일차적으로 모든 패킷은 CHAR에서 CGENERIC으로 캐스팅 되어 어떤 패킷인지를 분석하게 됩니다.
그런데 CGENERIC에 패킷 판독용 DWORD만 있고 실제 데이터 크기를 담은 정보는 없군요?
1회 포스팅 때 나갔던 소스를 보시면 CDataMessage라는 버퍼용 클래스가 있고 그것에서 앞부분 4Byte를 패킷 사이즈용으로 사용하고 있습니다. 즉, 모든 패킷은 CDataMessage로 다시 감싸는 구조로 되어있습니다.

명확한 설명을 위해 그림으로 다시 그려보죠.

 

 

CLOGIN 패킷으로 접속과 함께 로긴 정보인 ID, PWD를 보냅니다.

 

 

 

DB 질의 결과에 따라 성공, 실패를 보냅니다. 성공시 NAME과 AVATAR를 보내며 실패면 클라이언트는 접속을 끊습니다.

 

 

이제 다른 클라이언트B가 접속을 합니다.

 

 

 

누군가 접속을 성공하면 서버는 모두에게 CLOGINSUCC을 보냅니다. 이미 연결되어 있던 클라이언트는 이 정보를 누군가 입장했다고 판단합니다.

 

 

 

서버는 단순히 CMESSAGE 패킷을 중계만 합니다.


2.4 클라이언트 구현
아래는 클라이언트의 핵심 구현부 입니다.
중요한 것은 로긴 성공과 메시지에 있어 같은 패킷을 이용하여 클라이언트에서 판정하고 있다는 것입니다. 
패킷 처리 부분은 이미 서버와 클라이언트가 비슷하므로 서버는 별다른 설명이 필요 없을 것입니다.

 

// 일단 받은 패킷의 종류를 알아본다.
CGENERIC * pGenericPack = (CGENERIC * ) szRecvData;

// 해당 패킷 처리로 따로따로 보낸다.
switch (pGenericPack - > head) {
    case MSG_LOGINFAIL: {
        pMainWnd - > OnCloseToServer(TRUE);
        pMainWnd - > AddToLog(_T("ID나 비밀번호를 체크하세요."));
    }
    break;

case MSG_LOGINSUCC: {
    CLOGSUCC * pLoginSucc = (CLOGSUCC * ) szRecvData;
    pLoginSucc - > avatar;
    pLoginSucc - > name;
    pLoginSucc - > id;
    CString strUserName, strID;

    if (pMainWnd - > m_strMyID.IsEmpty()) {
        strUserName = pLoginSucc - > name;
        strUserName = strUserName + " 님 어서오세요.(By Server).";
        pMainWnd - > AddToLog(strUserName);

        pMainWnd - > m_iMyAvatar = pLoginSucc - > avatar;
        TRACE("%d\n", pMainWnd - > m_iMyAvatar);

        pMainWnd - > m_strMyID = pLoginSucc - > id;
        pMainWnd - > DrawMyAvatar();
    } else {
        strUserName = pLoginSucc - > name;
        strUserName = strUserName + " 님이 입장하셨습니다.(By Server).";
        pMainWnd - > AddToLog(strUserName);
    }
}
break;

case MSG_MESSAGE: {
    CMESSAGE * pMessagePack = (CMESSAGE * ) szRecvData;
    CString strRecvMessage, strID;
    strRecvMessage.Format("%s : %s", pMessagePack - > id, pMessagePack - > message);
    strID = pMessagePack - > id;

    pMainWnd - > AddToLog(strRecvMessage);

    if (pMainWnd - > m_strID != strID) {
        pMainWnd - > m_iOtherAvatar = (int) pMessagePack - > avatar;
        pMainWnd - > DrawOtherAvatar(); // 상대 캐릭터
    }
}
break;


3. 인증샷, 하지만...
자, 이렇게 완성되었습니다.

 

 

덕내쩌는 클라이언트라 덕후가 아니면 받지 못함 ㅋㅋㅋㅋㅋㅋㅋ


이 포스팅을 읽으며 한가지 새로운 사실을 알수가 있는데,
서버에서는 별다른 연산이 없었지만 나름 아바타가 포함된 그럴싸한 채팅 시스템이 만들어졌습니다.
이는 DB라는 자료구조가 존재하기 때문이죠.
사실 게임 서버에서 타게팅, 넌타게팅, 3D 위치값 같은 게임 냄새 물씬 나는 요소는 결코 절대적인 비중을 차지 하지 않습니다.
개발자의 시선에서 게임 서버는 DBMS의 정보를 클라이언트에게 보내주고, 또한 클라이언트가 보낸 정보를 DBMS에 쌓기 위해 존재합니다.

3회만에 이 프로젝트를 포스팅 할 수 있어 무척 기쁩니다.
소스를 잘 만들었다는 이야기가 아니라, DB가 투하된 실전용 클라이언트/서버의 핵심을 선보였다는 것이며
그와 동시에 클라이언트/서버의 모든 문제점을 고스란히 알수 있는 프로젝트이기 때문입니다.

문제점이야 밤새도록 적을 수 있겠지만 우선 몇가지만 적어보죠.

4. 더 생각해볼 꺼리
오늘은 더 생각해볼 꺼리가 참 많네요.

1) 서로 채팅을 할때 NAME 대신 ID가 보여지고 있습니다. 기껏 만든 NAME을 자기만 보고 있는 셈이죠.
이를 ID와 NAME이 동시에 보여지도록 해봅시다.
또한 그렇게 할때 어떻게 하면 가장 작은 사이즈의 패킷을 보낼 수 있을까요?

2) 이 프로젝트에는 현재 치명적인 버그(?)가 있습니다!
한 클라이언트가 로긴을 실패하면 CLOGINFAIL 패킷을 받아 모든 클라이언트가 접속이 끊어지고 맙니다.
이 문제를 어떻게 해결할수 있을까요?
클라이언트에서 고치는 방법(패킷 수정 포함)과 서버에서 해결하는 방법, 각각 두가지 방법을 생각해봅시다.

3) DB 연결
현재 서버는 CLOGIN 패킷을 받을때 ADO로 DBMS에 연결과 질의, 그리고 닫기를 수행하고 있습니다.
만약 MMORPG처럼 다수의 사용자가 접속을 한다면,
아니 지금 프로젝트에서도 모든 사용자의 채팅을 서버에 저장한다면(법적인 문제를 떠나) DBMS는 뻗게 됩니다.
어떻게 하면 DBMS에게 무리를 주지 않으며 편안한 서비스를 할 수 있을까요?
단순히 DBMS 폴링도 좋은 방법이지만 그 규모조차 넘어서면 어떻게 할까요?
또한 게임 서버뿐만 아니라 웹서버에서도 사용자 정보를 갱신한다면(닉네임 변경) 어떤 문제와 해결점이 있을까요?

아, 그리고 지금 현재에도 이 프로젝트에서 DB질의 시간이 가장 많은 시간을 소모하고 있을 것입니다.

4) 서버와 월드
이 프로젝트가 인기가 좋아 동접 20만명(...)의 프로젝트가 되었다면 합시다.
그래서 서버를 늘립니다.
서버를 늘렸을때 각각 다른 서버에 있는 사용자들끼리 채팅을 하게 해야할까요?
아니면 깔끔하게 포기할까요?

전자라면 서버"들"을 어떻게 구성해야 할까요?
후자라면 그게 바로 한 월드가 되며 다른 월드의 친구와는 게임을 당연히 할수 없을 것입니다.
그럼에도 불구하고 다른 친구에게 쪽지라도 보내고 싶다는 고갱의 건의가 왔고 기획팀에서 해달라고 하면 어떻게 할까요?

5) 지난 시간 소개해드린 WireShark로 보면 ID와 PWD는 그대로 드러납니다.
오픈하자마자 중국계 IP들의 놀이터가 되겠군요.
웹에서 로긴이 아닌 게임에서 로긴이라면 어떤 방식으로 숨겨야 할까요?

1), 2)는 네트워크 공부를 처음하는 분이라도 해결할수 있는 부분이고
나머지는 현업을 준비하는 분들에게 알맞은 토픽입니다.

이것으로 이번 포스팅을 마치도록 하겠습니다.
늦어져서 다시 한번 죄송하고요, 이번 포스팅의 서버에서는 ADO 질의 이외에는 일체의 로직이 없었습니다.
다음 시간에는 서버에 로직과 자료구조를 투척해보겠습니다. 바로 게임으로 들어가는 관문인 로비(Lobby)를 만들어볼까 합니다.

 

 

언젠간 이런 날이... .

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/ )를 명시해주세요.

프로젝트 다운로드 : http://rhea.pe.kr/attachment/cfile22.uf@1272AD3A4F32B534061400.zip

 

GDF3.zip
0.47MB

 

---------------------------------------------------------------------------------------------------------------------------------------------------

  1. 에에에에... 답을 달고 싶은데... 
    이번 회의 문제는 출제 의도를 모르겠거나
    전혀 경험이 없는 분야라...

    (답을 달고시퍼 달고시퍼 달고시퍼~~~~)

    특히 4번의 '다른 서버로 메시지 전달'이 매우 궁금합니다.
    답이 뭔가영???

    그리고, 네기마 카드 이미지 파일좀 얻을 수 없을까영? (응? 더쿠 인증 한건가?)
    • Rhea 2012.02.09 11:07    
    • 3), 4)는 본격 분산서버에 대한 떡밥입니다. 서버끼리 통신해야죠. :-)
      아마 그거 이야기는 튜터리얼 편이 끝나고이지 않을까 합니다.

      네기마 카드는 저도 없어요, 딱 저만한 쫴끄만한 이미지여요. 그것도 퀄엉망.
  2. 크로스 2012.02.09 11:13      
    • Rhea君 2012.02.09 16:40    
    • mySQL 열심히 하세요~
  3. 이번에도 좋은 글 감사드립니다~!
    그런데 C++코드를 올리실때 http://bluekms21.blog.me/10131355275 이런걸 써보시는건 어떠신지요?
    자신있는 DBMS라.. 전 사실 윈7 초반에 SQL Server를 깔려다 대 실패한 뒤로 mySQL을 쓰고있는데
    예제는 엑세스DB로 되어있군요.. 이력서에 엑세스DB로 적어도 되는걸까 싶네요.. ㅇ<-<


    그냥 생각해 본 대답들입니다...
    부끄럽고요;; 틀렸으면 누가 보시고 비웃지만 마시고 알려주시면서 비웃어주세요.. ㅠ_ㅠ;;

    1) 체팅을 시도한 클라이언트의 키값인 ID만 입력받아
    NAME는 DB에서 SELECE 해서 띄워주는 방법을 생각해 봤는데...
    느릴거같네요..? 혹시 다른 방법이 있으시면 가르쳐 주시면 좋겠습니다;;

    2) [클라에서 고치는 방법]
    - 로그인 성공 시 플레그를 설정하여 CLOGINFAIL을 받아도 끊어지지 않게 한다
    [서버에서 고치는 방법]
    - 로그인 실패 시 접속 시도한 소켓에게만 CLOGINFAIL을 전송한다
    전송되는 페킷의 양을 생각했을 때 서버에서 고치는 방법이 나을것 같네요?

    3) 일단 폴링에 대해 검색해봤습니다만...
    클라이언트에서 질의를 날리고 DBMS는 응답만 한다. 는것 같네요. 일단 이것도 해 놓고
    로그인 전용 로비를 따로 만드는것으로 해결되지 않을까 싶습니다.

    4) 깔끔하게 포기합니다
    대신 기획자의 강렬한 열망이 느껴질 경우 해당 아이템을 '케쉬템'으로 팔아서 서버의 부하를 줄입시다.
    아마도 경영진도 좋아할 것입니다. 하핫;;;
    전자라면 역시 체팅이나 쪽지 전용 서버를 구축하여 전체 메시지는 별도로 처리하는게 좋을것 같습니다.

    5) 페킷을 암호화해서 전송하면 될것 같습니다...만
    사실 제가 암호화나 보안 방식에 대해서는 많이 부족합니다;;
    그냥 게임가드같은거 쓰면 안될까? 싶기도 하고요;;
    이 글에 보안방식은 범위 밖이겠지만 다른 훌륭하신 분들이 언젠간 강의해주실거라 믿습니다!

    추신 : 일단 나갔다 온 뒤 이 좋은글을 제 블로그로 모셔가겠습니다.. 하핫;;
    언제나 좋은 글 감사합니다.
  4. 크로스 2012.02.09 11:36      
  5. 그런데.. 글쓰신 Rhea君님과 Rhea님은 동일인물 이시죠??
  6. 하늘향기 2012.02.09 14:37      
    • Rhea君 2012.02.09 16:43    
    • ASSERT(1 == sizeof(RHEA'S_HEART));

      글로만 보는 감사는 사양합니다.
      간혹 "그냥 댓가없이 많은 사람이 봐주셨으면 하는걸요~"라고 하시는 분들이 있는데,
      그거슨 "져도 괜찮아, 출전에 의미가 있는거야..."하는 것과 같습니다.

      저는,
      향응! 향응! 향응! 향응!
      향응! 향응! 향응! 향응!
      향응! 향응! 향응! 향응!
      향응! 향응! 향응! 향응!
      누나/여동생을 바쳐라!!
      을 원합니다.
    •  죠쉬 2012.02.09 17:31    
    • 웃흥~
      울 누님이 82 학번이신데... 괜찮으신가요?
      거부하시려면
      제게 향응을 제공하셔야 할겁니다.
      냐하하하하하~~~~!!!!
    • 하늘향기 2012.02.18 12:48    
    • ㅠㅠ 누나를 바치고 싶어도... 4분 다.. 임자 있는몸이라서 ㅠㅠ
  7. 매번 좋은 글(숙제) 올려주셔서 감사합니다 
  8. SanAh Kang 2012.02.09 17:03      
  9. 아아 좋군요! 하지만 회는 무리입니다! ㅋㅋ

    책은.. 출판사랑 최종 수정본 왔다갔다 하고 있습니다.ㅎㅎ

    SQL Server Express Edition 같은 무료 버전을 학습용으로 사용하면 그만입니다. ㅋ

    게임 개발에 들어가는 db설계, 최적화 이슈는 사실 그리 많지는 않습니다만 최적화를 위해 공부해야 하는 부분이 워낙 광범위하고 깊이 들어가야 하는지라 세미나 등으로 알려드리기엔 터무니 없이 시간이 부족해버립니다. 
    *Index 설명에 8시간 정도 걸린다거나... ㅡㅅ ㅡ;;
  10. 오메가 2012.02.09 19:12      
  11. 재밌는 떡밥을 던져주셔서 감사합니다.

    생각해 볼 거리의 3, 4번에 대한 대답이 "우리도 걍 NoSQL로 개발해~ NoSQL로 뒤짚어~~" 가 아닌가 싶습니다.

    게임에서 이용되는 데이터 중에서 채팅과 같이 ACID가 요구되지 않으며 부하가 큰 부분이나 웹서버와 공유해야 할 부분을 NoSQL에서 처리하는 것도 한 가지 방법이 될 수 있을 것 같구요, 
    트랜잭션 처리 등의 모델은 언급하신 MongoDB나 Redis, Memchached 같은 속도와 스케일링 위주의 Key-Value 개념의 NoSQL 대신 Cassandra나 HBase 등의 진화된(?) DB를 이용하면 좀 더 편하게 처리할 수 있을 것 같네요.
    실제로 Facebook도 메시지 시스템을 HBase 위에 얹어놨는데 써 보신 분들은 아시겠지만 거의 실시간입니다. (http://www.facebook.com/note.php?note_id=454991608919)


    지나가다 답변에 불타오른 김에 5번도 질러보자면,
    일단 로그인 과정이나 개인정보 전송 등에만 부분적으로 SSL을 이용한다는 원론적이면서도 대책 없는 대답 하나랑 
    패스워드 전송에 있어서는 뜬금없이 떠오른 방법으로...
    1. 서버가 클라이언트에 SALT로 쓸 문자열을 보낸다.(SALT는 매번 랜덤하게 바뀌어야 하구요)
    2. 클라이언트는 패스워드의 해시에 SALT를 붙인 다음 해싱해서 서버로 로그인을 시도한다.
    3. 서버는 <1에서 보냈던 SALT 값을 DB의 패스워드 해시에 붙여 다시 해싱한 결과>와 <2의 결과>를 비교하여 로그인 시켜준다.
    를 한 번 던져 봅니다.
  12. 크로스 2012.02.10 23:22      
  13. http://bluekms21.blog.me/10131514869 로 퍼갔습니다. 출처 명시 확실히 했구요...
    너무 좋은 글이라 본문을 소장하고 싶어서 시작한 일인데 사람들이 저를 오덕으로 봅니다. 어쩌죠?
  14.  김포프 2012.02.17 07:44      
  15. 이번엔 다리가 매우 맘에 듭니다 -_-;
  16. 황금나무 2012.02.17 11:51      
  17. 정말 재미있게 보고 있습니다.
    책 많이 보고 헤딩해야 알수있는 내용들이 담겨있네요.
    저도 본문을 퍼가고 싶지만... 음 친구들이 어떻게 볼지.
  18. 우리 2015.12.01 21:08      
  19. 구글링하다가 이 블로그에 오게되었습니다. 강좌도 잘 보고있구요. 혹시 게임서버 구동을 위한 서버팩구축에 대해서만 다룬 강좌도있나요? 와이어샤크로 현존게임서버의 패킷분석과 서버팩 제작간의 연관성만을 자세히 다룬 강좌가 있나해서 댓글달아봅니다.

 

 

=================================

=================================

=================================

 

 

 

반응형

 

728x90

 

 

 

 

출처: http://www.gamedevforever.com ,http://www.gamedevforever.com/128

 

 

네트워크 게임 튜터리얼 4 - 로비, 그리고 할말이 많다

 

한 회 빠뜨려먹은 Rhea입니다.
대신 다음 회는 좀 빨리 쓰겠습니다.

그리고,


 

양성평등을 위해 이번 사과의 짤은 남자 사진으로 올려드립니다.
저는 이렇듯 녀성부와 셧다운제를 찬성하는 녀성님들을 존중합니다.

1. C/S의 생명주기
Scott Meyers의 명저서, Effective C++을 한마디로 정의하지면 객체의 잘 생성해서 잘 써먹다가, 필요없는때는 잘 제거하는 차가운 도시남자의 객체 관리법이라고 할수 있습니다. 그리고 이 객체라는 것은 C++ 클래스의 객체 만이 아닌 한 인스턴스(Instance) 그 자체라고도 볼수 있습니다.

 

 

흔한 올바른 객체 제거 방법. 비단 컴퓨터 내부 뿐만 아니라 현실에도 제거를 잊은 객체들이 많습니다.

따라서 인스턴스는 나름의 객체 수명이 있습니다.
이 객체 수명은 인스턴스 자기가 결정하는 것이 아니라 OS나 자신을 Embeding하는 컨테이너에 의해 결정됩니다.
Win32 API를 처음 배울 때부터 우리는 이것을 알게 모르게 배워왔습니다.
RegisterClassEx()로 윈도우를 등록하고 CreateWindow()로 창을 생성하고 각종 메시지를 받아 해당 작업을 하다가 WM_DESTROY 메시지가 날라올때 new로 만들어진 객체들을 지워주어야 합니다.

이런 것은 비단 Win32만이 아닙니다, 아래 이미지는 아이폰 앱의 수명 주기의 일부분인데 대부분의 인스턴스의 생명주기는 이와 같은 구조를 지닙니다.

 

 

출처 : https://developer.apple.com


뻔한 이야기를 왜 하냐면 Clinet/Server의 평등한 관계라는 시각에서 볼때,
클라이언트와 서버도 이같은 네트워크 생명주기를 지닌다는 점을 강조하기 싶기 때문입니다.

다시 말해, 게임룸에 들어가기전, 로비(Lobby)에서 C/S에서 필요로 하는 객체의 초기화 과정이 필요합니다.
게임 서버가 아닌 로비 서버에서 클라이언트에 필요한 객체를 초기화 및 업데이트를 하고 올바른 정보를 갖고 게임룸으로 들어가야 합니다.
분산 서버이기 때문에 이와 같은 과정은 로비에서만 일어나고 게임룸에서는 오직 게임에만 신경쓰도록 해야합니다.
잘 돌아가고 개발이 잘되는 프로젝트는 이같이 뻔한 것들이 잘 지켜지는 프로젝트들입니다.

 

 

반도의 흔한 네트워크 게임 프로세스


위의 그림은 간단한 네트워크 게임의 프로세스를 다시 한번 간략히 그려본 것입니다.
이 그림위에 수명 주기를 다시 체크해보죠.
네트워크 역시 다른 곳에서 자주보던 그림이 나왔습니다.

 

 

비밀인데 아무 것도 안하면서 일한 척 할려면 그림을 잘그리면 됩니다.


이러한 그림 연습이 코딩하는데 도움이 되냐고 반문할수 있지만 점점 복잡해져가는 네트워크 구성에 큰 도움이 된다고 확신합니다.
이런 설계가 있으면 언제 어디서 클라이언트와 서버에 각각 정보를 채울 것인지 확연해집니다.
물론 호출되는 함수는 send() 함수와 recv() 뿐이겠지요.

이런 식의 그림 연습은 앞으로도 종종 해볼 것입니다.
4년 내내 다른 친구들 장학금 받는데 큰 기여를 한 성적이라 대학 수업은 잘 모르지만,
대학에서 OOP을 배우는 이유는 C++이나 JAVA같은 언어 자체를 배우기 위한 시간이 아니라고 생각됩니다.
하도 공부를 못해 4학년 졸업할 때 너는 프로그래밍을 못하니 일년 더 다녀보라는 말을 들을지라
제 말이 맞는지는 잘 모르겠습니다만, 제 생각은 이렇습니다.

OOP는 현실을 반영한 철학과도 같은 개념이기에 객체간의 관계와 수명주기를 다른 곳에도 많이 응용해서 디자인할수 있습니다.
위의 그림들에서 클라이언트와 각 서버들은 한 인스턴스 속에 들어있는 다섯개의 객체(혹은 클래스)라고 봐도 무방합니다.
Client 객체가 Path Server, Load Balance Server, Lobby Server, Game Server라는 네개의 객체에게 질의를 하는 구조이며
서버인 Lobby Server와 Game Server의 객체는 Database에 의해 전역 데이터, 혹은 공유 데이터를 교환할 것입니다.
각 객체들은 자신의 역활만을 해주고 있고 수명도 존재합니다.
즉, OOP의 고급(?)이라 할수 있는 디자인 패턴 역시도 네트워크에서 사용해볼수 있지 않을까요?

...은 지금은 너무 떡밥이 세니 본론으로 돌아가 지난 시간의 아바타 채팅을 다시 변경시켜봅시다.

2. 로비 서버의 제작, 그러나 할말이 많다.
지난 시간에 서버와 클라이언트 이외의 데이터는 로긴을 위한 사용자 테이블 뿐이었습니다.
위의 채팅서버에서 로비서버로 바뀐다면 변경할 포인트가 뭐가 있을까요?
지난 시간처럼 명세서를 꾸며봅시다.

로비서버 명세서

1) 클라이언트가 로비 서버에 입장하면 자신의 정보(닉네임, 전적)를 받는다.
2) 같은 채널내 다른 유저의 정보를 받는다.
3) 다른 유저와 채팅을 할수 있다.
4) 게임룸 리스트를 볼수 있다.
4-1) 자신의 등급이 맞지 않는 룸에는 입장이 불가능하다.
5) 게임룸을 직접 만들수 있다.
5-1) 자신의 등급이 맞는 않은 룸은 만들 수 없다.

...만들어야 할 항목들이 죄다 나왔습니다.
이중 가장 중요한 것은 클라이언트 접속시 다른 유저들과 게임룸 리스트를 클라이언트에게 보여준다는 것입니다.
결론부터 말하자면 이게 생각만큼 쉽지는 않습니다.

..............


..........................................



....................................................................................



....................................................................그냥 수식이나 포스트 하고 나머지는 직접 짜보세요~ 하면서 인공지능 연재할껄.... .

 

 

지금 기분



...그래도 강의 연재 역시 "만들기"라고 본다면 시작했으면 기어이 끝장을 봐야겠죠?





먼저 필요한 것은 클라이언트와 서버에 존재할 CGameUser와 같은 단일 사용자 정의 클래스입니다.
서버는 접속 들어온 사람들만큼 이 CGameUser를 자료구조로 저장하고 있어야 합니다.
이때 속력을 위해 굳히 DBMS에 현재 로비에 접속된 유저를 저장하지는 않습니다.
당연히 로비서버 안에 저장하고 있습니다.
사용자 리스트를 뿌려줄려면 어떻게 해야 할까요?

1) 현재 유저수만큼 send() 함수를 호출해 일일이 CGameUser를 보낸다.

...네 말도 안되죠. 사용자 정보는 UDP가 아닌 TCP로 보낼 중요한 데이터입니다.
줄곧 보낼수도 없거니와 새로 들어온 사용자와 나간 사용자를 다시 결정해야 합니다.
몇명 안되는 게임이라면 이 방법도 가능하겠지만 실제로 코딩해보면 상당히 지저분해 질수밖에 없습니다.
따라서 다른 방법으로,

2) 서버 자료구조의 CGameUser를 통채로 보낸다.

가 있습니다. 
엄청 멋있지만 척 들어도 어렵게 느껴지죠? 만약 CGameUser를 STL map에 저장하고 있다면,
send(UserMap, sizeof(UserMap)); 식으로 보낸다는 이야기입니다.
당연히 서버와 클라이언트는 사용자 관리에 같은 콜렉션을 써야 합니다.
그래서 클라이언트 코드라도 이런 부분은 서버쪽에서 만들기도 합니다.

하지만 콜렉션, 컨테이너를 통채로 보내는 것은 결코 쉬운 방법이 아니며 코딩 길이도 상당해집니다.
네, 맞습니다, 각 게임회사에는 이 방법이 자사 서버 엔진의 아주 중요한 요소중 하나입니다.
바로 객체 직렬화(Object Serialization)입니다.

2-1. 직렬화
직렬화는 서버가 아니더라도 한번쯤 들어보셨을 것입니다.
MFC를 공부하면서 "<<"로 객체를 그대로 파일에 저장하는 것이라던가, JAVA에서 ObjectOutputStream() 같은 함수죠.
단순 구조체라면 쉽습니다만 핵심은 역시 자료구조를 통채로 옮기는 것입니다.

1) XML
XML은 텍스트 기반으로 이뤄진 자료구조입니다.
CGameUser 객체 10개라도, 100개라도 XML로 쉽게 표현할 수 있습니다.
게임에서는 네트워크 프로토콜로 XML을 잘 사용하지는 않습니다. 일단 너무 큰게 문제겠지요.
대신 XML은 HTTP에 실려져 SOAP(http://www.w3.org/TR/soap/)을 구현하는데 많이 사용되며
Action Script와 맞물려 플래시 네트워크 게임에서도 많이 사용됩니다.
저는 일단 비추! 합니다.
단, 게임 말고 Facebook이나 Twitter 정보 받아 올때는 JSON보다 XML을 좋아합니다.
JSON은 사람이 읽기에는 너무 헛갈리거든요.
정말이지 XML은 사람을 위한 언어이고 JSON은 기계를 위한 언어입니다.

2) Protocol Buffer
직렬화는 만만치 않은 작업이기에 구글에서는 무료로 쓸수 있는 Protocol Buffer라는 것을 내놓았습니다.
(http://code.google.com/intl/ko-KR/apis/protocolbuffers/)

그러나 시기를 잘못 탔습니다. 구글이 Protocol Buffer를 내놓았을 때는 이미 스마트폰 열기가 시작되었고,
스마트폰에서의 평가는 좋지 못했습니다.
무엇보다 구글에서 나온 멋진 물건(?)이라는 기대와 달리 컨테이너를 자동으로 직렬화해주는 기능이 없었습니다.
SOAP이나 플래시에서 사용하는 XML보다 사실 별로 나은게 없는거죠.
(혹시 Protocol Buffer를 이용해 재미를 본 분이 있으면 반론과 경험담을 부탁드립니다.)

3) JSON
최근 인기를 가장 끌고 있는 것은 역시 JSON(JavaScript Object Notation)입니다.
(http://json.org/)
JSON은 원래 JavaScript용으로 나온 경량 데이터 교환 포맷이나 XML보다 가볍고 파서 역시 가벼워 PC와 스마트 디바이스를 가리지 않고 위엄을 떨치고 있습니다.
트위터, 페이트스북 같은 유명 SNS들은 XML과 JSON을 모두 지원하며 앱스토어 역시도 JSON을 프로토콜로 사용합니다.
게임에서 역시 마찬가지죠, 실시간 게임 뿐만 아니라 특히 SNG들은 대부분 JSON으로 작동된다고 보셔도 됩니다.

 

 

어떤 과학의

 

 

프 로 토 콜

JSON



...드립 실패했다는 거 알고 있으니 댓글로 지적하진 말아주세요.

4) boost::serialization
XML과 JSON은 그 바탕에 이기종 통신용이라는 탄생 목적을 갖고 있습니다.
윈도우, 유닉스, C++, JAVA 등 서로 다른 OS와 언어끼리 손쉽게 데이터를 교환하는 것이 목적입니다.
그래서 텍스트(UTF-8)형식으로 만들어졌죠.
아이폰 앱이라도 서버까지 Objective-C로 짜지는 않습니다, 안드로이드 앱이라도 서버까지 JAVA로 짤 필요는 없지요.
어쩌면 이런 성격이 스마트 디바이스에서 JSON을 최고의 스타로 만들어준 것인지도 모릅니다.

하지만, 같은 C++끼리라면 어떨까요?
굳히 텍스트 기반을 들고 가야할 필요는 없을 것입니다.
PC 온라인 게임이라면 서버와 클라이언트 모두 C++로 가능하고 C++가 가장 많이 사용되는 환경일 것입니다.
이런 경우에는 boost::serialization 가 깔끔한 솔루션을 제공합니다.
(http://www.boost.org/doc/libs/1_49_0/libs/serialization/doc/index.html)
boost::serialization는 STL 콜렉션들을 그대로 직렬화 시켜주는 정말 멋진 라이브러리입니다.
바로 이 부분이 많은 게임회사 서버팀의 비밀중 하나였지요.
그리고 boost::serialization로 그 C++ STL 직렬화의 비밀을 누구나 사용할 수 있게 되었습니다.

2-2. 패킷 생성기, 혹은 컴파일러
이제까지 적은 내용을 한줄로 요약하면,

실제 온라인/네트워크게임에서는 직렬화를 통해 STL 콜렉션 같이 길이가 정해지지 않은 데이터 덩어리 자체를 그대로 패킷으로 보낸다.

입니다.

하지만 아직 해결해야 할 부분이 있습니다.

지난 시간의 소스를 다시 봅시다.

// 
// MFCClientDlg.cpp
// 로긴 데이터 보내는 부분
//
CLOGIN loginPack;
ZeroMemory( & loginPack, sizeof(loginPack));
loginPack.head = MSG_LOGIN;
memcpy((char * ) & loginPack.id, m_strID, m_strID.GetLength());
memcpy((char * ) & loginPack.pwd, m_strPWD, m_strPWD.GetLength());

CDataMessage msg;
msg.body_length(sizeof(CLOGIN));
memcpy(msg.body(), & loginPack, sizeof(CLOGIN));
msg.encode_header();

m_pClientSocket - > Send( & msg, msg.length());

//
// 채팅 메시지 보내는 부분
//
CMESSAGE messagePack;
ZeroMemory( & messagePack, sizeof(messagePack));
messagePack.head = MSG_MESSAGE;
messagePack.avatar = m_iMyAvatar;
memcpy((char * ) & messagePack.id, m_strMyID, m_strMyID.GetLength());
memcpy((char * ) & messagePack.message, strSendData, strSendData.GetLength());

CDataMessage msg;
ZeroMemory( & msg, sizeof(CDataMessage));
msg.body_length(sizeof(CMESSAGE));
memcpy(msg.body(), & messagePack, sizeof(CMESSAGE));
msg.encode_header();

m_pClientSocket - > Send( & msg, msg.length());

 

 

무언가 코드가 상당히 지저분하지요, 그리고 중복되는 코드가 많습니다.
이는 분명 완전히 자동화 할수 있습니다.

처음부터 위의 코드를 만들지 않고,

 

// SendPacket.idl
SendLogin([in] DWORD head,[in] string id, [in] stl::string pwd);
SendChat([in] DWORD head,[in] DWPRD avatiar, [in] strl::string message);

 

이런 식의 파일을 하나 만들어두면 누군가가 이 파일을 헤더 파일과 CPP 파일에 원래 소스를 만들어 주면 어떨까요?
SendLogin()과 SendChat()이라는 이름을 가진 함수까지 만들어서요.
그리고 수신부에도 이 두 패킷을 받아 처리하는 함수 원형을 만들어 줄수도 있겠죠, OnRecvLogin(), OnRecvChat() 같은 식으로 말입니다.

그런 역활을 해주는 것이 흔히 패킷 생성기(Packet Generator)라고 불리우는 외부 컴파일러입니다.
이에 대한 힌트는 네트워크 프로그램에서 나온 것만은 아닙니다.
CORBA, COM 같은 분산 프로그래밍에서 흔히 사용되는 방법이며 멀리서 찾을 필요없이
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\midl.exe"이 바로 이런 역할을 합니다.
물론 midl.exe은 MS의 IDL 컴파일러로 COM 프로그래밍시 IDL 파일을 빌드시켜주는 컴파일러입니다.
(IDL : Interface Definition Language)

이것을 네트워크 프로그램용으로 만드는 것이 패킷 생성기죠, 네트워크 직렬화에 이어 각 개발사의 핵심 보물 중 하나입니다.
부르는 명칭은 달라도 하는 역활은 비슷합니다, 어느 개발사에서 사용하지는 알수 없지만 ADL.exe이라는 훌륭한 컴파일러도 있죠,^--^
다행히 http://mastercho.tistory.com/9 같은 훌륭한 개발자분을 통해 제작 기법은 많이 알려져 있습니다.
최근에는 스크립트 언어를 이용한 방법도 많이 공개가 되었는데 보다 쉽게 직렬화 + 패킷 노가다 하는 일을 줄일수가 있게 되었습니다.

아래 이미지는 imays님의 국내 서버 엔진인 넷텐션 프라우드넷의 IDL 컴파일러 장착(?)모습입니다.

 

 

이런 짤 올려도 되나요? ㅎㄷㄷ;;; 수위가 너무 센것 같으니 문제 있다면 내리겠습니다.

IDL로 패킷을 정의하면 빌드시 외부 컴파일러(pidl.exe)가 헤더와 CPP를 만들어 줍니다.
Custom Build Tool 옵션은 이럴때 써먹는거죠, MFC나 ATL로 ActiveX 컨트롤을 제작하더라도 IDL에 기본적으로 midl.exe가 Custom Build Tool로 붙어있는 것을 볼수 있습니다.

(참고로 해외에선 DDoS 테스트 클라이언트툴을 패킷 생성기라 부릅니다. 이런 IDL 컴파일러를 패킷 생성기라고 부르는 것이 정확한 명칭인지는 잘 모르겠습니다.)


3. 분량 조절을 실패했습니다.
네, 실제 로비 제작은 다음 시간에 하겠습니다. (-_-);;

 

 

너무 세게 떄리진 말아주세요...


이제까지 소개한 것들을 다시 정리해 보죠.

1) 대용량서버는 IOCP, 혹은 epoll 소켓 모델을 이용한다.
2) 서버든 클라이언트든 I/O 관련은 비동기 함수를 이용한다.
3) C/S 모델이나 P2P 모델이나 listen() accept()/connect(), send()/recv(), close()의 배치로 구현할 수 있다.
4) 서버에는 반드시 DBMS가 따라다니며 서버와 DBMS의 통신 시간 역시 네트워크 수행시간에서 C/S의 통신 이상 중요한 요소이다.
5) 분산서버 아키텍쳐는 결국 객체간 통신과 객체 수명주기와 같다.
6) 실제 게임을 만들려면 STL 컨테이너같은 콜렉션을 주고 받아야 하고 이것을 직렬화(serialization)라 부르는데 몇가지 방법이 있다.
7) 보다 뽀대나고 전문적으로 할려면 패킷을 외부 파일에 정의하면 자동으로 이벤트 함수를 만들어 주는 외부 컴파일러가 있으면 참 좋다.


휴우, 여기까지 설명하니까 이제서야 백그라운드로 이해해야할 네트워크의 기초 지식이 끝났습니다.

다음 연재에 실제 로비를 만들겠습니다.
뭘로 만들까요? 과연 IDL 컴파일러를 붙여야 할까요?
...그냥 JSON 정도로 용서해 주시면 안될까요? 멀티플랫폼이 대세니까 용서해주세요.

4. 더 생각해볼꺼리

1. 각자 사용하시는 패킷 생성기 이야기나 하면서 놀아요~ ^-')b



PS) 다시 읽어보니 이미 알고 계신 분들에게는 정리 정도인데,
      모르시는 분들에게는 한도 끝도 없는 이야기...로 느껴질수도 있겠습니다.
      각 링크는 다 클릭해보시고 모르는 용어는 구글에서 검색해 보시길 바랍니다.
      직렬화 + 패킷 생성기를 직접 구현하는 것만 하더라도 서버 엔진의 절반이라 볼수 있습니다.
      또한 이 작업을 보다 멋있게 구현하는게 서버 엔진 개발자들의 끝나지 않는 로망이기도 합니다.
      경우에 따라서는 내부용(서버간 통신) 패킷과 외부용(클라이언트 연결) 패킷 생성기 두개를 만들 때도 있습니다.



내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/ )를 명시해주세요.

 

--------------------------------------------------------------------------------------------------------------------------------------------

 

  1. 이종 언어 간 바이너리 패킷 통신까지 감안한다면 (툴 등과의 통신이 필요한 경우) 아파치 thrift를 쓰는 것도 괜찮습니다. 속도도 괜찮고, 지원되는 기능도 많습니다. 페북 내에서 사용되는거라 검증도 되어 있고요.
    • Rhea君 2012.03.06 17:42    
    • 사실 직렬화 라이브러리는 참 많지요, 페북에서 쓴다니 다시 한번 봐야겠군요. 아파치 서버 프로젝트들이 괜찮은게 참 많은 것 같습니다.
  2.  swkwon04 2012.03.06 08:45      
    • Rhea君 2012.03.06 17:48    
    • 역시 여장 남자 사진이 인기가 많군요,
      강의료는 라면과 계란, 김치, 파, 햇반을 받습니다.
  3. 아침에눈뜨자마자 봤습니다. 참으로 보물같은 정보 입니다. 재밌게 잘봤습니다~^^
  4.  Foker 2012.03.06 10:03      
    • Rhea君 2012.03.06 17:47    
    • 여장에 관한 불같은 열정이군요, 압니다.
      건투를 빕니다.

      사진은 공유 부탁드립니다.

  5. 화요일 아침부터 눈에 확확 들어오는 정보네요.

    비록 머리는 따라가진...못해도

    가슴에선 불같은 열정이...하하하;;

    좋은 강좌 감사드립니다^^/
  6. Junios 2012.03.06 11:04      
    • Rhea君 2012.03.06 17:46    
    • 그래서 한번 잘 만들어두면, 절대로 바뀌지 않지요.

      그리고 개발툴은 버전업을 못하게 되는데.....

  7. 잘 보고 갑니다. 흠. 자료 왔다 갔다 편하게 하는게 언제나 귀찮아요 ㅠ.ㅠ
  8. 크로스 2012.03.06 12:07      
  9. 두번째 짤(gif)이 인상깊군요...
    마치 STL의 remove-erase 콤보
    c.erase( remove(c.begin(), c.end(), /*지울것*/), c.end() );
    를 보는듯 강력했습니다.
    이팩티브 시리즈를 잘 정리한 짤이 아닐 수 없네요...ㅋㅋ
  10. 크로스 2012.03.06 13:32      
    • Rhea君 2012.03.06 17:53    
    • 위용......?
      그게 먼가염? 저 그냥 평범한 백수여염;;

      boost::serialization 사용법은 위의 링크에 자제히 나와있지 않나요?
  11. 장소를 옮겨서 침착히 정독했습니다.
    역시 주옥같은 명짤과 설명들이... 라기보다는
    음.. 역시 현업에서 위용을 떨치시는 분의 직강이란 느낌이 팍팍 다가오네요.
    분량 조절에 실패하셨다고 하셨지만 강의의 포커스가 분명하고 실무적인데다
    디테일 할 부분은 자세히 설명해 주셔서 (일은 안하지만 그림은 잘그리셨다는 부분...ㅎ)
    무척 도움이 되었습니다.

    그리고.. 기회가 된다면 boost::serialization에 대해서 알고 싶은데..
    다음 강의에서 저것을 사용하는지요...? (적어도 사용법만이라도...ㄷㄷㄷ)
    부스트 라이브러리는 빌드 자체도 겁나서 그냥 바로바로 적용되는것 몇개만 써봤을 뿐인지라..;;
  12. 2012.03.06 22:18      
  13. 비밀댓글입니다
  14.  홍구 2012.03.08 00:23      
    • Rhea君 2012.03.09 03:46    
    • 라면과 쌀 좀...
  15. 이런글을 공짜로 보다니... 아.. 행복하다..
  16.  mastercho 2012.03.08 16:32      
    • Rhea君 2012.03.09 03:46    
    • 안녕하세요?
      실은 먼저 링크 허락을 구하고 싶었지만
      GPG쓰레드도 너무 예전 것이고
      블로그도 오래된 글이라 관리 안하시는 것 같아 부득이 그냥 링크했습니다.

      좋은 글 감사합니다.
  17. 깜작놀랬네요 몇년전에 작성하고 저 조차 잊어버린 내용을 소개해주시니 감사합니다 ^^
  18. 울레리오 2012.03.09 09:40      
    • Rhea君 2012.03.10 00:54    
    • 프라우드넷 개발자시군요,

      흐,흐응, 따, 딱히 넷X션을 위해서 올린게 아니어요,
      라이센스 기간 일년만 더 늘려달라는 것도 아님요...
  19. 우왕 저희 PIDL 컴파일러도 출연하는군요~ ㄱㅅ
  20.  mastercho 2012.03.09 12:06      
    • Rhea君 2012.03.10 00:01    
    • 네, 기대하겠습니다. ^^
  21. 이제부터라도 좀 다시 글을 쓰면서 관리를 해야겠어요 , 어째튼 좋은글 잘봤습니다
  22.  친절한티스 2012.03.10 22:52      
    • Rhea 2012.03.12 06:29    
    • 함부로 보여줄수 없당께~
  23. 메이드복 입은 레아횽 보고 싶다.

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.com/ ,http://www.gamedevforever.com/158

 

네트워크 게임 튜터리얼 5 - 드디어 직렬화

 

안녕하세요? Rhea입니다.

누군지 모르신다고요?

 

 

아...흠....그러니까..... (;゜∇゜);;;;;;;;

한때 네트워크 강좌를 적다가 잠적했던 백수 히키코모리인데요...

그것도 추가 강좌를 적겠다고 맘먹고 놀기 바빠 사라졌는데요......

 

 

으으윽 잘못했습니다. ㅠㅠ

성실하게 살겠습니다.

 

 


때리시면 맞겠습니다... ㅠㅠ

 

지난 시간 Serialize, 직렬화에 대해 언급했습니다.

그리고 직렬화를 이뤄지는 라이브러리에는 상용도 있고 공개된 버전도 있다고 하였습니다.

물론 Boost.Serialization을 잊지 말아야겠죠, 이 강좌에서 사용하는 엔진이 ASIO이고 ASIO는 Boost 답게 Boost.Serialization과 멋진 궁합을 보여줍니다.

 

1. 패킷 정의

이번에 보내볼 패킷은 다음과 같습니다. 대강 그럴싸하게 만든 엉터리 패킷입니다. (* ̄∇ ̄)/

//

// MySerializePacket.h

//
class ANS_LOGIN {
    public:
        // 로긴성공
        BOOL isOK;
    // 사용자 식별번호
    UINT uUserSerialNumber;
    // 사용자 이름
    string strName;
    // 경험치
    long lExp;
    // 레벨
    UINT uLevel;
    // 소유 장비
    map < long, long > mapEquipment;
    // 친구목록
    vector < UINT > vtGuildFriend;
}

 

이것은 로비에 접속되었다고 가정했을때, 로긴 성공 메시지로 알려주는 내용입니다. BOOK isOK로 성공했다고 알려주고, UINT uUserSerialNumber 라고 사용자 식별번호도 알려줍니다.

 

UINT uUserSerialNumber 같은 INT형 사용자 식별번호는 시중의 책에서는 거의 나오지 않습니다(아니 그전에 로긴 방법을 소개하는 게임 제작 책이 있던가요?) 예전에 만든 아바타 채팅에서는 단순하게 사용자별 Key값을 단순히 ID로 했습니다. 그러나 실제 서비스되고 있는 상용 서버(게임 이외에도) 내에서는 string 형태의 ID를 사용하지 않고 INT형으로 분류합니다.

 

string에서 INT로 바뀌면 개발자에게 상당히 편합니다. 개발자 뿐일까요, string으로 검색하지 않고 INT로 검색하니 컴퓨터에게도 빠르고 좋습니다. 당연히 DB에서도 Primary Key값은 이 UserSerialNumber 같은 INT값입니다!!! 이렇게 짤때 따라오는 코딩의 편리함에 대해서는 따로 이야기하지 않겠습니다. 여기에 그치지 않고 서비스의 성격부터 바꿀수 있습니다.

 

이것에 대한 예로는 트위터를 들까 합니다.

트위터는 표시되는  프로필이름(대화명)을 바꿀수 있습니다.

계정 메뉴에서 아이디를 바꿀수 있습니다.

그리고 무려 이메일 주소도 바꿀수 있습니다!!!

그럼 사용자 구분은 뭘로 할까요, <ID>라는 별도의 INT형(실은 LONG 정도 되겠죠?) 필드로 따로 관리합니다. 물론 사용자에겐 보여지지 않습니다.

 

 


JAWITTER = Joint Assault Windows Interface for twiTTER 라는 4달째 개발"중"인 툴입니다. 필자의 ID가 보입니다.

모든 멤버쉽 서비스는 이렇게 돌아갑니다. 이메일이나 ID를 변경하느냐 못하느냐는 기획과 사업적인 문제이지 개발적인 문제가 아닙니다.

 

다음을 보죠, 드디어 STL string이 나타났습니다. char에 담은게 아닙니다. 그리고 장비목록은 STL map에 담았고 친구리스트는 STL vector에 담았습니다.

과연 제대로 날라갈수 있을까요?

 

2. Boost.Archive

Boost.Serailize의 컨셉은 Boost.Archive란 유틸리티 클래스에 기반합니다.

Archive는 직렬화를 하는 text_oarchive 클래스와 풀어주는 text_iarchive 로 구분됩니다.

이건 코드를 보는 편이 훨씬더 빠릅니다.

 

#include <boost/archive/text_oarchive.hpp>

#include <boost/archive/text_iarchive.hpp>

#include <iostream>

#include <fstream>


void save() {
    std::ofstream file("archiv.txt");
    boost::archive::text_oarchive oa(file);
    int i = 1;
    oa << i;
}

void load() {
    std::ifstream file("archiv.txt");
    boost::archive::text_iarchive ia(file);
    int i = 0;
    ia >> i;
    std::cout << i << std::endl;
}

int main() {
    save();
    load();
}

 

간단한 파일 세이브/로드지만 막강합니다. << 로 파일 스트림으로 쓰고 >>로 파일 스트림에서 읽어왔습니다.

이제는 일반 변수가 아닌 클래스를 갖고 놀아보죠. 왜냐면 패킷도 클래스이기 때문입니다.

 

#include <boost/archive/text_oarchive.hpp>

#include <boost/archive/text_iarchive.hpp>

#include <iostream>

#include <sstream>

std::stringstream ss;

class person {
    public:
        person() {}

    person(int age): age_(age) {}

    int age() const {
        return age_;
    }

    private:
        friend class boost::serialization::access;

    template < typename Archive >
        void serialize(Archive & ar,
            const unsigned int version) {
            ar & age_;
        }

    int age_;
};

void save() {
    boost::archive::text_oarchive oa(ss);
    person p(31);
    oa << p;
}

void load() {
    boost::archive::text_iarchive ia(ss);
    person p;
    ia >> p;
    std::cout << p.age() << std::endl;
}

int main() {
    save();
    load();
}

 

person이라는 클래스를 직렬화했습니다!!

이것을 해주는 것이 Boost안의 serailization.hpp의 인라인함수인 serialize()입니다.

 

//
//  serailization.hpp
//

// default implementation - call the member function "serialize"
template < class Archive, class T >
    inline void serialize(
        Archive & ar, T & t,
        const BOOST_PFTO unsigned int file_version
    ) {
        access::serialize(ar, t, static_cast < unsigned int > (file_version));
    }

 

이를 위해서는 person안에 serialize()를 오버라이딩해줘야 합니다.

이 모습은 우리에게 친숙한 MFC에서도 발견할수 있습니다. MFC의 맨상위 클래스인 CObject를 보시면 Serialize()이란 가상함수가 존재하고 있음을 발견하게 됩니다.

 

//
// afx.h
// 

virtual void Serialize(CArchive & ar);

 

이는 MFC에서 상당히 중요한 의미를 가지는데 MFC의 모든 클래스에서 직렬화를 구현할수 있다는 의미입니다.

Boost에서는 따로 선언을 해줘야 합니다. 위에서는 friend class boost::serialization::access;로 끌어댕겼습니다.

 

여기서 잠깐!

 

참고로 제가 이렇게 가끔 MFC로 설명하는 경우가 있지만 MFC광팬은 아닙니다.

지금보면 MFC는 Native C++과 생소한 Windows 자료형, 그리고 리소스가 당시의 부족했던 기술로 조합하거나 너무 큰 기술로 결합되어 있고 지금도 그 Legacy가 제대로 걷혀지지도 못했습니다.

누군가 제 블로그를 MFC 예제 사이트라고 오래 전에 멋대로 적은 바람에 그 이후엔 블로그에서는 MFC 사용도 안하고 있습니다.

 

다만 MFC은 무려 1992년에 나온 거의 최초의 상업용 C++ 클래스이자 프레임워크입니다(볼랜드는 OWL).

그안에 들어간 개념과 철학들은 C++의 역사라고 봐도 무방합니다.

그리고 후대의 많은 프레임워크들에게 엄청난 영향을 주었습니다. 아이폰 앱을 만드는 Cocoa 프레임워크도 MFC의 영향, 엄~~~~~~~~청나게 받았습니다.

 

제가 말씀드리고 싶은 것은 바로 코앞에 있는 MFC의 개념을 잘 이해하면 자신만의 프레임워크를 만들기도 편하고 다른 프레임워크를 이해하기도 무척 편하다는 말입니다.

 


MFC가 없었으면 C++도 없었어!!

그러니 광팬은 아니지만 MFC 너무 까지마염 ㅠㅠ 조만간 예제 코드 WTL로 바꿀꺼임.................무슨 말을 할려고 박스까지 쳤지는 까묵!!!

 

 

3. Asio.Serialization

직렬화의 비밀은 Archive에 있다고 알려졌습니다. 이제는 Archive를 ASIO, 즉 소켓으로 보내볼 차례입니다.

뜬금없지만 평소 적고 싶었던 말

 

 

 


코딩에 앞서 뿌니뿌니~♡

 

먼저 패킷 클래스에 직렬화 함수를 추가합니다.

 

//
// MySerializePacket.h
//
class ANS_LOGIN {
    public:
        // 로긴성공
        BOOL isOK;
    // 사용자 식별번호
    UINT uUserSerialNumber;
    // 사용자 이름
    string strName;
    // 경험치
    long lExp;
    // 레벨
    UINT uLevel;
    // 소유 장비
    map < long, long > mapEquipment;
    // 친구목록
    vector < UINT > vtGuildFriend;

    template < typename Archive >
        void serialize(Archive & ar,
            const unsigned int version) {
            ar & isOK;
            ar & uUserSerialNumber;
            ar & strName;
            ar & lExp;
            ar & uLevel;
            ar & mapEquipment;
            ar & vtGuildFriend;
        }
};

 

후훗, 추가되었습니다.

 

다음은 핵심인 직렬화 과정입니다.

이번 회의 소스는 ASIO 예제사이트인 http://www.boost.org/doc/libs/1_48_0/doc/html/boost_asio/examples.html 에 있는 Serialization 항목에서 가져왔습니다. 그런데 해당 예제는 서버에 Session 클래스가 없습니다. 실제 세션을 열어 데이터를 주고 받을 수 없기 때문에 http://www.boost.org/doc/libs/1_48_0/doc/html/boost_asio/example/serialization/connection.hpp 을 가져와 CSerializeEngine이라는 이름을 붙였습니다. 무려 클래스명에 Engine씩이나 붙인 이유는 이 정도 작업을 해주는 클래스는 정말로 서버 Engine 레이어이기 때문입니다.

다음은 실제 패킷을 직렬화하고 소켓으로 보내는 부분입니다.

 

/// Asynchronously write a data structure to the socket.
template < typename T, typename Handler >
    void async_write(const T & t, Handler handler) {
        // Serialize the data first so we know how large it is.
        std::ostringstream archive_stream;
        boost::archive::text_oarchive archive(archive_stream);
        archive << t;
        outbound_data_ = archive_stream.str();

        // Format the header.
        std::ostringstream header_stream;
        header_stream << std::setw(header_length) <<
            std::hex << outbound_data_.size();
        if (!header_stream || header_stream.str().size() != header_length) {
            // Something went wrong, inform the caller.
            boost::system::error_code error(boost::asio::error::invalid_argument);
            socket_.get_io_service().post(boost::bind(handler, error));
            return;
        }
        outbound_header_ = header_stream.str();

        // Write the serialized data to the socket. We use "gather-write" to send
        // both the header and the data in a single write operation.
        std::vector < boost::asio::const_buffer > buffers;
        buffers.push_back(boost::asio::buffer(outbound_header_));
        buffers.push_back(boost::asio::buffer(outbound_data_));
        boost::asio::async_write(socket_, buffers, handler);
    }

 

이제까지와 마찬가지로 앞부분에 헤더를 붙이는 과정을 일단 생략하면 Boost.Serialzation 예제와 똑같습니다.

직렬화로 보내고 받는 함수는 Session 클래스에 있던 함수 대신 CSerializeEngine에 있는 함수들을 써야 합니다.

그래서 실제로 이 부분은 서버의 Engine 레이어라는 것입니다.

 

ANS_LOGIN 패킷을 꼭꼭 채워봅시다.

 

//
//  RheaGameSession.cpp
// 
ANS_LOGIN ansLogin;

ansLogin.isOK = TRUE;
ansLogin.uUserSerialNumber = 10001;
ansLogin.strName = _T("레아스트라이크");
ansLogin.lExp = 68000 L;
ansLogin.uLevel = 99;

ansLogin.mapEquipment.insert(Long_Pair(1, 101));
ansLogin.mapEquipment.insert(Long_Pair(2, 102));
ansLogin.mapEquipment.insert(Long_Pair(3, 103));
ansLogin.mapEquipment.insert(Long_Pair(4, 104));
ansLogin.mapEquipment.insert(Long_Pair(5, 105));

ansLogin.vtGuildFriend.push_back(10011);
ansLogin.vtGuildFriend.push_back(10012);
ansLogin.vtGuildFriend.push_back(10013);
ansLogin.vtGuildFriend.push_back(10014);
ansLogin.vtGuildFriend.push_back(10015);

AnsLoginVector.push_back(ansLogin);

m_connection.async_write(AnsLoginVector, boost::bind( & CRheaGameSession::handle_write_serialization, 
this, boost::asio::placeholders::error));

 

string은 변환없이 string으로 채웠고 map과 vector에도 데이터를 넣었습니다.

앞서 말한대로 CSerializeEngine::async_write()를 통해 데이터를 보냅니다. ASIO에서는 데이터를 보낼때 기본적으로 vector에 담아 보냅니다. 이는 대단히 편리한데 만약 여러사용자에 대한 데이터를 보낸다면 그대로 vector에 담아 보낼수 있겠죠.

 

STL 컨테이너들의 실제 Archive는 여러 파일에 나눠져 있습니다. \boost\boost_1_47\boost\serialization 폴더에 보시면 보낼수 있는 컨테이너들이 들어있습니다. 정말이지 이런 것을 공짜로 작업하신 훌륭하신 분들에게 감사의 말씀을 드립니다.

 

 

 

실제로 데이터가 갈까요?

결과를 확인해보죠.

 

//
// ClientSocket.h
// 
void handle_read(const boost::system::error_code & e) {
    if (!e) {
        // Print out the data that was received.
        for (std::size_t i = 0; i < AnsLoginVector.size(); ++i) {
            BOOL bIsOK = AnsLoginVector[i].isOK;
            UINT uUSN = AnsLoginVector[i].uUserSerialNumber;
            string strName = AnsLoginVector[i].strName;
            int iEquipmentSize = AnsLoginVector[i].mapEquipment.size();
            int iFriendsSize = AnsLoginVector[i].vtGuildFriend.size();
        }
    } else {
        // An error occurred.
        e.message();
        OnClose();
    }
}

 

귀찮습니다, 그냥 Watch창으로 확인해보죠!

 

 

 

 

 

 

 

넵, 그대로 날라왔습니다. type값 역시 그대롭니다. 클라이언트에도 같은 클래스로 Archive하여 그대로 나왔습니다만,

마치 마법과도 같습니다.

 

우리는 string과 map과 vector를 그대로 소켓에도 쏘고 그대로 받은 것입니다!!!!!

 

4. 직렬화가 가져온 것

지난 강좌에도 말씀드렸지만 이런 네트워크 직렬화는 결코 쉬운게 아니었습니다. 엄청난 노가다의 결실입니다. MFC의 CSocket도 물론 네트워크 직렬화를 해줍니다만, WSAAsyncSelect 모델이라 서버로 사용할만한 것은 아니었습니다.

그러나 아직까지 개선해볼 사항이 있습니다. 역시 지난 강좌에 소개한 IDL 컴파일러 같은 것이죠.

이 과정들이 자동화 될 부분이 있습니다.

 

1) 빌드시 패킷 클래스에 자동으로 직렬화 함수 넣어주기
보셨겠지만 직렬화 함수는 단순합니다. 패킷 클래스의 멤버들을 파싱하여 자동으로 직렬화 함수를 만들수 있지 않을까요?
혹은 애시당초 별도의 자신만의 스크립트 형태로 만들어 빌드가능한 클래스로 자동 생성시키는 방법이 있습니다.


2) 각 패킷별 데이터 수신 함수를 자동으로 만들어주기
1)에 연장하여 각 패킷 이름을 파싱해 수신 핸들러 함수를 만드는 것입니다.
OnAnsLogin() 식으로 만들수 있을 것입니다.

 

이는 결코 어렵지 않습니다, 힌트 다 드렸잖아요.

 

그리고 우리는 여기서 아주 중요한 아키텍트를 하나 추리해 낼수 있습니다.

게임에 사용되는 사용자 클래스를 그대로 네트워크로 내보낼 수 있으니 게임용 클래스와 네트워크 패킷을 따로 만들지 않아도 된다는 것과

독립된 서버 I/O 모듈과 게임 엔진이 잘 작동한다면 클라이언트 개발자, 혹은 서버 개발자가 아닌 컨텐츠 개발자가 혼자 게임 로직을 만들수 있게 된다는 점입니다.

이는 생산 측면에서 아주 유용합니다. 이런 모델을 추천합니다. 하지만 제가 이 컨텐츠 개발자라면 클라이언트와 서버 코드, 둘다 제것으로 만들고 공부할 것입니다. 이런 개발 모델은 편한 작업 환경에서 팀생산성을 위한 것이지 개발자에게 서로 독립된 레이어니까 전혀 몰라도 된다~라는 의미는 아니라고 생각합니다.

 

5. 더 생각해볼 꺼리

평소에 자신의 소스는 항상 너무 빨리 빌드가 되어 불만이셨던 분 계십니까?

상용 게임 빌드는 몇시간씩 걸린다는데 나는 언제 그런 빌드 타임 걸려보냐라구요?

 

그런 고민, 이번 강좌를 통해 말끔히 해결됩니다,

아마 Boost.Serialize가 추가된 순간부터 눈에 띄는 빌드 속력 저하를 느끼셨을 것입니다.

이 짧은 소스도 앗! 하는 느낌이 올껍니다!

 

갑자기 빌드가 느려진 이유를 찾아보세요.

그리고 어떻게 하면 극복할 것인지 생각해보시길 바랍니다.

 

직렬화 관련 참고자료 : http://en.highscore.de/cpp/boost/serialization.html

 

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/)를 명시해주세요.

 

  1. 우왕~~ 잘 읽었습니다. boost 로 직렬화해서 네트워크로 깔끔하게 전송~ 멋지네요~

    boost 를 거의 안 써봐서 ar & age__; 이렇게 & operator 를 쓰는건, 저에게 무척 낯설긴하네요;;;
  2. 엑스터 2012.04.19 10:23      
  3. 예전에 이런 걸 만들겠다고 삽질한 기억이 나는군요.
    확실히 만들자면 어설프게라도 만들 수 있겠지만....
    이미 있는 것 쓰면 이렇게 편한 것을..
  4. 크로스 2012.04.20 20:26      
  5. BOOK isOK

    책은 좋은 법이지요..ㅎㅎ
    강좌는 조금 시간날때 천천히 정독하겠습니다!
  6.  ozlael 2012.04.21 01:58      
  7. 레아옹의글은 본문도 본문이지만 짤이 흥함 ㅎㅎ
  8. testtt 2014.05.28 00:16      
  9. 패킷제너레이터로 다 만들었었는데
    이런 방법이 있다니 정말 놀라웠습니다ㅎㅎ
    감사감사

    뿌니뿌니 짤도 매우 감사합니다

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.com/ ,http://www.gamedevforever.com/210

 

[네트워크 게임 튜터리얼 번외편] C/C++로 Socket.IO에게 돌직구를 던져보았다.

 

안녕하세요?

몇차례나 펑크를 내고 있는 비인기 포스트 == 네트워크 게임 튜터리얼의 백수 히키코모리 변태, 요망한 김레아입니다.

적고 있던 "네트워크 게임 튜터리얼 6 - 유저풀"편이 아직 완성이 덜되었기 때문에 부득이하게 번외편이 나갑니다.

 

원래 게임데브포에버용 글이 아니었기 때문에 평소에 달리 존댓말 대신 반말이며 읽는 재미도 떨어질 것입니다만,

요즘 아는 분들 사이에 뜨고 있는 WebSocket, node.js, Socket.IO에 대한 C/C++ 프로토콜 분석기입니다.

 

이게 무엇인지 이해 가지 않고 게임이랑 무슨 상관있는지 멍~하실 분들이 많으실텐데요,

분명 상관있고 조만간 판도를 뒤짚을 기술입니다.

웹프로그래머분들의 시선이 아닌 Native 개발자의 시선이므로 도움이 되실꺼라 믿으며,

다음 본강을 기대해주시길 바랍니다.

 

 

 

 

반도의 돌직구녀. 이런게 바로 제대로 "배운 녀자"지.


돌직구녀에게 감동을 받아 돌직구 포스트 한번 적어본다.
제목처럼 C/C++로 Socket.IO에게 돌직구 던지는 법이다. Socket.IO가 뭔지, 게임이랑 무슨 상관인지 어쩌구 저쩌구하는 머 그런 대딩같은 질문들은 일체 사절하도록 한다. 나중에 유료 세미나에서 돈내고 듣길 바란다.
돌직구는 언제나처럼 Socket으로 던진며, 당연히 BSD든 Winsock이건 똑같다.
아, Socket.IO 버전은 v.9.

1. 일단 주워보자.
있는 것을 다시 만들 필요없다, Socket.IO 홈페이지(http://socket.io/)와 github(https://github.com/)를 뒤졌다.
우선 홈페이지에는 얼랭, 안드레기, 자바, 루아, 오브젝레기-C, 펄, 고, 피똥, 루비, 플레기, 다 있는데......C++만 없었다. -_-
github와 구글느님께 도움을 요청했다.

 

 

아크데몬이 지저분한 소스에 기분이 상해버렸다.


딱 하나 나왔다.
요즘은 오픈소스도 참 잘 만들고 정리가 잘 되어 있는데 이건 마치 90년대 슬랙웨어를 보는 듯한
푸세식 화장실에서 건져올린 느낌의 C++ 프로젝트가 하나 나왔다.
물론 빌드도 제대로 안되고 에러처리로 제대로 안되어 있어 도저히 사용할 수 없었다. 
이런거나 만들라고 공개된 Boost.Asio가 아닐텐데... .
간만에 코딩을 해야할 운명에 처했다.

2. RFC 6455
그래서 다른 언어로 된 Socket.IO 클라이언트를 읽고 C/C++로 포팅할려고 했다.
대부분 이미 구현된 WebSocket 클래스 위에 Socket.IO용 JSON 프로토콜을 올린 식이었다.
하지만 문제는 WebSocket이 이미 Fix된 프로토콜이 아니란 점이다.
소스들이 제각각 달랐다. 이래저래 별 도움이 안됐다. 역시 이 넘들에게 기대한 내가 바보였어.

할수 없이 RFC 6455(http://tools.ietf.org/html/rfc6455) 문서와 Socket.IO 문서(https://github.com/learnboost/socket.io-spec)를 읽었다.
오오미, 신기술답게 "ws://"도 등장한다.
하지만 우리들은 http든 ws든 그게 그거라는 것을 잘 알고 있다. GET이나 POST냐 PUT이냐 그런게 문제지.

 

 

이보다 더 자세한 설명이 있었는데 찾질 못하겠다.


하지만 단순히 WebSocket을 구현하는 것만으로는 안된다.
Socket.IO에게 WebSocket으로 접속하는 것 이전에 Socket.IO의 프로토콜의 알아야 하기 때문이다.

3. 뜯기

결국 직접 구현하기로 맘을 먹었다.
나에게 도움을 주실 분은 Fiddler2(http://www.fiddler2.com/fiddler2/)와 TCPView.
Fiddler2는 웹개발자를 위해 탄생한 툴이나 웹개발자보단 내 경험상, 항상 C/C++ 클라이언트 개발자들이 써왔다.

 

 

하악하악 섹시한 바이올린으로 갈것 같다.


서버 소스는 구글에서 가장 먼저 검색된 http://psitsmike.com/2011/09/node-js-and-socket-io-chat-tutorial/ 를 쓰기로 했다.
블로그가 분홍색으로 알록달록하지만 분명 블로그 주인은 넷카마남자일꺼다.
......트위터 털어보니(사실은 링크) 남자분 맞으시네, 포스트 다 적고 인사하고 팔롱해드려야지.
아뭏든 우리의 피들러2느님께서 딱 잡아주신다.

 


암튼 뒤에 체크한 두개가 소켓으로 구현해야하는 부분이다.
마지막 접속 부분이 중요한데 만약 WebSocket을 지원하지 못하는 브라우저라면,
"/socket.io/1/xhr-polling/15479987321848421359?t=1339783678755" 식으로 XMLHTTPRequest로 접속할 것이다.
이는 실제 소켓 연결이 아니라 계속 요청을 보내 데이터를 가져온다는 뜻이다.
스마트 디바이스에 이런 연결을 하면 이통사에서 존나 싫어할 것 같다.ㅋㅋㅋㅋ

결국 웹브라우저가 아닌 소켓의 입장에서는

1) 접속
2) 클라이언트에서 만든 Timestamp용 랜덤키값 전송
3) Session ID와 사용가능한 Transport ID 받음
4) websocket 혹은 XMLHTTPRequest 방식으로 가져올 것인지 선택
5) XMLHTTPRequst 라면 계속 접속을 하여 데이터 획득
6) WebSocket이라면 실제 전송을 위해 새 Transport 연결 획득
7) 지정된 시간만큼 핸드쉐이킹
8) Socket.IO에서 지정된 JSON 프로토콜대로 데이터 주고받기

이다. 랜덤키를 클라이언트에서 만드는게 독특하다. 그리고 랜덤키가 없어도 세션ID는 돌아온다.
세션 ID는 Socket.IO 모듈의 manager.js의 691 라인의 이 부분이라 추측된다.
그냥 현재 시각을 이용한 랜덤이다.

 

/**
 * Generates a session id.
 *
 * @api private
 */
Manager.prototype.generateId = function() {
    return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString() +
        Math.abs(Math.random() * Math.random() * Date.now() | 0).toString();
};

 

문제는 헤더에 실수가 있을 경우 에러값도 전혀 없이 접속을 종료한다. 즉 접속하고 아무 데이터나 보내면 그냥 close시켜버린다.
처음 이것 때문에 고생 좀 했다.

4. 구현
4.1 접속 및 세션 ID얻기
웹브라우저가 아닌 Winsock으로 하니 피들러2가 보여준 HTTP 요청들이 대거 축소되었다.
몇번이나 예제로 나간 MFCClient.sln을 고쳐 HTTP 요청을 보내도록 하였다.
주소 이후 URL은 "socket.io/1/?t=타임스탬프(랜덤)13자리&jsonp=0" 이다.

CString strHead = "GET " + strFileURL + " HTTP/1.1\n\n";
m_pClientSocket - > Send(strHead, strHead.GetLength(), 0);

참고로 1.1로 요청하면 HTTP 연결형으로 접속이 된다.


1.0으로 요청하면 응답 후 소켓이 끊어지는 것을 눈으로 확인할 수 있다(직접 확인해보길 바란다.)
대부분이 잘못 알고 있는 것과 달리 현재 HTTP는 매회 요청시 Connection 요청을 하지 않는다.
따라서 WebSocket, 아니 서버에서 클라이언트로 메시지를 뿌리는 것도 가능한 것이다.
사실 WebSocket 자체는 결코 신기한 기술이 아니다.
HTTP 1.1부터는 1.0과 달리 연결이 항상 맺어져있다고 생각하면 무엇이든 이해가 될 것이다. 끊어지면 다시 연결하면 되는거고.

 

 

Connection: Keep-Alive에 주목! 그리고 세션 ID를 얻어왔다.


세션 ID(149821928277990092)를 얻어왔다.

세션 ID와 함께 날라오는 것은 Transport ID로 websocket, htmlfile, xhr-polling, jsonp-polling 이란 메시지이다.
이것은 세션 ID만큼 중요한데 이 클라이언트가 구현할수 있고, 서버가 응답가능한 통신 프로토콜들 리스트다.
즉, 이 포스트의 핵심인 websocket도 쓸수 있고, 계속 HTTP 요청으로 무식하게 htmlfile을 요청할수도 있고,
XMLHTTPRequest나 JSON Padding으로 구현할수도 있다는 이야기다.
물론 이것은 Socket.IO에서 설정한 대로 나오게 된다.

var serverSocket = socketio.listen(server);
serverSocket.set('transports', ['websocket', 'flashsocket', 'xhr-polling']);

 

이런 식으로 말이다.
이게 바로 Socket.IO의 역활이다. 어떤 웹브라우저라도 연결된 것처럼 써먹을 수 있게 해주겠다는 것이다.

물론 우리는 다른 것 다 필요없다, 웹브라우저가 아니니까 오직 WebSocket만 쓰면 된다.
위에도 적었지만 어떤 이유로 폴링 방식을 쓰겠다면 서버에도 xhr-polling 을 선언해주고 클라이언트는 "/socket.io/1/xhr-polling/세션ID?t=아까 정한 랜덤키"로 요청을 계속하면 된다.

4.2 Transport connection
드디어 HTTP가 아닌 "ws://" 프로토콜을 구현할 시간이다.
앞서 말한 것처럼 그래봤자 GET으로 요청하는 것에 불과하다.
위키피디아(http://en.wikipedia.org/wiki/WebSocket)에서 가져온 헤더들을 적자면,
요청시

 

GET /mychat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://example.com

 

로 보내면 서버는

 

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

 

라고 보내준다고 한다.
핸드쉐이킹을 위해 클라이언트에서 랜덤하게 생성한 Sec-WebSocket-Key가 어떤 알고리즘으로 Sec-WebSocket-Accept로 바뀌는지는 역시 RFC 6455 에 친절히 설명되어 있다. WebSocket을 위한 GUID인 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"붙여 SHA-1과 Base64로 지지고 볶는데 서버를 만들 것이 아니라면 큰 관심을 가질 필요는 없을 것 같다.
다만 클라이언트에서 정하는 Sec-WebSocket-Key는 16자리 랜덤키를 Base64로 뽑아낸 것임은 기억할 필요가 있다.

테스트 삼아 대충 아래와 같이 헤더를 만들어 보냈다.
소켓은 더 만들 필요없고 첫 HTTP 접속에 사용한 소켓을 그대로 사용하면 된다.

 

CString strSessionID;
GetDlgItem(IDC_ADDRESS2)->GetWindowText(strSessionID);

CString strHead2;
strHead2 = "Upgrade: websocket\n"
"Connection: Upgrade\n"
"Host: localhost:8080\n"
"Origin: http://127.0.0.1:8080\n" 
"Sec-WebSocket-Key: MTIzNDU2Nzg5MDEyMzQ1Ng==\n" 
"Sec-WebSocket-Version: 13\n\n"
"Sec-WebSocket-Extensions: x-webkit-deflate-frame\n";

CString strHead = "GET /socket.io/1/websocket/" + strSessionID + " HTTP/1.1\n" + strHead2; 
TRACE ("%s\n", strHead);

m_pClientSocket->Send(strHead, strHead.GetLength(), 0);

 

거지같은 소스지만 연결은 훌륭하게 잘된다.
문자열 파싱 좀 붙여 주소랑 포트, 그리고 랜덤하게 Sec-WebSocket-Key만 정해주면 훌륭해질 것이다.

 

 

WebSocket 연결 성공


4.3 Socket.IO Data Frame(혹은 Framing)
정말로 채팅이 될까? 기존 웹채팅이랑 통신해보자.

서버와 접속이 된 상태이니 당연히 메시지를 받는다.

 

 

폴링따윈 껒!!


이제 데이터를 보내보자, 진정한 돌직구다.
결론적으로 Socket.IO에서 설명하고 있는 JSON 형식을 그대로 보내서는 안된다. 또 연결이 끊길 것이다.
그러나 WebSocket이 연결된 이후 데이터 전송에 대해서는 Fidder2로도 잡을수 없다.
왜냐면 WebSocket으로 Switching이 된 이후부터는 HTTP 가 아니라 일반 소켓 접속이기 때문이다.
크롬 + 외부 서버 + WireShark를 돌려봤지만 이렇다 할 답은 찾지 못했다. 크롬이 일반 binary로 통신하기에 데이터 모습도 모른체 binary를 뜯기는 너무나 힘이 든다.

Switching Protocol을 받은 이후(WebSocket으로 구현된 후), 패킷 데이터 구조체는 역시 RFC 6455 문서에 적혀있다.

 

 

WebSocket Data Framing


 

 

 

아... 솔까 이런 그림만 보고 코딩하라면 나도 당황스럽다...


이것을 C/C++로 옮겨보면,

 

string payload = "Socket.IO에 정의된 JSON 문자열";
payload = AnsiToUTF8(payload); // UTF-8로 보낸다.

char frame[131];
frame[0] = '\x81';
frame[1] = 128 + payload.length();
frame[2] = '\x00';
frame[3] = '\x00';
frame[4] = '\x00';
frame[5] = '\x00';

_snprintf(frame + 6, 125, "%s", payload.c_str());

int iRet = m_pClientSocket - > Send( & frame, payload.length() + 6, 0);

 

이런 형태가 된다.
아래 짤이 구현된 모습이다. C/C++소켓에서 보낸 "Hi, Hi"란 메시지가 웹브라우저에게 나타나고 있다.

 

 

돌직구 완성.


아쉽게도 WebSocket은 한번에 최대 125byte밖에 보내지 못한다.
가장 앞의 "0x81"은 이 패킷이 단일 패킷(single-frame)이라고 상대에게 알려주는 것이다.
따라서 125 이상되는 내용을 보내고자 한다면 Socket.IO에게 보낼 JSON 데이터(상기 소스에서 string payload)를 길이에 맞게 나눠 가공하는 작업이 필요하며, 첫 패킷의 앞부분은 "0x01", 두번째 패킷부터는 "0x80"로 보내야 한다.
binary 데이터를 보낼려면 "0x82"를 붙여야 하지만 node.js의 속력을 감안하면 binary는 IIS같은 웹서버를 이용하는게 좋을 것 같다.

close()쪽은 특별히 신경쓸게 없는 것 같다. 그냥 close() 호출해줘도 node.js나 Socket.IO에서 에러는 나지 않았다.

5. 결론
이 포스팅을 하는 현재, 그 어느 소스도 맘에 들게 정상적으로 작동하는 것을 보지 못했다.
오픈 소스 대부분이 외국 학생들의 숙제였던 경우가 많고 WebSocket 프로토콜과 Data-Framing 규약이 그간 엄청나게 빠른 속도로 바뀌었었기 때문이다.

 

 

지난 일주일...


나 역시도 인터넷 검색하며 오래전 규약의 문서(현재 Socket.IO v9이 사용하는 WebSocket에 맞지 않는)들을 보며 수많은 함정에 빠졌었다.
마지막엔 WebSocket이 아닌 Socket.IO의 변태같은 JSON 프로토콜에 이틀밤을 낭비했다.
조금더 정리해서 나두 github같은데 올려볼까 한다.

전부 완성하고 보면 언제나 그렇듯 별 게 없다.
WebSocket 프로토콜과 Socket.IO도 또 언제 바뀔진 모른다.
그러나 힘들게 한번 구현하고 나니 앞으로 바뀔 모습도 예상되고 빨리 적용할 것 같다.
가장 중요한 것은 웹브라우저로 서비스할 것이 아니니 버전을 Fix 시켜놓고 그것만으로 운영해도 무리는 없을 것 같다.
이 엉망인 문서도 혹시 WebSocket을 쓰고자하는 진취적인 개발자들에게 작은 도움이 되길 바란다.
아무것도 아니지만, 나름 일주일 고생했다. 레알.

 

 

분석 마쳤으니 나는 다시 잉여질이나 해야지~

 

 

 

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/ )를 명시해주세요.

 

저는 백수라서 잘 모르지만, 어떤 곳에서 이런 재미있는 연구들을 바탕으로 빅재미 넘치는 게임을 만들꺼라고 하오니,

관심있으신 분은 트위터 @RheaStrke_DEV 로 연락바랍니다. 제가 주선해드림.

 

-----------------------------------------------------------------------------------------------------------------------------------------------

 

  1. 아... 솔까 이런 그림만 보고 코딩하라면 나도 당황스럽다...
    내용이 너무 워프해서 벙쪘는데 이 부분부턴 좀 안심이 되네요...ㅎㅎ
    이번엔 번외편이군요?? 잘봤습니다.
  2.  끼로 2012.06.21 19:01      
  3. "어떤 곳에서 이런 재미있는 연구들을 바탕으로 빅재미 넘치는 게임을 만들꺼라고 하오니,
    관심있으신 분은 트위터 @RheaStrke_DEV 로 연락바랍니다. 제가 주선해드림." 이게 포인트네요!! 우와!! 어딘지 모르겠지만 정말 좋은 곳일것 같아요!! 저도 거기로 이직하고 싶네요!
  4. Berdo 2015.03.18 15:34      
  5. 안녕하세요..

    포스팅 보고 적용 해보고 있는데요. 

    웹소켓까지 연결을 하고 나면 서버에서 연결을 끊는데.. 이게 정상인건가요?
  6. itselfer 2015.11.05 18:12      
  7. socket.io에서 websocket을 따로 소스에서 설정해줘야 MFC와 만날수 있는건가요....
    허어... socket.io랑 MFC를 붙이는게 목적인데 이게 뜻대로 잘 되지 않네요.ㅠㅠㅠ

    참고할수 있는 소스나 도움이 좀 있을까요...? 혹시 github에 올리셨다면 링크 부탁드립니다 ㅠㅠ

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.com/ ,http://www.gamedevforever.com/237

 

네트워크 게임 튜터리얼 2기 - 컨텐츠편 1화

 

안녕하세요?
정말이지 5개월만에 네트워크 게임 튜터리얼 연재를 다시 시작합니다.

비 한방울 내리지 않던 무더운 여름, 헬파이어 미사일을 맞은 듯한 시간을 보내다 부활했습니다.
기나긴 슬럼프를 극복시켜준 것은, 소드 아트 온라인 덕분입니다.

덕분에 이런 주말 http://twitter.com/RheaStrike/status/247300021152788480/photo/1 을 보냈지만 이게 다 네크로먼서질 당하는 과정임.

 

 

얼떨결에 나를 부활시켜준 아스나


먼저 지난 1기(멋대로 1기 타령... 한 쿨도 아닌 주제에ㅠㅠ)에 배운 것을 정리해볼까요?

1화 : 떡밥 투척
2화 : C/S와 P2P의 간략한 구조
2-1화 : 홀펀칭 좀 적다가 생략. 본진에도 업로드 안함.
3화 : ADO를 이용한 데이터베이스 연결 및 로긴
4화 : 로비 - 예제없음. 크윽
5화 : boost.serialize를 이용한 직렬화. 사실 5화를 보면 3, 4화를 바탕으로 셀프 제작 가능해야함.


돌아보니 주로 서버 엔진측에 대한 필수+기초만을 다룬 것 같습니다.
사실 연재를 못한 기간동안에도 대략 이런 일들과 플랫폼 서버쪽을 만들고 놀고 있었습니다.
실전에서도 언제나 이런 밑밥투척부터 잘 되어야 튼튼한 프로젝트가 되기 때문입니다.

그래서 연재방향도 자칫 미들웨어(middle ware) 제작 쪽으로 흘러갈뻔 하다가...
네, 마음대로 지난 1기를 종결하고 새롭게 2기를 시작해봅니다.
2기 때부터는 본격 컨텐츠 제작 이야기를 해보죠, 물론 제목대로 네트워크는 필수입니다.
이 모든게 소드 아트 온라인 때문입니다.

저두 서버측 밑밥 엔진 이야기만 하다보니 질린게 사실이고요.

그럼 2기도 떡밥 투척부터 시작하겠습니다.

1. 보통 프로그램팀의 인력구성은?
먼저 말씀드리고 싶은게 흔히 말하는 "프로그램팀"의 인력구성입니다.
라이브를 하지 않고 신규 개발만 하는 팀이라면 사실 10여명이면 왠만한 서버와 클라이언트등 게임에 필요한 애플리케이션들을 뽑아냅니다.
또한 실력은 보통 10명 중 3~4명은 고수, 5명은 중수, 1~2명은 뉴비 정도로 이뤄집니다.
제가 감히 실력을 구분짓는건 무언가 아니겠지만 보통 이렇게 이뤄지는게 사실입니다.

그리고 이 10명은 어떻게 일을 할까요?
10명이 개떼처럼 서버를 뚝딱 만들고 클라이언트를 뚝따 만들지는 않을 것입니다.
보통 서버파트와 클라이언트 파트로 나눠지며 혹은 서버 파트, 컨텐츠 파트, 클라이언트 파트로 구성되기도 합니다.
(참고 : http://rhea.pe.kr/283)

흔히 말하는, 게임을 공부하는 분들이 가고 싶어하는 분야가 클라이언트 파트라고 생각합니다.
시중에 나와있는 책들의 대부분이 렌더러를 포함하여 클라이언트 기술들이죠.
그러나 실제 렌더러만 전담하는 개발자는 이 10명중 1, 2명이거나 아예 엔진팀이 별도로 있는 경우가 많습니다.
따라서 클라이언트 개발자는 절대 렌더러 개발자가 아닙니다.
서버 역시 마찬가지입니다. 이 10명 중, 제대로 소켓을 다룰수 있는 개발자는 2, 3명만 되어도 어떻게든 팀은 돌아갑니다.
그렇다면 렌더러도 아니고 서버도 아닌 5명은 무엇을 할까요?

가장 많은 수를 차지하는 것은 컨텐츠 개발자입니다.
라이브팀에는 90%가 컨텐츠 개발자로 구성된다고 봐도 무방할 정도입니다.

물론 가장 좋은 상황은 이 10명이 전부 렌더러, Win32 애플리케이션, 서버, 미들웨어, DB, 그리고 게임 컨텐츠를 다 할수 있는데,
자기가 하고 싶은 것에 따라 업무를 맡는 것일 겁니다.
그리고 프로그램팀의 리더는 이것들을 전부 경험해본 사람이 단연코 좋습니다.
나는 밑에 애들 잘시키면 되지, 기술적으론 잘몰라도돼~ 라는 개소리를 하시는 "실무 리더"분이 간혹 있는데,
대부분 오래 못가 오리알 신세됩니다.

 

 

내가 바라는 개발팀

 

 

 

현실

 

경고!!
투자가, 사장님을 비롯한 높으신 분들이 혹시 이 글을 봤다면 절대 이 글을 따라 아는 체하며 인력구성을 하지말고
팀장이나 젤 똑똑한 애 요청대로 팀을 짭니다. 안그러다간, 아주 좆돼는 경우가 생깁니다.


2. 게임은 컨텐츠다
네, 따라서 게임 프로젝트는 컨텐츠 프로젝트입니다.
화려한 렌더링과 가공할 성능의 서버 시스템이 있어도 게임이 재미없으면 망합니다.
이 컨텐츠 프로그래밍을 하는데에는 렌더링도 서버도 처음에는 필요없습니다.

게임 개발을 꿈꾸는 분이라면 평소의 모든 현상을 게임 컨텐츠로 옮길수 있는 코딩 능력이 있으면 참 좋습니다.
애시당초 객체지향 프로그래밍이란 현실을 컴퓨터로 옮기는 작업 아니겠습니까?

혼자서, 혹은 아마츄어 팀으로 게임 프로젝트를 시작하며 망하는건, 무리하게 상용게임을 흉내낼려는 것에도 원인이 있습니다.
엄청난 자본과 시간과 인력이 투입된 게임에 비교하면 자신의 것은 초라해지고 의욕마저 상실합니다.

그러나 그래픽과 네트워크를 제외하고 생각해보면 이야기는 달라집니다.

다음은 엔하위키에서 퍼온 스타크래프트2의 한 대목입니다.
(출처 : http://rigvedawiki.net/r1/wiki.php/%EC%8A%A4%ED%83%80%ED%81%AC%EB%9E%98%ED%94%84%ED%8A%B8%202?action=show&redirect=스타크래프트2 )

 



이 게시물은 오직 게임컨텐츠에 대한 설명입니다.
먼저 글상자 안에 있는 내용은 공격력에 대한 내용인데 필수적으로 공격의 주체와 공격을 받는 객체들의 판정에 대한 이야기입니다.
구조체, 아니 C언어로 사칙연산만 할수 있는 초보자라도 위의 공식을 프로그래밍 언어로 옮길 수가 있을 것입니다.

고스트 객체;
마린 객체;

결과 = 고스트 객체.기본공격(마린);

if(으앙쥬금 == 결과)
{
    delete 마린 객체;
    MessageToAllUser(마린 객체, 쥬금);
}


스타크래프트2는 물론 초당 몇십 프레임이 돌아가고 이동이 자유로운 게임입니다만, 공격이라는 한 순간을 구현하면 위와 같은 루틴으로만 끝내버릴수 있습니다. 이런게 바로 컨텐츠 안에서의 로직 부분에 해당합니다. 좀더 추가해 객체별 상성관계 역시 if 문과 숫자 증감으로 쉽게 구현 가능합니다. 아마 이 글을 읽고 계신 분들 중에 이 정도도 코딩 못하시는 분은 없을 것입니다.

그런데 설명을 읽어보니 컨트롤에 대한 이야기가 나오는군요.
컨트롤이라는 것은 앞서 적은대로 실시간으로 객체들이 계속 돌아가는 게임이기 때문에 생기는 현상입니다.
그래도 별것 없습니다. 객체들이 계속 돌아간다는건, 게임 프로그램이 윈도우 메시지를 처리하지 않는 Idle 타임중에 계속 로직과 화면을 뿌려주는 것에 불과하기 때문이죠. 아, 물론 키보드, 마우스, 네트워크 입력도 같이 받습니다.

 

while(pGameWnd->OnSystemMessageIdle())
{
    네트워크 수신();
    키보드 입력감지();
    마우스 입력감지();
    ......
    고스트 객체;
    마린 객체;

    결과 = 고스트 객체.기본공격(마린);



    if(으앙쥬금 == 결과)
    {

     delete 마린 객체;
     MessageToAllUser(마린 객체, 쥬금);
    }
    ......
    화면갱신();
}

 

간단하죠?
여기서 더 신경써야할 것은 각 객체들의 생성, 소멸, 함수간 전달 관리이며
입출력을 위한 Win32 애플리케이션, 3D, 네트워크 공부인 것입니다.

여기의 게임을 이루는 객체들의 관리가 결국 게임프로그래밍입니다. 비록 허술하다고 하더라도 이것이 게임 코딩의 본질입니다.
물론 사용자 경험을 만들어주는 렌더러의 중요성은 이 자리에서 언급하기 민망할 정도이지만 렌더러 만으로는 절대 게임이 되지 않습니다.
렌더러도 충돌이 들어가야지 비로소 게임틱해집니다 충돌이 들어갔다는 말도 단순히 렌더 타겟이 아니라 게임 객체가 되었다는 말입니다.



뜬금없는 이상적인 렌더러의 사례


3, 그래서 MUD를 만들어봅시다.
2기에서는 이제는 기억에서 잊어진 게임 쟝르인 MUD(multi user dungeon)를 만들어 볼 것입니다.
(절대 그래픽 코딩하기 귀찮아서가 아님.)

 

 

마음만은 사실같은 그래픽으로 해드리고 싶네요...... .


사실 MUD에서 사용되는 기술은 현재의 MMORPG와 큰 차이가 없습니다.
단지 실시간 이동과 공격이라는 것을 지원하기 위해 데드리커닝(dead reckoning)이 추가되었을 뿐이며
3D가 추가되며 부분적으로 여기에 3D 연산이 들어갑니다. ...사실 서버에 3D 안써도 클라만 잘하면 깜쪽같이 자~알 됩니다.
사족을 달면 실제 서버 3D나 넌타게팅 같은 것보다 수십, 수백대의 서버 동기화나 빠른 DB처리가 같은 장비 영역와 섞인 부분이 훨씬 더 어렵습니다.

부활 신호탄은 여기까지 적고,
2화 부터 "소드 아트 온라인 MUD"을 구현해보도록 하겠습니다. 역시 개발환경은 C++과 Win32/64입니다.
그리고 다시 연재를 시작하게 되어 무척 기쁩니다.
이제 다른 멤버들에게 도망다니지 않을수 있게 되었어!!! ㅠㅠ

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/)를 명시해주세요.

PS) 올해 KGC2012에는 나가지 않습니다만, 제 가까이 있는 아주 훌륭한 컨텐츠 개발자 두분이 나가십니다.
      근데 무슨 주제인지는 저두 몰라요...... . -ㅅ-;;

 

---------------------------------------------------------------------------------------------------------------------------------------------------------

 

  1. 예전부터 쭉 보고있었습니다.

    저는 언제쯤 실력자가 될까요 =_=;

    부러워용 ㅠ.ㅠ
    • Rhea君 2012.09.17 21:10    
    • .....................................................
      .....................................................
      ............고백 데이 깜짝 이벤트 인가요?

      .................................가, 감사합니다.
  2. 크로스 2012.09.18 00:11      
    • Rhea君 2012.09.19 13:43    
    • PS3용 아이돌마스터2의 하츠네미쿠 콜라보네이션입니다.
      SEGA의 미쿠보다 남코의 미쿠가 훨씬더 낫다는게 함정이죠.
  3. MUD앞에 있는 동영상때문에 '머드'라고 읽지않고 '엠유디..가 뭐지?' 라고 생각해버렸는데
    정작 동영상은 MMD가 아니고 XBOX용 게임이었군요...
    사전지식이 풍부하기때문에 글을 읽는데 전혀 방해되지 않았습니다. (뭔가 달라!!)
  4.  대마왕J 2012.09.18 00:45      
    • Rhea君 2012.09.19 13:43    
    • 키리토와 아스나 모델링만 있다면 정말 3D로 달릴지도 모릅니다. :-)
  5. 우와 사실 같은 그래픽. 길씨와 너무 똑같이 생겨서 진짜 실사인줄 알았어요 
  6.  친절한티스 2012.09.18 17:23      
    • Rhea君 2012.09.19 13:44    
    • 날 너무 좋아하면 상처받게됨...
  7. 레아횽 조...조... 사랑해요.

 

 

=================================

=================================

=================================

 

 

출처: http://www.gamedevforever.com/http://www.gamedevforever.com/240

 

네트워크 게임 튜터리얼 2기 - 2화 월드 세우기

 


지난 시간에 MUD(Multi User Dungeon)을 만들겠다고 선언했습니다.
뜬금없이 MUD를 만드는 목적은 MUD 그 자체가 목적이 아닙니다.
최종 목적은 다수의 유저가 접속하고 게임 서비스를 진행해주는 C/S 시스템을 만드는 것인데,
그것을 위해 기본이 되는 컨텐츠가 필요하기에 MUD를 만드는 것입니다.

다행히 MUD는 상당수의 MMORPG와 비슷합니다.
MMORPG도 MUD에 결국 그래픽이 덮씌워진 것입니다.
물론 그래픽을 비롯해 여러 최신 기법들이 적용되어 있고 언젠가 그것들을 이해해야 합니다.
하지만 처음부터 복잡한 원리를 억지로 이해할려고 하는 것보다 먼저 그게 왜 필요했는지를 알면 더 좋을 거라 생각듭니다.
이번 튜터리얼을 통해 MUD를 만들며 거기에 대한 해답을 얻을수 있었으면 합니다.

1. RPG 게임의 기본 요소, 혹은 객체
MUD를 구현하기에는 RPG가 가장 알맞으니까 RPG를 만들 것입니다.
소드 아트 온라인을 만들고 싶다!!!!


그럼 반도의 흔한 양산형(^^;;) RPG에는 어떤 요소들이 필요할까요?

1) 공간
지형이죠. 땅은 물론이고 하늘과 바다같은 공간입니다.
공간이 있어야 플레이어와 몹이 뛰어다니고 날라다닐수 있습니다.
특별한 경우를 제외하면 지형 이나 공간 자체가 객체가 되는 일은 없습니다만
공간이야말로 캐릭터들을 위한 가장 기본적인 데이터가 됩니다.

 

 

위엄돋는 프로브


대부분의 땅은 2차열 배열로 충분합니다.
물론 게임이 커지며 쿼드트리, 옥트리나 분할타일...등등 다양한 방식들이 존재하는데 지금은 굳히 어려운 기법으로 구현할 필요 없습니다.
필요할때 공부하면 됩니다. 사실 클라이언트와 결합되면 이런 것들만 소개해도 책 몇권 나올겁니다. 그래서 우리는 엔진을 선택하죠. ㅠ.ㅠ;

2차원 배열, 다시 말해 타일앱(Tilemap)은 게임에 있어서 뼈대와 같습니다.
아니 2D 게임이라면 죄다 타일맵입니다. 배열을 보여줄때 어떤 꽁수를 부리느냐에 따라 사각형이나 마름모가 될뿐입니다.

 

 

이것도 2차열 배열

 

 

요것도 2차원 배열

 

 

심지어 이것도 2차원 배열~

2차원 타일앱에 대해 더 알고 싶다면,
http://kangcom.com/sub/view.asp?sku=200107040021 과 http://kangcom.com/sub/view.asp?sku=200407060008 책을 추천합니다.
좀 고전적인 내용이긴 합니다만 그만큼 기초적이며 필수적인 내용들을 다루고 있습니다.

 

 

꽤 오래된 책이긴 하죠? ^^

사실 Isometric Game Programiing with DirectX는 쓸때없이 두껍습니다.
정작 필요한 챕터는 몇십페이지가 안됩니다.
반면 DirectX9를 이용한 전략 게임 프로그래밍은 길찾기부터 전반적인 내용이 다들어가고 있고
정말 좋아했던 번역자분이 번역하신 책입니다. 좋은 번역이 무엇인지를 제대로 보여주는 몇안되는 기술서적이죠.


너구리 같은 사이드뷰(side view) 방식, 혹은 장기같은 탑뷰(top view)타일맵의 기본은 정말이지 너무 간단합니다.
사이즈에 맞는 2차원 배열을 만들고 for() 문 두개면 뚝딱 나옵니다.
일단 가로x세로, 각각 10칸씩 차지하는 맵을 만들어봅시다.

 

int map[MapSizeX][MapSizeY] = 
{
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};

 

네 이것으로 다입니다. ^-^;;;
배열 처음 배울때 배운 것 그대로입니다.


땅을 화면에 표현해볼까요?

void DrawAllMap() {
    for (int MapX = 0; MapX < MapSizeY; MapX++) {
        for (int MapY = 0; MapY < MapSizeX; MapY++) {
            cout << MapY << "," << MapX << ": " << map[MapX][MapY] << " ";
        }
        cout << endl;
    }
}

 

드디어 첫선을 보이는 소드 아트 온라인(?) 머드

몇 바이트 안되는 자료구조로 캐릭터가 뛰어놀 땅이 생긴 것입니다.
각각의 배열은 좌표계를 의미합니다.
그래픽 게임이라면 각 칸을 이동하는 애니메이션이 추가되면 됩니다.
하지만 이건 MUD니까 그냥 "~로 이동했다" 정도로 표시하면 될 것입니다.

 

 


드디어 첫선을 보이는 소드 아트 온라인(?) 머드

만약 공간 구현에 있어 데이터 최소사이즈 덕후가 되어 보고 싶다면 이 책을 추천합니다.
http://kangcom.com/sub/view.asp?sku=200205090004&mcd=571
제가 아주 좋아하는 책인데 지형뿐만 아니라 앞으로 다룰 플레이어 객체와의 상호작용까지 다룹니다.

 

 

각종 게임회사 입사 퀴즈 문제가 이 안에 있음.

 

2) 캐릭터 - PC/NPC
보통 플레이어와 몹을 따로 생각하는 경향이 있지만 결국 같은 액터(actor)라고 봐도 무방합니다.
보통 유저가 직접 조작할수 있는 PC(Playable Character)와 NPC(None Playable Character)로 나뉩니다.
NPC는 상점 주인이나 몹이 대표적입니다.

 

 

PC와 NPC의 구분이란?


경우에 따라 특정 아이템이나 장치도 NPC가 될수 있습니다.
상자를 열자마자 나타나는 저주받은 칼이라던가 두동강 내는 절단 장치 등도 게임에 따라 내부적으로 NPC로 구현된 경우가 있습니다.
이런 캐릭터야말로 OOP방식으로 게임을 만든다면 가장 기본적인 객체로 구분할 수 있을 것입니다.
물론 이동, 공격, 스킬도 캐릭터의 가장 중요한 속성입니다.

우선 싱글 위주로 구현을 해보죠.
원래는 C++ 없이 C만으로도 게임 만들기 쉽다는 것을 보여주기 위해 순수 C로 만들어볼 생각이었지만,
역시 C++을 안쓰니 제가 불편해 구조체 대신 C++ 클래스로 만들었습니다.

우리의 캐릭터, 아니 플레이어 객체가 가지고 있어야할 가장 기본적인 데이터는 무엇일까요?

1) 클래스 자체의 정보 - RTTI를 위해
2) 현재 위치 - 플레이어가 뛰어다닐 공간은 2차원 배열이니까 2차원 배열의 X, Y값이 들어갑니다.
3) 이름, 레벨, 경험치...
4) 소유 아이템
5) 가능한 퀘스트(이벤트)/현재 퀘스트(이벤트)

이중 우선 움직이는게 관건이니 2)의 현재 위치를 먼저 구현하겠습니다.
나머지는 다중 사용자와 컨텐츠가 붙으며 천천히 구현해도 될 것 같네요.
1)의 클래스 자체의 정보는 고급 토픽입니다.
일단 상속과 다른 캐릭터들이 추가되며 그때 다시 이야기해보지요.

 

enum _DIRECTION {
    NORTH = 0,
        EAST = 1,
        SOUTH = 2,
        WEST = 3
};

typedef struct _GamePosition {
    int iPosX;
    int iPosY;
    _DIRECTION Direction;
}
GamePosition;

class CGamePlayer {
    public:
        CGamePlayer(GamePosition & playerPos);
    ~CGamePlayer(void);

    int SetPosition(GamePosition & playerPos);
    GamePosition GetPosition();

    private:
        GamePosition m_playerPos;
};

 

일단 위치만 넣었습니다만 위치 변수를 int형 X, Y를 그대로 사용하지 않고 GamePosition이라는 구조체를 만들어 썼습니다.
중요한 것은 위치정보 안에 _DIRECTION이라는 방향정보까지 들어가 있다는 점이죠.
게임을 만들때 위치가 있으면 대부분 바라보는 방향도 있으니까요.
이렇든 구현을 하다보면 자체의 데이터를 정의하기 위한 구조체나 클래스들이 어쩔수 없이 많이 나오게 됩니다.
그리고 이런 것들을 잘 쓰는게 OOP의 은닉화, 혹은 캡슐화(encapsulation)입니다.

아뭏든 상기의 플레이어 클래스를 보니 현재 이 게임은 10x10의 공간 속에 방향을 가지고 위치를 이동하는 게임이라는 것이 명확해졌습니다.

여담으로 며칠전 간만에 학부생들이 한가득한 어떤 프로그래밍 게시판을 들렀습니다(후빨 받을려고 간거 아님).
언제나처럼 불변의 진리로 "숙제 때문에" 클래스와 인터페이스 설계 떡밥은 가라앉질 않더군요.
학교에서 내주는 클래스 설계 숙제나 초보용 예제는 그 힘든 OOP 개념을 설명하긴 설명하긴 해야겠는데,
억지로 간단한 수준에서 구현할려보니 도리어 더 헛갈린다고 생각합니다.
사실 상속이나 인터페이스는 학부 교재 수준의 단순한 코드를 위해 만들어진건 아닙니다.
실제 캐릭터들간의 교과서적인 상속같은 것 역시 별로 중요한 문제도 아닙니다.
개념적인 상속을 받지 않고도 구현할 수 있고 다양한 방법들이 많습니다.
예를 들면 클라이언트라면 코드의 재사용이 아닌 렌더러를 타기 위해 상속을 받거나, 상속 자체보다 통보 메시지를 일시에 받기위한 다형성(polymorphism)이 보다 중요합니다.

이런 이야기 역시 천천히 진행해볼까 합니다. 물론 여기는 현업 개발자마다 프로젝트마다 다른 방법이 있고 치고 박고 싸울 떡밥은 충분히 됩니다만, 저라면 제일 중요한게 "라이브할때 편한 방식"입니다. 라이브팀에게 "이거 만든 넘이 너지?!"하면서 등에 칼맞긴 싫거든요.

3) 돈/아이템
돈과 아이템이 없는 게임이 있을까요.
아이템은 캐릭터의 상태와 속성을 변화시키고 수치를 바꾸고 이벤트를 발생시킬수도 있습니다.
아이템은 C/S 환경에서 더욱 복잡해집니다.
언젠가부터 필수 요소인 경매장도 아이템 설계에 많은 변형을 가져와야 했으며 복사템 문제는 여전히 골치를 앓게 합니다.
이 부분은 구현 이전에 알아야할 다른 문제들이 있으니 조금 더 나중으로 미루겠습니다. ^^

여담으로 최근 가장 관심이 갔던 아이템은 디아블로3의 "소멸의 염료"였습니다. 하의는 항상 소멸의 염로로 염색한다! 

 

 

캐릭터 객처와 아이템 객체간 상호작용에 대한 아주 좋은 사례

(출처 : Bump of Romance http://djrtoddl.egloos.com/3328165)

4) 퀘스트/이벤트
사람과 팀에 따라 구현방식이 달라지겠지만,
저는 퀘스트와 이벤트도 하나의 객체로 취급합니다.
이런 개념의 차이는 게임 구현에 큰 영향을 끼치게 됩니다.
아이템과 함께 퀘스트/이벤트는 캐릭터와 끊임없는 상호 작용을 거치게 됩니다.

 

 

NPC를 통해 퀘스트를 해결하는 좋은 사례


아이템과 같이 일단 긴 설명은 시기상조입니다만 상기의 네가지들은 게임 컨텐츠를 만드는데 있어서
제가 뽑은 가장 기본적인 필수요소들이라 생각합니다.
있어 보일려고 한다면 더 적을수도 있겠지만 정말이지 이 네가지면 기본은 충분합니다.

3. 움직이자!!
아무리 좋은 동영상도 단순한 게임 그래픽보다 못합니다. 게임은 입력을 받아 인터랙티브하게 움직여 주니까요.
이제 움직여야죠.
플레이어를 움직이게 하는 것은 간단히 getchar()나 cin이면 충분합니다. 오오~ CUI 창의 위대함이여~
C 첫 시간에 배운 순환문과 입력함수면 게임 루프를 만들수 있습니다.

여기에서 한가지 기억해둬야 할 것은 사용자가 여러개의 입력을 한 경우입니다.
아쉽게도 C/C++ 표준함수만으로는 깔끔하게 한글자만 정확히 입력받을순 없습니다.
아래 보시면 while문이 있는데 첫번째 글자 이후로는 리턴시킵니다.

움직임은 플레이어의 위치 값만 바꿔주면 됩니다.

int SetMovePlayer(CGamePlayer * pGamePlayer) {
    cout << "어디로 이동할 것인가?" << endl;
    cout << "E: 동쪽 W: 서쪽 S: 남쪽 N: 북쪽 .: 종료" << endl;

    char ch;
    while ((ch = cin.get()) != '.') {
        ch = tolower(ch);
        switch (ch) {
            case 'e': {
                GamePosition pos = pGamePlayer - > GetPosition();
                pos.iPosY++;

                if (pos.iPosY >= MapSizeY) {
                    pos.iPosY--;
                    cout << "그곳으로는 이동할 수 없다." << endl;
                } else {
                    cout << "키리토는 동쪽으로 이동하였다." << endl;
                    pGamePlayer - > SetPosition(pos);
                }
            }
            break;

            // 생략

        default:
            cout << "잘못된 명령입니다." << endl;
        }

        while (cin.get() != '\n')
            continue;

        SetDirectionPlayer(pGamePlayer);
    }

    return 1;
}

 

 

흠....2차원 배열이라는게 깔끔하게 직관적인 X, Y 좌표계가 아니라서 이동이 좀 헛갈리겠군요.
그림으로 그려보니 이렇게 동남쪽을 향하는 좌표계가 되었습니다.

 

 

SAO의 제멋대로 좌표계


논리적 좌표계는 게임마다 다른거니 귀찮아서 수정하지 않고 이대로 갈까합니다.
(이미 Move함수를 만들어 두었으니 또써먹으면 되니까요.)
위의 소스 정말 쉽죠?
방향에 따라 X, Y 위치만 바꿔주면 정말로 이동합니다, 아니 이동하는 것처럼 됩니다.
그리고 중요한 부분이 있죠, 바로 화면 밖으로 나가지 못하도록 하는 클리핑(clipping)입니다.
너무나 중요해서 다시 적습니다, 이 부분이죠, 이 부분.

pos.iPosY++;

if (pos.iPosY >= MapSizeY) {
    pos.iPosY--;
    cout << "그곳으로는 이동할 수 없다." << endl;
}

 

 

그런데 코드를 보니 일단 이동한번 해봤다가 조건에 걸리면 뒤로 빼는군요.
아!!!!!!!!!!!!!!!!!!
이래서 게임을 하다보면 오브젝트에 끼워 왔다리 갔다리 화면이 덜덜덜덜 거리는 현상이 일어나는가 봅니다!!!
다음 시간에 다룰 것이지만 이 루틴에 조건들만 추가하면 그럴싸한 충돌처리가 됩니다.

아래 부분에 while()과 continue 가 있습니다.
이 부분의 역활은 무엇일까요?
사용자가 방향이라는 한 단어 대신 "eeeeeeeeeeeeeeeeee"식으로 여러개의 명령이 왔을때 skip하는 부분입니다.
이런 입출력에 대한 예외처리는 이런 단순한 게임이라도 반드시 들어가며 나중에 완성도를 좌지우지하는 중요한 부분입니다.
아직 공부 중이라 작은 소품들을 만드는 것은 아주 훌륭합니다만,
이런 예외처리를 놓치지 마세요.
거대한 프로젝트라도 바로 이런 부분에서 새어버리면 망하는 지름길이 됩니다.

그런데 일부러 코드에서 이 부분을 빼버리면 어떨까?
흠.................. ?

 

4. 사실같은 그래픽

 

 

마음의 눈으로 보면 사실같은 그래픽이 됩니다.


사실같은 그래픽까지는 바라지도 않고 원작같은 그래픽 뽑고 싶습니다!!!!!!!!!!!
...는 무리고 현재 배열과 플레이어의 위치를 능력껏 최대한 화려한 그래픽으로 뽑아봅시다.

배열에 따라 땅과 플레이어의 방향을 표시할 것입니다.
이번에도 for()문 두개면 끝납니다.
조건은 플레이어의 위치와 방향입니다. 아무 것도 없는 땅은 "▦"로 표시합니다.

void DrawAllMapWithPlayer(CGamePlayer * pGamePlayer) {
    for (int MapX = 0; MapX < MapSizeY; MapX++) {
        for (int MapY = 0; MapY < MapSizeX; MapY++) {
            if ((pGamePlayer - > GetPosition().iPosX == MapX) && (pGamePlayer - > GetPosition().iPosY == MapY)) {
                switch (pGamePlayer - > GetPosition().Direction) {
                    case EAST:
                        cout << "▶";
                        break;

                    case WEST:
                        cout << "◀";
                        break;

                    case SOUTH:
                        cout << "▼";
                        break;

                    case NORTH:
                        cout << "▲";
                        break;

                    default:
                        break;
                }
            } else {
                cout << "▦";
            }
        }
        cout << endl;
    }
}

 

 

 

 

 

그럭저럭 이번 원고 완성 ㅜㅜ

자, 몇줄 안되는 코드로 나름 MUD같은게 나왔습니다. 정확히 말하자면 싱글게임이지만요.
이번 화에서 강조하고 싶은 것은 게임 컨텐츠를 만드는데 엄청난 실력까지 필요없다는 것입니다.

소스를 보면 아시겠지만, 대입, 순환, 조건과 너무나 간단한 기본 입출력으로 게임틱한 프로젝트를 만든 것입니다.

"지형, 플레이어, 입력, 이동", 거기에 예외처리까지 이번 화속에 다 들어있습니다.
화려한 상용게임도 크게 다르지 않습니다.

그야말로 존경하옵는 다익스트라님의 증명이 다시한번 이뤄지는 순간입니다.

 

 

다음 시간에는 NPC와 전투를 다뤄볼까 합니다. 조금씩 덩치가 커지며 필요한 클래스들도 추가될 것 같습니다.

이제 추석 연휴군요, 추석 동안 살찌지 말고 친척분들에게 정신공격 당하지도 공격하지도 않으시길 바랍니다.

건강하게 연휴를 보내고 다시 뵙겠습니다.

PS) 소스를 실행시키면 사실같은 그래픽...뿐만 아니라 화려한 사운드가 나옵니다.

내용은 맘대로 퍼갈수 있지만 동의없는 수정은 안되며 출처(http://www.gamedevforever.com/ , http://rhea.pe.kr/)를 명시해주세요.

 

SAO1.zip
0.24MB

 

  1. 도플광어 2012.09.29 12:30      
    • Rhea君 2012.10.04 17:37    
    • 사실 대학교 2학년 정도의 수준 + 실제 강의용으로 글적고 있습니다.
      분량 나오면 학교에서 테스트삼아 가르쳐보셔도 좋을 것 같습니다. ^^

      실제 패킷은 C++ 직렬화를 이용할 것이라 웹소켓만으로는 조금 힘들지 않을까...합니다.
    • 도플광어 2012.10.06 22:13    
    • 네 교재로도 활용하면 좋을거같습니다^^;
  2. 앞으로 내용도 엄청 기대됩니다 ^^;
    좋은 연재감사드립니다.
    나중에 네트웍도 되면 html5기반으로 클라이언트도 한번 만들어봐야겠네요.
  3.  요물딴지 2012.10.04 17:55      
  4. 언제나 잘 보고 있습니다!
    댓글을 매번 달지는 않지만요... ㄱ-
  5. Vitamin IT 2013.03.20 13:54      
  6. 이거 더 연재안해요~? ㅜ_ㅜ NPC와의 전투도 보고싶어요~
  7. onIT 2013.08.14 19:13      
  8. 다음화 언제 나오나요~~
  9. onIT 2013.08.14 19:13      
  10. 다음화 언제 나오나요~~
  11. 좆뉴비 2014.02.27 21:15      
  12. 다음화 보여줘요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  13. 뉴늅비 2014.03.27 16:55      
  14. 현기증납니다. 빨리 다음화 보여줘요~~~~~~

 

=================================

=================================

=================================

 

 

 

반응형


관련글 더보기

댓글 영역