안녕하세요?
정말이지 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/)를 명시해주세요.

 

출처 :  http://rhea.pe.kr/517

저작자 표시 비영리 변경 금지
신고
블로그 이미지

프로그래머 지향자 RosaGigantea

바쁜 일상 생활중의 기억 장소

반도의 돌직구녀. 이런게 바로 제대로

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


돌직구녀에게 감동을 받아 돌직구 포스트 한번 적어본다.
제목처럼 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를 얻어왔다.

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 or Switching Protocols
드디어 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 연결 성공

WebSocket 연결 성공


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

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

폴링따윈 껒!!

폴링따윈 껒!!


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

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

WebSocket Data Framing

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을 쓰고자하는 진취적인 개발자들에게 작은 도움이 되길 바란다.
아무것도 아니지만, 나름 일주일 고생했다. 레알.

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

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



 

WebSocket 프로토콜의 비밀은 파헤친 아크데몬, 아니 하바라가 해맑게 웃고 있다.

WebSocket 프로토콜의 비밀은 파헤친 아크데몬, 아니 하바라가 해맑게 웃고 있다.

 

출처 : http://rhea.pe.kr/515

저작자 표시 비영리 변경 금지
신고
블로그 이미지

프로그래머 지향자 RosaGigantea

바쁜 일상 생활중의 기억 장소

안녕하세요? 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 twiTTER 라는 4달째 개발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가 없었으면 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 = 68000L;
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/)를 명시해주세요.

 

 

출처 : http://rhea.pe.kr/508

저작자 표시 비영리 변경 금지
신고
블로그 이미지

프로그래머 지향자 RosaGigantea

바쁜 일상 생활중의 기억 장소

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

그리고,


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

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

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

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

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

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

https://developer.apple.com

출처 : 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/ )를 명시해주세요.

 

출처 : http://rhea.pe.kr/503

저작자 표시 비영리 변경 금지
신고
블로그 이미지

프로그래머 지향자 RosaGigantea

바쁜 일상 생활중의 기억 장소

으음, 본의 아니게 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개

꼴랑 필드 4개


 

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

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를 보냅니다.

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


 

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

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

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

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


 

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

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


 

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

서버는 단순히 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/500

저작자 표시 비영리 변경 금지
신고
블로그 이미지

프로그래머 지향자 RosaGigantea

바쁜 일상 생활중의 기억 장소