티스토리 툴바


달력

04

« 2014/04 »

  •  
  •  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  •  
  •  
  •  

'전체'에 해당되는 글 304

  1. 2012/09/25 C/C++로 Socket.IO에게 돌직구를 던져보았다.
  2. 2012/09/25 네트워크 게임 튜터리얼 5 - 드디어 직렬화
  3. 2012/09/25 네트워크 게임 튜터리얼 4 - 로비, 그리고 할말이 많다
  4. 2012/09/25 네트워크 게임 튜터리얼 3 - 데이터베이스 그리고 C/S
  5. 2012/09/25 네트워크 게임 튜터리얼 2 - 아키텍트 돋는 첫걸음
  6. 2012/09/25 윈도우에서 mongoDB는 C#으로
  7. 2012/09/25 네트워크 게임 튜터리얼 1 - 워밍업
  8. 2012/09/24 [ VC11-C++11 ] range base for - 1
  9. 2012/09/10 GREP for Windows (2.6.3)
  10. 2012/09/10 코드 최적화 팁
  11. 2012/08/25 CAtlMap 사용법 정리
  12. 2012/08/09 MSSQL 2005 메모리 관리
  13. 2012/08/07 MS-SQL 특정 테이블 데이터를 백업/복구
  14. 2012/07/31 Visual Svn post-commit 에 관해서
  15. 2012/07/03 MSXML 파싱
  16. 2012/06/26 ActiveX 만들기 총정리
  17. 2012/06/22 Visual Stdio 2010 마이그레이션중.. 가끔 링크에러 날때
  18. 2012/06/20 Visual C++ 2010 마이그레이션중... error C2825: '_Fty': '::'가 뒤에 나오면 클래스 또는 네임스페이스여야 합니다.
  19. 2012/06/20 [Upgrade to VC++ 10] _WIN32_WINNT 버전 문제
  20. 2012/06/20 Visual C++ 2010 으로 소스 판올림 하면서 msado15.dll 에러 날때...
  21. 2012/06/08 지금 PC의 IP 알아내기
  22. 2012/04/27 ASP.NET 페이지에서 갑자기 MS-SQL 접속이 안될때 (ADO 접속에러)
  23. 2012/04/26 MS-SQL ldf 파일 사이즈 줄이는방법!
  24. 2012/04/25 MS-SQL : restore database is terminating abnormally 뜰때
  25. 2012/04/25 MS-SQL, DB에 속해 있는 테이블 안의 데이터 모두 일괄적으로 삭제하는 쿼리
  26. 2012/03/21 프로그램 시간 측정
  27. 2012/03/06 버츄얼과 오버라이드 차이 Virtual? Override?
  28. 2012/02/14 MS-SQL의 내장 프로시저의 특정 단어 검색하기
  29. 2012/01/05 Predefined Macros
  30. 2012/01/03 [C/C++] Debug Printf
반도의 돌직구녀. 이런게 바로 제대로

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


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

저작자 표시 비영리 변경 금지
Posted by 考えの進化 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

저작자 표시 비영리 변경 금지
Posted by 考えの進化 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

저작자 표시 비영리 변경 금지
Posted by 考えの進化 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

저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea
본의 아니게 업계에 알려진 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.214에 접속


 

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

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


 

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

게임서버로 짐작되는 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를 위한 준비를 합니다.

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

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

1-2) Connect()

로긴

로긴

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

1-3) recv()

배틀넷 서버에 접속

배틀넷 서버에 접속


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

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

방을 만들면...

방을 만들면...


 

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

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

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

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

UDP 막 날라다님

UDP 막 날라다님


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

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

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

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

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


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

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

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


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

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

마땅한 짤이 없어...지만 이건 스타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/ )를 명시해주세요.

 

 

출처 : http://rhea.pe.kr/498
저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea
2012/09/25 16:29

윈도우에서 mongoDB는 C#으로 C# 관련2012/09/25 16:29

얼마전 mongoDB를 좀 갖고 놀았다.

DBA는 DB 저장 프로시져가 당연히 첫번째 관심사항이겠지만
서버 개발자의 경우 DB와 연결하는 드라이버가 첫번째 관심사항이다.
당연히 개발자답게 Windows용 C++ Driver부터 빌드하였다.
소문대로 mongoDB 빌드는 무척 더러웠다.
mongoDB 소스를 갖고 Windows용 C++ 드라이버를 빌드한다는 것이 좀 이해가 안된다.

그리고 내린 결론은...
Windows 서버에서는 C++로 mongoDB 서버를 짜지 말자는 것이다.
추천언어는 C#.

이런 결정이 내려진 이유는...

1) scons에서 더이상 force32로 옵션으로 32비트 빌드가 안된다.
32비트 드라이버를 빌드할려면 32비트 PC나 가상 머신을 띄워 빌드 시켜야 한다.
64비트로만 서버를 짜겠다면 할말은 없지만 최소 클라이언트에서는 문제가 된다.
(사실 mongoDB 데몬을 따로 개조 & 빌드하여 Access를 대신하는 용도로 써볼까 생각을 해봤었다.
물론 이와 같은 문제로 안됨.
그리고 32비트에서는 mongoDB 역시 데이터 사이즈 2GB가 한계.)

2) boost를 1.42로만 써야한다.
mongoDB를 빌드시키는 것뿐만 아니라, mongoDB 드라이버를 이용하는 프로젝트는 역시 boost 1.42를 써야한다.

서버상에서 가장 큰 이유는 물론 두번째 항목이다.
boost 1.45에서 interface가 많이 바뀌었는데 mongoDB만으로 위해서 boost 1.42 를 따로 관리하고 빌드시 참조해야 한다는게 말도 안된다.
일괄빌드에 치명적인 문제를 안긴다. 소위 말하는 빌드비용의 증가 문제가 따라온다.

그래서 C++은 포기했다.
아니 애시당초 이렇게 복잡한 빌드 방법이라니 mongoDB는 Windows에서 C++로 쓰지 말라는 말같다. ㄴㅁ...

기왕 좀 삽질한 거, 서비스로 드라이버인 mongoclient.lib를 포스트에 올려드리고 싶지만,
용량이 무려 150 메가 이상이라 차마 올려드리진 못하겠다.

대신 mongoDB 드라이버 빌드팁을 적어본다.

0) 비쥬얼 스튜디오에서 빌드하는게 아니라면 Spider Monkey는 필요없다.
Scons로 빌드하므로 괜히 복잡하게 다운받고 어디에 설치해야 하는지 고민말자.

1) 몽고 DB소스(DB+ C++ 드라이버) 다운 https://github.com/mongodb/mongo , https://github.com/mongodb/mongo/downloads
(드라이버-리눅스-가 아님.)

2) 파이썬 32비트 설치

3) Scons 설치- 설치시 Python\Scripts 라는 기본 위치에 설치
Scons은 *.mak 파일 빌드를 대체하는 개념인데 파이썬을 쓰므로 이렇게 설치를 한다.

4) MongoDB는 boost 1.42에서 빌드되었음으로 MongoDB홈페이지에서 제공하는 해당버전 다운하고 적당한 폴더에 푼다.
http://www.mongodb.org/pages/viewpageattachments.action?pageId=12157032
이것은 앞으로 boost 빌드뿐만 아니라 mongoDB 드라이버를 이용하는 모든 프로젝트에서 사용해야 한다. 좆같다.

5) 4)에서 받은 mongoDB를 SconStruct 파일에서 boost 경로 지정.
대략 640라인으로 이동,
def find_boost():
for x in ('', ' (x86)'):
boostDir = "C:/boost" <-- 4)에서 지정한 경로로 적어줌.
if os.path.exists( boostDir ):
return boostDir
for bv in reversed( range(33,50) ):
for extra in ('', '_0', '_1'):
boostDir = "C:/Program Files" + x + "/Boost/boost_1_" + str(bv) + extra
if os.path.exists( boostDir ):
return boostDir
if os.path.exists( "C:/boost" ):
return "C:/boost"
if os.path.exists( "/boost" ):
return "/boost"
return None


6) 콘솔창에서 1)에서 받은 mongoDB 소스 폴더로 가서 PATH 명령어로 파이썬경로와 파이썬 아래 script 경로(Scons) 설정.
기존 PATH에 있는 것 쓸일 없으니까 단순히 아래대로 자신의 파이썬 경로를 적어주자.
SET PATH=D:\Program Files (x86)\Python27;D:\Program Files (x86)\Python27\Scripts

7) 클라이언트 빌드
--> 8)에서 몽고DB를 64로 빌드하면 클라이언트도 64, 몽고DB를 32로 빌드하면 클라이언트도 32...인데 Scons가 더이상 64비트에서 32비트로 강제 빌드가 안된다.
따라서 32비트 드라이버가 필요하면 32비트 머신이나 가상머신 필요...

8) Client 개발시 ws2_32.lib 추가
까보진 않았지만 mongoDB 데몬과 통신시 역시나 TCP/IP를 쓰는 듯 하다.
님들, 걍 C# 쓰세요~

님들, 걍 C# 쓰세요~

 

 

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

저작자 표시 비영리 변경 금지

'C# 관련' 카테고리의 다른 글

C#으로 게임서버를 개발한다면?  (1) 2012/10/09
윈도우에서 mongoDB는 C#으로  (0) 2012/09/25
Posted by 考えの進化 RosaGigantea
원문 : http://www.gamedevforever.com/39

안녕하세요? 요양과 잉여짓으로 바쁜 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(NetBEUI) 설치 없이 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에게도 공식 모델이 필요합니다.

모델의 좋은 사례, 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 네명을 고르시오.

이중 알맞은 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 서버를 그대로 썼습니다.
단, 한 파일로 설명된 소스를 보기 좋게 클래스별로 나눠놨습니다.
이 정도면 네트워크 프로그래밍에 익숙하지 못한 초보분이라도 약간의 노력이면 다음 시간부터 따라오실수 있을 것입니다.

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




4. 더 생각해볼꺼리

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



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

 

 

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

저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea
2012/09/24 13:06

[ VC11-C++11 ] range base for - 1 C/C++언어2012/09/24 13:06

출처 : http://vsts2010.net/712

 

VC10에서 선보였던C++11의 기능 중 강력하면서 사용하기 쉽고, 자주 사용한 기능이 아마 'auto'이지 않을까 생각합니다. 예전에 강연을 할 때 auto와 관련된 예제를 보여드리면 많은 분들이 아주 좋아하시더군요(좀 놀라기도 하시더군요^^). 어떤 분들은 딴 건 제쳐두고 이것 때문이라도 VC10을 사용해야겠다는 분들이 있었습니다.

이번 VC11에서도'auto'와 같은 강력한 기능이 있습니다. 바로 'range base for' 입니다. 이것을 사용하면 반복문을 아주 쉽고, 강력하게 사용할 수 있습니다.

VC 특화 기능인 for each와 비슷하기 때문에 기존에 for each를 사용하고 있다면 이제는 range base for로 쉽게 바꾸어서 사용하면 됩니다.

예제를 통해 일반적인 for , VC for each,range base for문의 차이를 예제를 통해서 보겠습니다.

< 예제. 1 >

#include <iostream>

int main()

{

int NumberList[5] = { 1, 2, 3, 4, 5 };

std::cout<< "일반적인 for "<< std::endl;

for( int i = 0; i < 5; ++i )

{

std::cout<< i << std::endl;

}

std::cout<< "VC++ 특화의 for each" << std::endl;

for each( int i in NumberList )

{

std::cout<< i << std::endl;

}

std::cout<< "range base for " <<std::endl;

for( auto i : NumberList )

{

std::cout<< i << std::endl;

}

return 0;

}

< 실행 결과 >


<예제.1>을 보면 일반적인 for 문은

for( int i = 0; i < 5; ++i )

와 같이 시작과 종료 조건, 증가 값 이렇게 3개의 조건에 의해서 반복 됩니다.

그러나 range base for문은 VC만의 반복문인 for each와 비슷하게 데이터셋 변수와 이 데이터셋 요소의 타입을 선언하면 됩니다.

for( auto i : NumberList )

기존의 for 문에 비해서 또는 for each 보다도 간편해졌고, for each는 표준이 아닌 VC만의 기능인 것에 비해서 range base for C++ 표준 기능입니다.

range base for 문의 문법은 아래와 같습니다.

for ( for-range-declaration : expression ) statement

range base for 덕분에 반복문의 사용이 쉬워졌고, for 문을 사용할 때 종료 조건이 잘못되어 메모리 침범을 하는 위험도 피할 수 있게 되었습니다.

저작자 표시 비영리 변경 금지
TAG C++0x, C/C++
Posted by 考えの進化 RosaGigantea
2012/09/10 16:12

GREP for Windows (2.6.3) Util 프로그램2012/09/10 16:12

http://code.google.com/p/dngrep/

.NET implementation of GREP tool with GUI. Requires Microsoft .NET 4.0 to run (http://msdn.microsoft.com/en-us/netframework/aa569263.aspx). Version 2.0+ is implemented using WPF framework.

Features

  • Shell integration (ability to search from explorer)
  • Plain text/regex/XPath search (including case-insensitive search)
  • Phonetic search (using Bitap and Needleman-Wunch algorithms)
  • File move/copy/delete actions
  • Search inside archives (via plug-in)
  • Search MS Word documents (via plug-in)
  • Search PDF documents (via plug-in)
  • Undo functionality
  • Optional integration with text editor (like notepad++)
  • Bookmarks (ability to save regex searches for the future)
  • Pattern test form
  • Search result highlighting
  • Search result preview
  • Does not require installation (can be run from USB drive)

 

이걸 알고 프로그래밍 하면 효율이 120% 상승하는 효과가 ㅋㅋ

저작자 표시 비영리 변경 금지
TAG grep
Posted by 考えの進化 RosaGigantea
2012/09/10 11:03

코드 최적화 팁 C/C++언어2012/09/10 11:03

출처 : http://blog.naver.com/chaewh83/140016180847

 

이걸 내가 쓰는 프로그래밍으로 풀면

 

1. 컴파일러 최적화 옵션에서 '속도 최적화' 대신 '코드 크기 최적화' 설정이 캐쉬 성능을 향상시켜 좀 더 빠른 코드를 얻게 함.

 

-> 네트워크 프로그래밍의 경우, 경우에 따라 덜 최적화된 코드가 필요하므로 volatile 를 써야 함.

 

2. 각 CPU(인텔의 SSE, SSE2 / AMD 3D Now)의 SIMD를 활용하는 함수를 만들것.

-> 서버는 현재로선 인텔 CPU니까.....

 

3. 가능한 나눗셈 연산을 최소화 할것.

예)

-> b = a / m;

    c = d / m;

이 있으면

 

-> m = 1/m;

    b = a * m;

    c = d * m;

... 쉬프트가 여러우면 곱셈으로 해버림...

 

4. switch문을 쓸때 연속된 수치를 사용할것, 그러면 VC가 테이블 형태의 분기를 만들어줌.

-> 즉 가능하면 enum {} 으로 쓰는게 좋다는 이야기임.

 

예) case 0:
     case 1:

     case 2: ... (역순으로도 상관없음)

 

잘못) case 2:

        case 4:

        case 3:

 

5. 펜티엄 pro 이상에서는 CMOVxx/FCMOVxx 같은 조건적 이동 명령이 가능.

간단한 if문보다는 ? 를 사용할것.

 

->A == 0 ? choice1 : choice 2;

 

다만 디버깅할때 어느쪽 분기로 떨어지는지는 추적하기 힘드므로

#if _DEBUG

 if()...

#else

 ?...

#endif

를 쓰는것도 나쁘지 않을듯.

 

6. 자주 사용하는 자료 구조는 32bit 배수로 정렬시켜서 사용할것,

컴퓨터의 캐쉬라인을 최적으로 쓸 수 있기때문에 성능이 크게 향상됨. 팬티엄4는 L1캐쉬가 64바이트고 L2가 128바이트

이런걸 감안해서 쓰는 기법을 padding 이라 함.

 

7. sin, cos, tan, exp, arcsin등 수학 함수는 가급적 쓰지말고,

각 수치 데이터를 테이블로 만들어서 간략한 함수로 만들어 쓸것

 

-> psp나 3ds때도 이렇게 했는데...

 

8. 부동소수점은 가급적 double보다 float를 사용할것.

double 의 나눗셈은 39사이클이 걸리지만, float는 19사이클임. (단 2의 제곱수로 나누면 8사이클정도만 걸림)

그리고 float형일때 반드시 뒤에 f를 붙일것.

float a = 1.0; 보다는

float a = 1.0f; 이쪽이 더 빠름

 

9. 순서에 상관이 없다면 후연산자 보다는 선연산자를 사용할것.

a++; 보다는 ++a; 를 쓸것.

 

->출처쪽에선 이유가 안나와 있지만, 이걸 어셈 코드상에서 보면

a++; => a = a + 1; 개념이고, ++a; => a+1; 개념임.

 

10. 가능한 const를 사용할것. 컴파일러가 좀더 최적화를 잘 해줄 가능성이 높아짐.

 

11. 메모리 관리 함수는 따로 사용하는것이 좋음. malloc 과 free는 느리기 때문에 가능한 메모리풀을 만들어사용할것.

 

저작자 표시 비영리 변경 금지
TAG C/C++
Posted by 考えの進化 RosaGigantea
2012/08/25 14:51

CAtlMap 사용법 정리 C/C++언어2012/08/25 14:51

[ 선 언 ]

#include <atlcoll.h>

// Key 값이 문자형 일 경우

CAtlMap<CString, CImageData*, CStringElementTraits<CString>> m_tImage;

// 위의 조건을 제외한 선언

CAtlMap<int, CImageData*> m_tImage;

[ 등록 ]

m_tImage.SetAt( Key , Value );

[ 삭제 ]

m_tImage.RemoveKey( Key );

m_tImage.RemoveAll();

[ 검색 ]

m_tImage.LookUp( Key, Value ); // Key 값을 넣으면, Value 를 준다. 리턴값은 bool

CAtlMap<Key, Value>::CPair* pPair = m_tImage.LookUp( Key );

[ 시작 위치 ]

POSITION pos = m_tImage.GetStartPosition(); // iterator 라고 생각하면 된다.

[ 다음 위치 ]

m_tImage.GetNext(POSITON); // 리턴값 : CPair*

m_tImage.GetNextValue(POSITON); // 리턴값 : Value

[ 검색 응용 ]

POSITION pos = m_tImage.GetStartPosition();

CAtlMap<Key, Value>::CPair* pPair = NULL;
while (pos)
{
pPair = m_tImage.GetNext(pos);
}

POSITION pos = m_tImage.GetStartPosition();
while (pos)
{
CImageData* data = m_tImage.GetNextValue(pos);
}

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

나머지 다른 기능들도 있지만, 위의 내용만 알아도 충분히 알수 있는 내용들이라서 정리하지 않겠다.

[출처] CAtlMap 사용법 정리|작성자 스쿠프

 

 

지금, 서버 엔진쪽을 보고 있는데,  map 구조를 hash_map 으로 바꿔보고 다음 위의 CAtlMap으로도 적용시켜봐서

성능 테스트를 해봐야 할꺼 같다.

저작자 표시 비영리 변경 금지
TAG atl, C/C++
Posted by 考えの進化 RosaGigantea

태국에 게임 서버 서비스를 하려고 이것 저것 테스트 하던 도중

이상하게 쿼리를 한번에 3만개 정도 실행 시키면, 데드락 걸리면서 쿼리가 제대로 실행이 안되는 경우가 발생했습니다.

 

처음엔 이게 시간과 관련된 쿼리라 태국의 로컬시간대랑 뭔가 연관이 있지 않을까 이것 저것 보던중

태국에서 셋팅한 윈도우 설정에 문제가 있다는걸 확인했습니다.

 

CPU가 Xeon X5675 @ 3.07Ghz 에다가 16GB 렘이 있는 시스템인데

윈도우 2003 (32bit)를 설치해서 메모리를 제대로 소화를 못하는거 같더군요.

 

그런 고로, 메모리를 설정하는 것을 구글에서 찾아내서 적용시켜 보니 쌩쌩 날아다니는 DB....

ㅁㄴㅀㄴㅏㅣㅓㅈㅎㅁ5!@$

 

어쨋든.... 메모리 설정에 대해선

http://toe10.tistory.com/63 을 참고해서 셋팅했습니다.

 

혹시 모를 폭파 위험에 여기서 제가 한 셋팅 순서도 올려 놓겠습니다.

 

1. MSSQL에서 AWE(Address Windowing Extensions)을 설정해야 합니다만,

32bit 운영체제 이므로 그전에 Lock page in memory 권한을 얻어와야 합니다.

 

시작 -> 프로그램 -> 관리도구 -> 서비스에서

SQL Server Agent(MSSQLSERVER)의 사용자 명을 확인합니다.

 

 

 

2. 시작 -> 실행 -> gpedit.msc 를 실행한뒤 아래 그림처럼

Lock pages in memory 권한을 1과 같은 사용자에게 줍니다.

 

3. 서버 설정 탭에 들어와서 AWE 사용을 체크하고

최대 서버 메모리를 설정합니다 (단위는 MB)

 

4. 네트워크 속성에서 네트워크 응용 프로그램을 취해 데이터 처리량 최대화를 선택합니다.

...

문제는 "Microsoft SQL Server 2005 포켓 컨설턴트 관리자용"이란 책에선 이 옵션을 피하라고 기술 되어있습니다만..

 

음....

 

 

저작자 표시 비영리 변경 금지
TAG MS-SQL
Posted by 考えの進化 RosaGigantea

특정 테이블의 레코드들만 따로 백업할때 쓰는 방법 입니다. 

 

(보안상 중요한 텍스트는 다 삭제)

 

command 에 들어가서

백업

$> bcp [db명].dbo.[백업할 테이블명] out [백업 파일명] -c /U[db의 유저명] /P[db의 암호]

 

복구

$> bcp [db명].dbo.[백업할 테이블명] in [백업 파일명] -c /U[db의 유저명] /P[db의 암호] 

 

 

저작자 표시 비영리 변경 금지
TAG MS-SQL
Posted by 考えの進化 RosaGigantea

SVN을 좀 비주얼 하게 관리 하게 하는게 Visual SVN.

실제로 현재 울 회사의 소스 관리도 visual svn에 맡긴 상태입니다.

 

하지만 굳이 소스관리만 하라는 용도는 아니고....

 

울 회사에서 게임 패치 서버를 만들려고 하는데, 보통은 CDN업체에 맡깁니다..

(파일 분산 다운로드 라던가.. 뭐 그런 문제로...)

하지만, 외국 회사의 경우 그런걸 할 처지가 안되는 관계로 (몇몇 국가에선 CDN이 없다고 바이어가 박박 우깁니다....)

약간의 편법을 쓴게 svn을 이용한 패치 서버 입니다.

 

우선, 패치 파일들을 넣을 svn 서버를 설치하고

svn의 repositories 안에 파일을 넣습니다.

 

그리고 이것을 IIS6 or 7에서 관리하는 http or ftp의 주소가 링크된 폴더랑 파일 싱크를 맞추면

업데이트 파일을 commit 시키는것으로 자동으로 cdn이든 사설 IDC든 파일이 다운로드가 가능해집니다.

 

문제는... commit 을 시킬때 이것이 링크된 폴더랑 자동 싱크를 맞추는 것인데, visual svn 안에 post-commit 기능이 있더군요.

 

우선...

 VisualSVN 안의 레파토리에서 모든작업 -> Manage Hooks 를 선택합니다.

 

 

안에 보시면 Post-Commit hook 이 있습니다.

이건.. 만약 이 레파토리에 commit이 들어오면 그 뒤에 자동으로 뭔가 실행시켜주는 난입니다.

배치파일 양식으로 해주시면되고요. 

 

내용은

@echo on
"C:\Program Files (x86)\VisualSVN Server\bin\svn.exe" export [SVN의URL주소] [싱크시킬 폴더] --username [유저이름] --password [암호] --force

이외에 여러 응요이 가능하다고 보여지네요.

 

 

 

참고 사이트 http://sway.tistory.com/715

저작자 표시 비영리 변경 금지
TAG svn
Posted by 考えの進化 RosaGigantea
2012/07/03 16:27

MSXML 파싱 C/C++언어2012/07/03 16:27

출처 : http://kukuta.tistory.com/99

 

 

MSXML에 관한 VB나 C# 같은 언어의 예제는 많은데 C++의 예제는 별로 없는 것 같아 간단하게 나마 예제 코드를 만들어 봅니다.

코드를 보시기 전에 xml에 대한 기본적인 개념을 익히시고 싶으신 분은 XML 기초를 참조해 주세요.

MSXML API를 사용하기 위해서는 COM을 어느정도 알면 상당히 편하겠지만 몰라도 상관은 없습니다. 저도 COM에 관해서는 잘 모르기 때문에 이번 포스트에서 그와 관련된 설명은 건너 뛰기로 하겠습니다. 또한 MSXML 파서 설치 같은 것은 기본적으로 다 되어 있다고 가정하고 시작하도록 하겠습니다. 혹시나 설치나 설정 등에 어려움을 겪으신다면 댓글로 남겨 주세요. 그에 대한 포스팅을 따로 마련 해보도록 하겠습니다.

이 포스트는 MSXML4.0 버젼을 기준으로 작성 되었으며 포스트에서 예제로 사용하고 있는 xml파일은 첨부 파일로 저장되어 있으니 포스트를 읽어 보시기 전에 첨부 파일을 다운로드 받으시는 것이 전체적으로 이해 하시기 편할 것이라 생각 됩니다.



1. Dll import
MSXML 파서를 사용하기 위해서는 DLL을 import해야 합니다. 헤더 파일과 라이브러리를 로딩 하는 방법도 있지만 여기서는 dll을 import하는 방법을 사용하도록 하겠습니다. 우리가 사용하는 버젼은 msxml4.0 버젼이므로 아래와 같은 코드를 추가 합니다 :

#import <msxml4.dll>


2. 초기화
초기화 과정에는 COM객체를 사용하기 위한 초기화와 XML Document를 사용하기 위한 초기화 과정. 이렇게 두 단계가 있습니다. 물론 XML 파서가 COM을 사용하고 있으므로 COM에 대한 초기화가 먼저 이루어 져야 합니다 :

// COM 객체를 사용하기 위해 초기화를 한다.
// 윈도우 버젼 버젼이 올라가면서 확장된 초기화 함수를 사용 할 수 있다.
bool initializeCOM() {
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
CoInitialize(NULL);
#endif
return true;
}

COM객체를 사용할 준비를 마치고 나면 XMLDocument 객체를 초기화 해야 합니다. 여기서 사용되는 것XMLDOMDocument 인터페이스를 초기화 해야 합니다. 여기서는 편의상 IXMLDOMDocuement를 전역 변수로 놓도록 하겠습니다. 그리고 코드 작성의 편의를 위해 스마트 포인터로 정의 되어 있는 IXMLDOMDocument2Ptr을 사용하도록 하겠습니다 :

// DOM 의 핵심이 되는 객체
// 파싱된 XML을 저장하고 기타 각종 설정들을 담고 있다.

MSXML2::IXMLDOMDocument2Ptr g_pXMLDoc;
...
bool initializeMSXML() {
g_pXMLDoc.CreateInstance(__uuidof(MSXML2::DOMDocument40));
g_pXMLDoc->async = VARIANT_FALSE;
g_pXMLDoc->validateOnParse = VARIANT_TRUE;
g_pXMLDoc->resolveExternals = VARIANT_TRUE;
return true;
}

3. Validation check
MSXML파서의 기능중 하나는 xml파일의 정합성(validation)을 검증하는 것입니다. 여기서 정합성이란 단순한 well-formed 문서 뿐만 아니라, 스키마나 DTD에서 정의한 규칙을 제대로 따르고 있는가하는 것 까지 모두 검사하는 것입니다. 사용된 xml문서는 첨부 파일에서 다운 받으실 수 있습니다 :

bool validateDocument(const std::string& fileName) {
g_pXMLDoc->load(fileName.c_str());

// 최근 실행 된 XML관련 오퍼레이션에 대한 결과를 리턴
MSXML2::IXMLDOMParseErrorPtr pError = g_pXMLDoc->parseError;

_bstr_t strResult = "";
bool isValidate = true;
if (pError->errorCode != S_OK) {
strResult = _bstr_t("Validation failed on ") + fileName.c_str() +
_bstr_t("\n=====================") +
_bstr_t("\nReason: ") + _bstr_t(pError->Getreason()) +
_bstr_t("\nSource: ") + _bstr_t(pError->GetsrcText()) +
_bstr_t("\nLine: ") + _bstr_t(pError->Getline()) +
_bstr_t("\n");
isValidate = false;
}
else {
strResult = _bstr_t("Validation succeeded for ") + fileName.c_str() +
_bstr_t("\n======================\n") +
_bstr_t(g_pXMLDoc->xml) + _bstr_t("\n");
isValidate = true;
}

pError.Release(); // RefCount를 사용하는 포인터이므로 Release과정이 꼭 필요

std::cout << ConverseBstrToString(strResult) << std::endl;
return isValidate;
}

4. _bstr_t를 char* 로 변환 하기
XML파서에서 리턴하는_bstr_t 라는 문자열을 일반 cout이나 printf에서 출력 하면 정상적인 문자열이 출력되는 것이 아니라 알 수 없는 숫자들의 나열로 출력이 됩니다. 이유인즉슨 wide char를 사용하는 xml과 아스키코드를 사용하는 C/C++간의 문자열 바이트 차이 때문인데요, 이 문자열들을 정상적으로 보기 위해서는 간단한 변환 과정을 거쳐야 합니다 :

#include <atlconv.h> // USES_CONVERSION과 W2A를 사용하기 위해 include
...
std::string ConverseBstrToString(const _bstr_t& str) {
USES_CONVERSION;
return std::string(W2A(str));
}

altconv.h 파일을 인클루드 하기 싫으시다면 아래의 방법도 있습니다 :

std::string ConverseBstrToString(const _bstr_t& str) {
char buf[1024] = {0,};
sprintf(buf, "%S", (LPCTSTR)str);
return std::string(buf);
}

5. 엘리먼트의 텍스트 읽어 오기
첨부 되어 있는 예제 xml 문서를 보면 아래와 같은 형식의 comment 라는 엘리먼트가 있습니다 :

<purchaseOrder ... >
....
<comment>Hurry, my lawn is going wild!</comment>
....
</purchaseOrder>

위의 comment가 가지고 있는 텍스트를 읽어오기 위해서는 먼저 comment의 엘리먼트를 얻어오고, 그 엘리먼트에서 텍스트를 가지고 오면 됩니다 :

void selectSingleElement_and_getText() {
MSXML2::IXMLDOMNodePtr singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/comment"));
if(NULL == singleNodePtr) {
return;
}
const BSTR str = singleNodePtr->Gettext();
std::cout << ConverseBstrToString(str) << std::endl;

singleNodePtr.Release();
}

코드 자체가 워낙 간단하여 긴 설명 보다는 개조식의 간단한 설명을 드리겠습니다 :

  1. 엘리먼트를 저장하기 위해서 IXMLDOMNodePtr을 선언 합니다.
  2. XMLDocument객체에서 selectSingleNode를 이용해 엘리먼트 객체를 얻어 옵니다. 이 때 XPath를 이용합니다.
    (이 함수는 특정 하나의 엘리먼트를 얻어오기 위해 사용되는 것입니다. 엘리먼트 리스트를 얻어오기 위한 함수는 뒤에서 다시 한번 살펴 보기로 하겠습니다.)
  3. 얻어진 엘리먼트에서 Gettext()를 호출 하여 텍스트 내용을 얻어 옵니다.
  4. 얻어진 텍스트를 아스키 형태로 변환하기 위해 위에서 작성한 ConverseBstrToString() 함수를 이용 합니다.

6. 엘리먼트의 속성(attribute) 읽어 오기

void selectSingleElement_and_getAttribute() {
MSXML2::IXMLDOMNodePtr singleNodePtr;
MSXML2::IXMLDOMAttributePtr attrPtr;
std::string retString("");

do {
singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/shipTo"));
if(NULL == singleNodePtr) {
break;
}
attrPtr = singleNodePtr->Getattributes()->getNamedItem("country");
if(NULL == attrPtr) {
break;
}
const BSTR str = attrPtr->Getvalue().bstrVal;
// or 'const BSTR str = attrPtr->Gettext() is ok'
retString = ConverseBstrToString(str);
} while(false);

if(NULL != singleNodePtr) {
singleNodePtr.Release();
}
if(NULL != attrPtr) {
attrPtr.Release();
}

std::cout << "Attribute in Element : " << retString << std::endl;
}
  1. selectSingleNode() 함수를 이용하여 엘리먼트를 얻어 오는 것 까지는 위에서 살펴 본 것과 동일합니다.
  2. 엘리먼트를 얻고 난 후에는 Getattributes() 함수를 이용해 어트리뷰트들을 얻고, getNamedItem() 함수를 이용해 특정 어트리뷰트를 이름을 통해 가지고 옵니다.
  3. 어트리뷰트가 가지고 있는 값을 가지고 올 때, 어트리뷰트의 타입을 지정할 수 있습니다. 만일 어트리뷰트가 단순한 문자열이라면 Getvalue().bstrVal 이나 Gettext() 함수를 이용해 가지고 올 수 있습니다. 만일 정수라던지 하는 다른 타입일 경우 Getvalue().xxxVal에서 적절한 타입을 지정해 주시면 됩니다. 지정될 타입에 대해서는 MSDN이나 해당 union 구조체를 확인 하시면 됩니다.

7. 엘리먼트 리스트 읽어 오기

엘리먼트 리스트에서 값을 얻어 오는 것 또한 위의 과정들과 크게 다르지 않습니다 :

void selectElementArray() {
MSXML2::IXMLDOMNodeListPtr nodeListPtr = g_pXMLDoc->selectNodes(_bstr_t("/purchaseOrder/items/item"));
for(int i=0; i<nodeListPtr->Getlength(); i++) {
MSXML2::IXMLDOMNodePtr parentNodePtr = nodeListPtr->Getitem(i);
MSXML2::IXMLDOMNodePtr childNodePtr = parentNodePtr->selectSingleNode(_bstr_t("productName"));
if(NULL != childNodePtr) {
const BSTR str = childNodePtr->Gettext();
std::cout << util::ConverseBstrToString(str) << std::endl;
}
childNodePtr.Release();
parentNodePtr.Release();

}
nodeListPtr.Release();
}
  1. 가장 먼저 하는 일은 selectNodes() 함수를 이용해 엘리먼트 리스트를 얻어 오는 것입니다.
  2. 엘리먼트 리스트를 얻어 오는 것이 성공하면 IXMLDOMNodeListPtr에 그 값이 저장 됩니다. 만일 엘리먼트가 하나 뿐이거나 기타등등 조건에 맞지 않는 경우 NULL을 리턴하므로 다른 함수도 물론 그렇지만 이 함수를 호출 하고난 후에 NULL체크를 하여 성공 여부를 판단하는 것이 중요 합니다.
  3. 몇 개의 엘리먼트를 얻어 왔는지는 Getlength() 함수를 통해 알아 낼 수 있습니다.
  4. 엘리먼트 리스트는 인덱스를 지정하므로써 각각의 엘리먼트들에게 접근 할 수 있습니다. 이 때 사용되는 함수는 Getitem()이며 파라메터로 인덱스를 받습니다.
  5. 그 외의 나머지 과정의 위에서 설명한 과정과 동일 합니다.

결론
간단하게 나마 msxml 파서의 사용법에 대해서 알아 보았습니다. msxml에 대해서 정확하게 알아 보기 위해서는 DOM과 COM 스펙에 대해 좀 더 조사하고, 위에 언급한 API말고도 수많은 편리함을 제공 해 주는 API들이 많지만 필자가 공부를 하면서 가장 많이 사용했던 API 몇 가지에 대해서만 언급을 하였습니다. 보다 더 많은 정보가 필요하신 분들은 MSDN이나 W3C에 방문하셔서 검색해 보시면 심도 있는 정보를 얻으실 수 있으실 겁니다.

전체 소스 보기


Ref.

저작자 표시 비영리 변경 금지
TAG xml
Posted by 考えの進化 RosaGigantea

출처 : http://blog.naver.com/baek2187/150110320251

codesign.zip

1. ActiveX 만들기

 

비주얼스튜디오에서 ActiveX 를 만드는 방법은 두가지가 있다.

응용프로그램 마법사로 프로젝트를 이용하여 MFC ActiveX Control 프로젝트 또는 ATL Project 프로젝트로 만든다.

  1. MFC ActiveX Control
    • 프로젝트 생성하고 빌드하면 .OCX 라는 DLL이 만들어진다. 이 OCX 파일을 CAB에 포함하여 웹페이지에 링크하면 된다.
    • MFC를 이용하여 컨트롤을 개발할 경우에는 OLE를 전혀 모르는 프로그래머들도 원하는 형태의 OCX 컨트롤을 구현하기 쉽다.
    • 단 지, MFC로 ActiveX를 만들면 코드 사이즈가 커지며, 그것은 MFC가 컨트롤에 대해서 COleControl 클래스를 기반으로 생성해 주기 때문에 기존의 OLE 컨트롤 사양에서 필수적으로 구현해 주도록 한 부분을 모두 포함하고 있기 때문이다. 또한, MFC의 부수적인 Run-time DLL도 오버헤드로 작용할 수 있다.
    • CAB 배포시 MFC관련 DLL을 함께 포함하고 inf 파일에 명시해야 한다.
  2. ATL Project
  • MFC 프로젝트에 비해 배포에 필요한 DLL 용량이 작아서 효율적이다.
  • MFC 프로젝트에 비해 컨트롤 구현을 ATL로 직접 구현해야 하기 때문에 비교적 힘들다.
  • MFC를 이용한 UI가 거의 없고 배포 용량을 줄이는 프로젝트 구현에 적합하다.

MFC 기반으로 ActiveX 만들기

  1. MFC ActiveX Control 프로젝트를 생성한다.
  2. 프로그래밍후 빌드하면 OCX 파일이 생성된다.
  3. MFC 메서드와 HTML과 메서드 커뮤니케이션시에 안전하지 않는 컨트롤 실행에 대한 윈도우 경고창이 뜰 경우 안정성을 보장하는 컨트롤 을 제작한다.

개발 참조:

ActiveX 강좌

MFC ActiveX 컨트롤 만들기

Visual studio 2008 에서 ActiveX만들기

ATL 기반으로 ActiveX 만들기

  1. ATL 프로젝트를 생성한다.
  2. 솔루션의 프로젝트에서 Add Class로 ATL Control을 하나 추가한다. (아래는 추가시 옵션이다)
    1. 추가시 Options
      • Control type: 보통은 Standard control로 설정한다. Windows Form과 비슷한 컨트롤을 작성하려면 Composite control을, DHTML을 호스팅해서 UI를 구성하려면 DHTML을 사용한다.
      • Minimal control: 최소한의 COM 인터페이스만을 구현하도록 한다. 이 예제는 IE에서만 동작하면 되므로 체크해 두었다. VB나 VC등으로 만든 다른 COM Host어플리케이션에서 사용되어야 하는 컨트롤이라면 체크하지 말자.
      • Connection points: 컨트롤이 외부에 이벤트를 공개해야 한다면 선택하자. 예제에서는 이벤트 사용 예제를 위해 체크해 두었다.
    2. 추가시 Interfaces
      • IObjectSafety: 컨트롤이 스크립트, 초기화 등에 안전함을 컨테이너가 인식할 수 있도록 한다.
      • IPropertyNotifySink: 컨트롤에서 변경된 속성을 즉시 사용할 수 있도록 해 준다.
      • IProvideClassInfo2: 이벤트 사용을 위해서는 반드시 필요하다. Connection Point를 구현하는 경우라면 반드시 포함시키자.
      • IQuickActivate: 콘트롤을 보다 빨리 활성화 시킨다.컨테이너는 컨트롤에 구현된 IQuickActivate::QuickActive메서드를 호출 QACOTAINER구조체정보를 매개변수로 컨트롤에 넘겨준다.
      • ISupportErrorInfo: 컨트롤에서 발생한 예외(Exception)을 컨테이너가 처리할 수 있도록 해 준다.

개발 참조:

ATL ActiveX 만들기 - Part1. 프로젝트 구성

ATL ActiveX 만들기 - Part2. 컨트롤 구현

ATL ActiveX 만들기 - Part3. 이벤트(Connection point) 구현

ATL ActiveX 만들기 - Part4. 관리자권한 UAC Elevation

안정성을 보장하는 ActiveX 컨트롤 만들기

MFC는 기본적으로 위자드에서 생성된 컨트롤에 스크립팅과 컨트롤 초기화를 위한 안전처리가 되어있다. IE에서 보안이 보통 이상인 경우 컨트롤이 안전하지 않다는 메시지가 출력될 것이다.

해결하는 방법은 두 가지로, IObjectSafety 인터페이스를 구현하는 것이고 다른 하나는 컨트롤의 DLLRegisterServer 함수안에 레지스트리에 안전표시를 하도록 하는 것이다.

HOWTO: MFC 컨트롤을 스크립트 사용에 안전(Safe for Scripting)/초기화에 안전(Safe for Initialization)으로 표시

안정성을 보장하는 ActiveX 컨트롤 제작

ActiveX Control 보안처리

액티브X 생성에서 배포까지

디버거 설정 및 디버깅하기

ActiveX 컨트롤은 웹페이지상에서 실행되기 때문에 일반 어플리케이션과는 디버깅을 다르게 설정해야 한다.

  1. 프로젝트 속성(Property)에서 Debugging - Debugger to launch 에서 Web Browser Debugger를 선택한다.
  2. HTTP URL 부분에 $(ProjectDir)컨트롤이름.htm 설정한다.
  3. F5로 디버거를 실행하면 브라우저가 뜨고 ActiveX 가 작동하며 디버깅이 가능해진다.

$(ProjectDir)는 프로젝트 파일이 저장되어있는 로컬 경로를 의미하며, 이곳에는 만들려고 하는 컨트롤의 OBJECT태그가 포함된 컨트롤이름.htm파일이 자동으로 생성되어 있다.

ActiveX 테스트를 위한 ActiveX Control Test Container 사용 방법

Visual Studio 2008 에는 작성된 Active X를 테스트를 위한 ActiveX Control Test Container 툴(TstCon32.exe)이 포함되어있지 않습니다.

VS 6.0에서는 기본 툴로 제공 되었으나 닷넷부터는 제공되지 않아 소스만 구할수 있으므로 빌드해야 한다.

첨부 파일에 실행파일만 첨부했지만 소스가 필요하다면 위에 제목으로 링크따라 가면 다운로드할 수 있다.

이제 ActiveX 프로젝트를 디버깅할 때 Executable For Debug Session 창의 Executable file name에 C:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools\TstCon32.exe 를 선택하여 사용 가능하다. (나중에 project property 창에서 Debugging > Command 에서 변경할 수 있다.)
C:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools 폴더에 아래 첨부파일을 넣어서 사용한다.
또는, ActiveX를 Regsvr32 명령을 통해 OS에 등록 하신 후, 직접 TstCon32.exe 실행 후 Edit->insert New Controls ... 를 이용하시면 된다.

2. INF(.inf) 파일 만들기
섹션 편집

INF 파일은 컨트롤을 실행하거나 다운로드해야 하는 파일(DLL 또는 기타 OCX)을 지정하는 텍스트 파일이다. INF 파일을 사용하면 필요한 모든 파일을 하나의 압축된 CAB 파일로 묶을 수 있다. 기본적으로 사용자의 하드 디스크에 있는 기존 파일과 버전 번호가 같은 파일은 다운로드하지 않는다.

CAB 파일을 생성하기에 앞서 먼저 INF 파일을 만들어야 한다. INF 파일에는 Control과 함께 클라이언트측으로 다운로드 되어야 하는 파일들에 대한 정보가 저장된다. 비주얼 베이직은 자동으로 생성해주지만 VS일 경우 수동으로 메모장을 이용하여 작성해야 한다.

아래 내용은 inf 파일 예제이다. text.ocx 파일이 ActiveX 파일이며 기타 dll은 MFC 실행에 필요한 DLL 파일이다.

INF 파일 작성에 대한 자세한 내용은 아래 참조 링크를 보면 자세히 나와있다.

; ========================= test.inf ========================

; This .inf file will control the installation of the MFC test
; control. This control has been compiled with Visual C++ version 4.2.
; The FileVersion tags in the dependent DLLs section on this file
; reflect this requirement.

[version]
; version signature (same for both NT and Win95) do not remove
signature="$CHICAGO$"
AdvancedINF=2.0

[Add.Code]
test.ocx=test.ocx
; These are the necessary supporting DLLs for MFC 4.2 ActiveX Controls
mfc42.dll=mfc42.dll
msvcrt.dll=msvcrt.dll
olepro32.dll=olepro32.dll

; dependent DLLs
[msvcrt.dll]
; This is an example of conditional hook. The hook only gets processed
; if msvcrt.dll of the specified version is absent on client machine.
FileVersion=6,0,8168,0
hook=mfc42installer

[mfc42.dll]
FileVersion=6,0,8168,0
hook=mfc42installer

[olepro32.dll]
FileVersion=5,0,4261,0
hook=mfc42installer

[mfc42installer]
file-win32-x86=<LINK TYPE="GENERIC" VALUE="http://activex.microsoft.com
/controls/vc/mfc42.cab">http://activex.microsoft.com/controls/vc
/mfc42.cab</LINK>
; If dependent DLLs are packaged directly into the above cabinet file
; along with an .inf file, specify that .inf file to run as follows:
;InfFile=mfc42.inf
; The mfc42.cab file actually contains a self extracting executable.
; In this case we specify a run= command.
run=%EXTRACT_DIR%\mfc42.exe

; thiscab is a keyword which, in this case, means that test.ocx
; can be found in the same .cab file as this .inf file
; file-win32-x86 is an x86 platform specific identifier
; See the ActiveX SDK - ActiveX Controls - Internet Component Download -
; Packaging component code for automatic download

[test.ocx]
file-win32-x86=thiscab
; *** add your controls CLSID here ***
clsid={0D886696-C7CE-11D3-A175-08002BF17507}
; Add your ocx's file version here.
FileVersion=1,0,0,1
RegisterServer=yes

세미콜론(;) : 주석문을 표시한다.
"[Add.Code]" : 설치할 파일들을 나열한다. inf 내에 등록 하려는 ocx 는 [Add.Code] 섹션 맨 위 제일 위에 적어준다. 설치되는 순서가 중요하다.

"hook"은 설치할 방법을 설정하는 부분의 Title을 입력한다.

"thiscab"은 이 INF를 포함하는 CAB 파일을 의미하는 키워드이다.

"file-win32-x86"에 thiscab 대신 아래와 같이 절대 경로 또는 상대 경로를 지정하여 HTTP 위치에서 필요한 DLL을 다운로드할 수 있다.

file-win32-x86=http://example.microsoft.com/mydir/NEEDED.DLL 


FileVersion은작성한 OCX 파일의 버전을 입력(Resource에서 설정한 값과 같이 입력)한다. 설명상 FileVersion은 ActiveX 파일을 작성하여 배포하던 중 버그를 발견하여 수정하여 재 배포를 할 때 이 파일 버전을 높여 주면 Client에서 알아서 다시 다운로드를 받는다. 버전 표기시 쉼표로 꼭 구분해준다. (제대로 다운로드 되지 않음)

Windows 탐색기에서 마우스 오른쪽 단추로 클릭하여 파일의 버전 번호를 확인할 수 있다.

"DestDir"은 파일을 로드할 디렉터리이며, 11은 WINDOWS/SYSTEM 또는 WINNT/SYSTEM32 시스템 디렉터리를 지정하며, 10은 WINDOWS 또는 WINNT Windows 디렉터리를 지정합니다. 일반적으로 DestDir는 지정하지 않는데, 그런 경우 코드는 고정된 OCCACHE 디렉터리에 설치됩니다.

위의 내용 중 test를 자신이 작성한 ActiveX 컨트롤의 이름으로 변경하고

"clsid"는 설치될 컨트롤의 CLSID입니다.

clsid={0D886696-C7CE-11D3-A175-08002BF17507} 를 자신이 작성한 컨트롤의 clsid로 수정한다.

clsid는 ActiveX 컨트롤을 만든 디렉토리의 .idl 파일을 열어 가장 마지막에 나와 있는 제작한 ActiveX 컨트롤의 id를 입력하면 된다.

CLSID란???
Class Identifier로서 COM 개체를 식별하는데 사용되는 GUID 값이다. 그렇다면 GUID란 또 무엇인가? GUID(Globally Unique Identifier)란 128비트의 크기를 갖는 구조체로서 전세계적으로 시간과 장소에 관계없이 고유하다고 보장할 수 있는 어떤 값을 나타내는 식별자로 사용된다. UUID(Uinversally Unique Identifier)라고도 한다. COM에서 Interface와 개체의 고유성을 보장하기 위해 사용되는 값이라고 알아두자. 더욱 자세한 내용을 알고 싶은 독자는 COM 관련 서적이나 MSDN을 참고하길 하란다.

INF 파일을 만들어서 아래 CAB 파일 생성에 포함시켜야만 배포시 웹페이지에서 ActiveX 다운로드할 때 참조된다.

참조 링크:

CAB 파일 만들기.

Code Sign과 배포

CAB 파일 작성법

3. 디지털 서명(Code Sign)된 CAB 파일 만들기
섹션 편집

ActiveX Control을 만들어서 웹페이지에 올리기 위해서는 반드시 Code Sign을 해야한다.간단히 말해 Code Sign은 보안 문제 때문에 필요한 작업이다.

웹페이지에 올려진 Control은 Client의 시스템에 설치된 후 실행되기 때문에 항상 위험성을 내포하고 있다.

따라서 Internet Explorer는 Control을 다운로드 받을때 Control이 디지털 서명(Digital Signature)을 가지고 있는지를 검사한다.

디지털 서명이란 해당 Control을 제작한 회사를 나타내는 바이트 문자열이다. 또한 Control은 디지털 서명을 확인할 수 있도록 보안증(Certificate)이라고 하는 또 다른 바이트 문자열을 제공해야 한다.

개발기간중에는 임시로 테스트용 디지털 서명을 만들어서 사용할 수 있다.

정식 서비스 배포시에는 유료로 제공되는 인증된 디지털 서명 파일이 필요하다.

CAB 파일 생성과 디지털 서명에 필요한 툴입니다. (첨부파일에 있음)

CabArc.exe: Cabinet Archive(CAB)파일을 생성해 주는 프로그램.
SignCode.exe: CAB 파일을 비밀키와 인증서로 싸인해 줍니다. CAB파일에 코드사인을 합니다.
ChkTrust.exe: 코드사인 작업이 잘 수행되었는가 검증해 주는 프로그램 입니다.
MakeCert.exe: 테스트용 인증서를 만들어 주는 프로그램 입니다.
Cert2Spc.exe: MakeCert로 만들어진 시험용 인증서를 사인하는 작업에 사용될 수 있도록 변환하는 프로그램.

  1. 배포에 필요한 CAB 파일 생성
    • CABARC -s 6144 n MYCTL.CAB NEEDED1.DLL NEEDED2.DLL MYCTL.OCX MYCTL.INF
    • CABARC는 MYCTL.CAB라는 CAB 파일을 만듭니다.
    • 소스 파일(INF, OCX 및 DLL 파일)이 들어 있는 디렉터리에서 CABARC를 실행해야 한다.
    • CAB 파일에 보관될 파일은 INF 파일에 나열된 순서대로 명령줄에 나열되어야 한다.
    • 위 예제의 INF 파일에서는 NEEDED1.DLL, NEEDED2.DLL, MYCTL.OCX의 순서로 나열되어야 한다.
    • -s 옵션은 캐비닛에코드 서명을 위한 공간을 예약한다. n 명령은 CAB 파일을 만든다는 것을 지정한다.
    • 라인 맨 뒤에 INF 파일은 위에서 CAB 파일을 위해 수동으로 만들어진 .inf 파일이다.
  2. 디지털 서명 (테스트) 파일 만들기 (유료 인증된 디지털 서명 파일이 있을 경우 테스트용으로 만들 필요 없음)
    • makecert -n "CN=MySoftwareCompany" -sv MyKey.pvk MyCert.cer
    • pvk 라는 개인 키 파일과 .cer 라는 회사 인증서 파일을 테스트용으로 만든다.
    • 생성시 개인 키 암호를 물어보는데 아무거나 설정하면 되면 나중에 CAB파일에 코드 사인할때 입력해야 한다.
  3. 디지털 서명 cer 파일로 spc 파일 만들기
    • cert2spc MyCert.cer MyCert.spc
    • 테스트를 위한 SPC(소프트웨어 게시 인증서)를 만든다.
  4. CAB 파일에 디지털 서명 파일로 코드사인하기
    • signcode -v MyKey.pvk -spc MyCert.spc MYCTL.CAB
    • 개인 키 암호 입력 대화 상자가 생성되고 위 과정에서 입력한 암호를 입력하면 .cab 파일에 디지털 서명한다.
  5. 테스트 디지털 서명이 인식되도록 클라이언트 시스템에 설정하기 (한번만 하면 됨)
    • SETREG 1 TRUE
    • 해당 클라이언트에서 보증 확인 과정을 제어하는 레지스트리 키를 설정하는 것이다.
  6. 디지털 서명된 CAB 파일 이상없는지 확인하기
    • chktrust MYCTL.CAB

(MSDN) 다음은 서명된 CAB 파일을 만드는 단계입니다.

  1. Software Publisher Certificate 얻기(한 번만 수행하면 됨)
  2. CAB 파일 만들기.
  3. CAB 파일 서명.
  4. 웹 페이지에 서명된 CAB 파일 포함(선택적)

참조 링크:

MFC 및 ATL 컨트롤을 위한 서명된 CAB 파일 만들기

Code Sign과 배포

CAB 파일 작성법

배포하기(Cab 파일 작성법)

signcode.exe 사용법 : DOS 버전
signcode.exe 사용법 : 윈도우 버전

cabarc.exe사용법 : CAB화일 제작

테스트용 디지털 서명된 ActiveX 실행할 때 보안 설정

정식 디지털 서명되지 않고 테스트 서명된 ActiveX는 보통 이상으로 보안 설정된 일반 유저의 IE에서는 보안상 실행되지 않는다.

임시 테스트를 하기 위해선 아래와 같은 방법이 있으며 유저들에게 정식으로 배포하기 위해서 공인된 디지털 서명 제공 업체에서 유료로 결제 후에 공인된 디지털 서명을 받아서 배포해야만 한다.

  • 인터넷 옵션의 보안 - 보안 수준 에서 보안 설정을 최하로 변경한다. (서명되지 않은 ActiveX도 다운로드 허용된다.)
  • 인터넷 옵션의 보안 - 신뢰할 수 있는 사이트 - 사이트 에서 해당 사이트를 신뢰하는 사이트로 설정한다.
  • 인터넷 옵션의 보안 - 사용자 지정 수준 - 보안 설정 에서 "서명 안 된 ActiveX 컨트롤 다운로드" "사용 안 함"에서 "확인"으로 변경해야 한다. 일반적으로 기본은 "사용 안 함 (권장)"으로 설정되어 있다.

인증된 디지털 서명 유료 구매하기

디저털 서명은 디지털 서명을 다루는 공인 기관에서 유료로 발급할 수 있다.

  • VeriSign CodeSign : 해외 인증기관. 해외에서 가장 높은 브랜드 : 1년 440000원 / 2년 850000원
  • Thawte CodeSign : 국내에서 가장 많이 사용. 비교적 저렴한 가격 : 1년 180000원 / 2년 320000원
  • Trust CodeSign : 상당히 저렴한 가격 1년 150000원 / 2년 280000원
  • YesSign CodeSign : 금융결제원에서 제공하며 금융관련에서 주로 사용 : 1년 150000원 / 2년 280000원

인증된 디지털 서명 업체

써트 코리아

애니써트

디지털 서명 가이드

자세한 인증서 가이드

4. 웹페이지에 CAB 파일 추가
섹션 편집

ATL 및 MFC 컨트롤은 <OBJECT> 태그를 사용하여 웹 페이지에 포함됩니다. <OBJECT> 태그 안에는 다음과 같은 컨트롤의 세 가지 특성을 지정해야 합니다.

HTML 파일에서 아래와 같이 ActiveX 컨트롤을 끼워 넣는다.

<OBJECT ID="PolyCtl"

CLASSID="CLSID:4CBBC676-507F-11D0-B98B-000000000000"

CODEBASE="http://example.microsoft.com/mydir/polygon.cab">

</OBJECT>

  • ID 컨트롤의 이름
  • CLASSID 컨트롤의 CLSID (위에서 INF 설정시 ActiveX 컨트롤의 CLSID 값과 동일)
  • CODEBASE 컨트롤을 다운로드할 위치. CODEBASE는 다양한 파일 형식을 가리킬 수 있습니다.

CODEBASE는 다음과 같이 OCX 또는 DLL 파일을 직접 지정할 수 있습니다.

CODEBASE="http://example.microsoft.com/mydir/polygon.dll#version=1,0,0,1"

이렇게 하면 DLL 또는 OCX 파일만 다운로드하여 설치하기 때문에 필요한 모든 지원 DLL이 클라이언트 컴퓨터에 있어야 합니다.

CAB 파일에 선택적인 버전 번호를 포함시킬 경우, 해당 버전 번호는 다운로드될 컨트롤을 참조해야 합니다. 예를 들어, POLYGON.DLL의 버전 번호가 1, 0, 0, 1이므로 캐비닛의 버전 역시 1, 0, 0, 1입니다.

CODEBASE="http://example.microsoft.com/mydir/polygon.cab#version=1,0,0,1"

버전 번호를 포함시키지 않을 경우, 동일한 컨트롤의 이전 버전이 클라이언트 컴퓨터에 있어도 새 버전으로 바뀌지 않습니다.

참조 링크:

웹 페이지에 서명된 CAB 파일 포함 (MSDN)

기존 ActiveX 컨트롤 업그레이드 (MSDN)

5. 문제가 생길 경우
섹션 편집

* ActiveX 제작 및 배포 시 참고 및 주의 사항들
- "DestDir은(는) Windows 디렉토리인 경우 10, WindowsSystem(32)인 경우 11이며, Occache 디렉토리인 경우 비워둡니다."

inf 안에 적어주는 배포 되는 파일의 위치입니다.
예)
[test.exe]
file-win32-x86=thiscab
FileVersion=1,0,0,1
DestDir=11

- inf 내에 등록 하려는 ocx 는 [Add.Code] 섹션 맨 위 제일 위에 적어준다.
- 버전 표기 시 쉼표로 꼭 구분해준다. -> 제대로 다운로드 되지 않음
예)
- inf 내 버전 표기 : 2005,4,12,1
- html 페이지 내 : codebase="npos.cab#version=2005,4,21,1"
- 모든 파일이 제대로 다운로드 되었는지 확인해보세요. 가끔 파일이 중간에 다운로드 받다가 다 받지 않은채 손상이 되는 경우가 있더군요.
- 익스플로러 도구-옵션-임시 인터넷 파일-저장된 페이지의 새버전 확인을 페이지를 열 때 마다로 해서 캐쉬 되어 있는 기존 파일을
참조하지 않도록 해주세요.
- 가끔 보니까 downloaded program files 에서 해당 ocx 를 지워도 지워지지 않는 경우가 있는데
regsvr32.exe 로 해제를 해주고 다시 다운로드 받아서 테스트하니 잘 되더군요.

- 그리고 ocx 내부에서 파일을 참조하거나 할 때 예외(파일이 없거나 파일 생성, 삭제 시 예외 발생)가 발생하면 익스플로러가 죽더군요.

- MSDN 에서 본 내용인데 cab 을 묶으실 때 [Add.Code] 내에 놓여져 있는 파일 순서대로 묶어주셔야 한다는겁니다. 아래는 MSDN 원문입니다.

You should run CABARC in the directory that contains your source files (the INF, OCX, and DLL files). The files to be archived in the CAB file should be listed on the command line in the same order they are listed in the INF file. In the example above, the INF file should list NEEDED1.DLL first, then NEEDED2.DLL, and then MYCTL.OCX.

- INF 파일목록에 [Add.Code] 나열된 순서대로 처리를 하지만, 파일들의 설치는 목록의 역순으로 실행된다. 보통 메인 OCX를 파일목록의 첫번째에 두고 참조dll을 그뒤에 두는데, 이것은 메인OCX의 등록시점에 참조dll들의 설치완료를 보장하기위한 것이다.

출처:INF파일

ActiveX 컨트롤이 웹페이지에서 정상적으로 다운로드 안될 경우 로그 보는법 (MSDN)

Code Down Load Log Viewer (cdllogvw.exe) 파일 다운로드

저작자 표시 비영리 변경 금지
TAG ActiveX
Posted by 考えの進化 RosaGigantea

서버 관련 함수중

mswsock.h 안에 WSAID_CONNECTEX 라던가, LPFN_CONNECTEX 같은것이 있습니다.

 

문제는 vs2010에서 이전 vs2005에서 작업한걸 불러오면 여기서 정의되지 않은 심볼이라고 에러 나오더군요.

 

코드 정의창에선 나오는데 왜 그런가 하고 보다가, 위에 보니

 

#if(_WIN32_WINNT >= 0x0501)

 

로 해놨더군요... orz

 

참고로 프로젝트에선 #define _WIN32_WINNT    (0x0500) 로 정의 되었으니...

그 이하 정의된 부분이 다 스킵되어서 컴파일이 안되었던 거였습니다.. ㅠㅠ

 

해결을 위해 위의 0x500 을 0x501로 바꿧습니다.. 

 

 

참고로 MS에선 각 OS마다 사용하는 라이브러리를 구분하기 위해 위의 정의문이 각각 있습니다.

Minimum system required

Macros to define

Windows Server 2003

_WIN32_WINNT>=0x0502

WINVER>=0x0502

Windows XP

_WIN32_WINNT>=0x0501

WINVER>=0x0501

Windows 2000

_WIN32_WINNT>=0x0500

WINVER>=0x0500

Windows NT 4.0

_WIN32_WINNT>=0x0400

WINVER>=0x0400

Windows Me

_WIN32_WINDOWS=0x0500

WINVER>=0x0500

Windows 98

_WIN32_WINDOWS>=0x0410

WINVER>=0x0410

Windows 95

_WIN32_WINDOWS>=0x0400

WINVER>=0x0400

Internet Explorer 6.0

_WIN32_IE>=0x0600

Internet Explorer 5.6

_WIN32_IE>=0x0560

Internet Explorer 5.01, 5.5

_WIN32_IE>=0x0501

Internet Explorer 5.0, 5.0a, 5.0b

_WIN32_IE>=0x0500

Internet Explorer 4.01

_WIN32_IE>=0x0401

Internet Explorer 4.0

_WIN32_IE>=0x0400

Internet Explorer 3.0, 3.01, 3.02

_WIN32_IE>=0x0300

 

OS 이외에 컴파일러에 대한 정의문도 따로 있습니다.

 

MS Visual C++의 컴파일러 자체 매크로 상수


VC++4.0: _MSC_VER = 1000 ( Visual C++ 4.x )
VC++5.0: _MSC_VER = 1100 ( Visual C++ 5 )
VC++6.0: _MSC_VER = 1200 (Visual C++ 6 )
VC++7.0: _MSC_VER = 1300 (Visual C++ .NET )
VC++8.0: _MSC_VER = 1400( Visual C++ .NET 2005 )
VC++9.0: _MSC_VER = 1500( Visual C++ .NET 2008 )

 

저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea

정말... 마이그레이션 하나 하는거 힘드네요 ㅠㅠ

이번엔 몇몇 라이브러리에서

 

error C2825: '_Fty': '::'가 뒤에 나오면 클래스 또는 네임스페이스여야 합니다.

이런식으로 뭔가 알지도 못하는 xxresult(28) 파일을 찾아봐염

 

에러가 떨어졌습니다.

 

아무리 이 파일 안을 봐도 뭔가 복잡한 코드라는것 밖에 알 수 없어서

이넘이 컴파일 하다 죽은 소스 부분을 살펴보다가 이런 부분이 나오더군요.

 

error C2678: 이항 '==' : 왼쪽 피연산자로 'std::tr1::_Bind_fty<_Fty,_Ret,_BindN>' 형식을 사용하는 연산자가 없거나 허용되는 변환이 없습니다.
1> with
1> [
1> _Fty=SOCKET,
1> _Ret=std::tr1::_Notforced,
1> _BindN=std::tr1::_Bind2<std::tr1::_Callable_obj<SOCKET,false>,sockaddr *,unsigned int>
1> ]
1> C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\exception(470): 'bool std::operator ==(const std::_Exception_ptr &,const std::_Exception_ptr &)'일 수 있습니다.
1> C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\exception(475): 또는 'bool std::operator ==(std::_Null_type,const std::_Exception_ptr &)'
1> C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\exception(481): 또는 'bool std::operator ==(const std::_Exception_ptr &,std::_Null_type)'
1> C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\system_error(408): 또는 'bool std::operator ==(const std::error_code &,const std::error_condition &)'
1> C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\system_error(416): 또는 'bool std::operator ==(const std::error_condition &,const std::error_code &)'
1> C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\include\guiddef.h(192): 또는 'int operator ==(const GUID &,const GUID &)'
1> 인수 목록 '(std::tr1::_Bind_fty<_Fty,_Ret,_BindN>, int)'을(를) 일치시키는 동안
1> with
1> [
1> _Fty=SOCKET,
1> _Ret=std::tr1::_Notforced,
1> _BindN=std::tr1::_Bind2<std::tr1::_Callable_obj<SOCKET,false>,sockaddr *,unsigned int>
1> ]

 

 

이게 뭔말인고 하니

#include <Winsock2.h> 에서 정의된 bind 랑

#include <functional> 에서 정의된 bind가 같은 네임스페이스 상에 있어서 충돌하는거라 하네요..

 

자세한건

  https://connect.microsoft.com/VisualStudio/feedback/details/500364/how-to-avoid-conflicts-between-tr1-bind-and-winsock-bind-function

에 설명하고 있습니다.

 

뭐 어쨋든.. 저걸 해결하기 위해서 stl이 아닌 bind를 쓰고 싶으면 bind -> ::bind로 바꿔서

지금 bind는 소켓 통신용이다 라는걸 알려줘야 한다는군요.....

 

ㅡㅡ.... 헐..... vs2010.....

 

저작자 표시 비영리 변경 금지
TAG 디버그
Posted by 考えの進化 RosaGigantea

출처 : http://vsts2010.net/411

 

VS.NET(VS2002)에서MFC 프로젝트로 만들었던 프로그램을 VC++10 프로젝트로 변환하여 컴파일 했더니 에러가 발생하면서 아래의 경고가 나왔습니다.

C:\Program Files\Microsoft Visual Studio 10.0\VC\atlmfc\include\atlcore.h(35):#error This file requires _WIN32_WINNT to be #defined at least to 0x0403. Value 0x0501 or higher is recommended.

에러 내용은 프로젝트에서 정의된 _WIN32_WINNT 버전이 0x403인데atlcore.h 버전이 최소 0x0501 이상이 되어야 한다는 것입니다.

그래서 _WIN32_WINN 정의한 stdafx.h 파일을 열어보니

#define _WIN32_WINNT 0x0400

되어 있었더군요. 그래서 일단 이것을 최신이 좋다라는 생각에 아래와 같이 했습니다. ^^;;

#define _WIN32_WINNT 0x0600

그랬더니 이제는 아래와 같은 에러가 나오더군요. -_-;

c:\program files\microsoft visual studio 10.0\vc\atlmfc\include\afxcmn3.inl(29): error C2065: 'CCM_SETWINDOWTHEME' : 선언되지 않은 식별자입니다.

그래서 바로 구글링 들어갔습니다.

쉽게 저와 같은 에러가 나와서 질문을 올린 글을 찾았고 답변도 보았습니다.

문제 해결은 stdafx.h 파일에 정의된 버전의 숫자를 아래와 같이 하면 된다고 하더군요

// Modify the following defines if you have to target a platform prior to the ones specified below.
// Refer to MSDN for the latest info on corresponding values for different platforms.
#ifndef WINVER // Allow use of features specific to Windows 95 and Windows NT 4 or later.
#define WINVER 0x0501 // Change this to the appropriate value to target Windows 98 and Windows 2000 or later.
#endif

#ifndef _WIN32_WINNT // Allow use of features specific to Windows NT 4 or later.
#define _WIN32_WINNT 0x0501 // Change this to the appropriate value to target Windows 98 and Windows 2000 or later.
#endif

#ifndef _WIN32_WINDOWS // Allow use of features specific to Windows 98 or later.
#define _WIN32_WINDOWS 0x0501 // Change this to the appropriate value to target Windows Me or later.
#endif

#ifndef _WIN32_IE // Allow use of features specific to IE 4.0 or later.
#define _WIN32_IE 0x0500 // Change this to the appropriate value to target IE 5.0 or later.
#endif

이렇게 하니 문제 없이 빌드가 성공 되었습니다.

주위에서 VC++의 새로운 버전이 나와도 쉽게 사용하지 못하는 경우가 오래 전에 만들었던 프로젝트를 포팅 할 수 없어서 이전 버전을 어쩔 수 없이 사용한다는 이야기를 종종 듣습니다.

그러나 저는 운이 좋아서인지 2002버전부터 순차적으로 새 버전의 VC++을 사용할 수 있어서 VC++6에서 VS2002로 넘어갈 때만 빌드 문제를 겪었습니다.

그래서 이런 포팅에 대한 문제는 잘 알지 못합니다. 이번에는 예전에 만들었던 코드를 C++0x 코드로 바꾸고 싶어서 오래 전에 만들었던 프로젝트를 VC++ 10로 포팅하면서 정말 정말 오랜만에 이런 문제를 겪어 보게 되고 해결 방법을 포스팅 할 수 있었습니다.

혹시 앞으로 또 이런 경우가 발생하면 바로 바로 공유하도록 하겠습니다.

 

저작자 표시 비영리 변경 금지
TAG 디버그
Posted by 考えの進化 RosaGigantea

회사에서 작업으로 받은 컴퓨터가 더 이상 작업하는데 인내심을 요구해서

(팬4 RAM 2GB ... ㅡㅡ... 아무리 장인은 연장탓을 안한다곤 하지만.. 기획자들은 i7에 ssd 보급해주면서 참...  )

현재 안쓰는 테스팅 컴퓨터가 굴러다니길래 (쿼드Q8300 RAM 5GB) 여기에

윈7 64bit 를 설치하고, 가능한 최신 컴파일러의 힘을 보고싶어서 Visual Studio도 2005 -> 2010으로 올렸습니다.

 

그런데.....

가뜩이나 빌드 환경을 적용 시키고

Lua 의 LNK2001 _lua ... 의 에러도 일일히 lib 파일을 지정 시켜서 서버 프로그램 빌드를 성공 시키는건 좋았습니다만...

 

이걸 테스트 서버 (윈2003) 에서 돌리니 프로그램이 걍 죽어 버리더군요.

 

처음엔 64비트로 컴파일 환경이 바껴서 그런가 해서 컴파일 옵션에서 전부 x86환경으로 맞췄는데 결과는 똑같아서

천상 원격 디버깅 까지 해가면서 원인을 찾아보니

 

CADODataBase::CADODataBase()
{
     ::CoInitialize(NULL);
     m_pConnection.CreateInstance(__uuidof(Connection));
     if( m_pConnection == NULL ){ 
          ASSERT(false && "CoInitialize() 를 먼저 호출해야합니다.");
          LOGCON(_T("[CADODataBase][ERROR][ADODB.Connection]\n"));  
     }
}

 

위의 ASSERT에서 죽어버리더군요.

 

그런데... 이게, 컴파일한 win7환경에선 전혀 문제없이 객체 할당에 성공시키니 뭐가 문제일까 하고 열심히 검색하던중

저랑 같은 증상으로 고생하신 분이 계시더군요.

(참고한분 블러그 입니다. http://icartsh.tistory.com/3)

 

결론은 msado15.dll 이 윈7이전이랑 이후 버젼이 있어서 만약 이후 버젼에서 컴파일 하면 위와 같은 함수에서 객체 생성에 실패할 수 도 있다는 이야기 입니다.

 

ms에선 이렇게 설명하고 있습니다.

http://support.microsoft.com/kb/2517589

 

 C:\Program Files\Common Files\System\ado

 

msado15.dll

 

실제로 저 파일의 타임 스템프가 2010년으로 찍혀 있고, 용량도 2배로 늘어있더군요.

MS가 무슨짓을 했는지 잘 모르지만, 해당 폴더의 원래 있던 msado15.dll 을 msado15_win7.dll 로 바꿔준뒤

저 파일을 다운 받아 재 컴파일 해서 테스트 해보니 이상없이 돌아가네요.

 

ㅜㅜ... 이것땜에 하루가 또... ㅠㅠ

저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea

 

Open VPN쓰려고 하다보니 서버IP (즉 회사에 있는 내 컴퓨터 IP)를 알아내야 접속이 됩니다.

 

보통  IP를 알아내는 방법은.

1. 일반적인 경우 공유기 없이 공인 IP를 사용하는 경우.

 - 도스창을 띄우고 ipconfig 를 실행해서 IP를 확인

 

 

 

2. 공유기를 사용하거나 회사내의 보안을 위해 사설IP를 사용하는 경우 다음 사이트를 이용해서 알아냅니다.

 

 http://www.tracemyip.org/ 

 

 

저작자 표시 비영리 변경 금지
TAG IP, openvpn
Posted by 考えの進化 RosaGigantea

서버 점검하던중 뭔가 설정이 초기화 됬는지,

갑자기 서버 관리자 페이지에서 쿼리가 먹히지 않더군요.   (사용자 'x맨'이(가) 로그인 하지 못했습니다)

 

먼저 서비스 DB에 연결 되어있는건 확인됬고,

다음 게임 컨텐츠 DB에 연결 하려고, 안의 프로시저를 실행해본 결과, 특정 DB에만 접속을 못하고 아래의 에러가 나왔습니다.

"메시지 18456, 수준 14, 상태 1, 서버 <computer_name>, 줄 1"

"사용자 '<user_name>'이(가) 로그인하지 못했습니다."

"메시지 4064, 수준 16, 상태 1,"

사용자 기본 데이터베이스를 열 수 없습니다. 

 

 

결국, 이 얘긴 해당 DB에서 상대쪽 DB에 접근이 안된다는 의미이고,

msdn 의 http://support.microsoft.com/kb/307864/ko 의 도움말을 살펴본 결과

 

상대쪽 DB 보안 -> 로그인 -> 해당 계정의 기본 DB지정에 문제가 있음을 알게 되었습니다.

 

 

결국... 기본 DB를 연결해 주려는 DB로 설정하니 말끔이 해결...

 

... 알고나면 허망한 sql 세계 ㅠㅠ

저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea

출처 : http://tistory.kkwang.com/161

 

 

mssql은 복구를 위해 실제데이터(확장자 mdf)와 트랜잭션로그데이터(확장자 ldf)를 기록합니다.

mdf는 말그대로 디비에 저장되는 실데이터를 말하며, ldf는 트랜잭션로그, 즉 이러한 데이터를 이용한 읽기, 수정, 삭제등의 모든 로그를 기록합니다.

그래서 특정데이터에 대해 입출력이나 업데이트가 반복되는 경우 비정상적으로 ldf파일이 커지는 경우가 발생하게 됩니다. 이러한 일은 정상적인 억세스로 일어날 수도 있으나 잘못된 프로그래밍 또는 오류로 인한 무한루프에 의해 급증하는 경우도 발생하게 됩니다.

 

하드디스크가 매우 여유롭다면 몰라도 이런일로 하디디스크 풀이 나는 장애가 발생된다면 정말 난감해집니다.

사실 ldf파일이 없이도 mdf만으로 디비를 복구하는 방법도 있습니다만 제대로? MS의 정책에 따라 완벽한 복구를 위해서 ldf를 버리지 말고 용량을 잘 조절해서 사용하는 방법을 말씀 드리겠습니다.

 

일단 EM환경보다는 제가 사용하기 편리한 쿼리분석기에서의 작업을 기준으로 설명 드리겠습니다.

일단 쿼리분석기를 실행합니다.

 

### 트랜잭션로그를 백업할 디비를 지정하여 줍니다.

use testdb  — 저는 testDB를 지정한다고 가정하였습니다.

 

### 로그파일의 정보를 확인합니다.

dbcc loginfo

 

### 현재 지정된 디비가 사용하는 mdf 및 ldf파일의 경로, 이름 및 크기를 확인합니다.

exec sp_helpfile

 

### 위에서 정해준 디비의 로그를 백업해 줍니다.

backup Log testdb to disk=’d:\dbbackup\temp\testdb.bak’

go

 

선택 1, ### 트랜잭션 로그파일을 최소의 단위로 축소합니다.

backup log testdb with truncate_only

 

선택 2, ### 트랜잭션 로그파일을 삭제합니다.

backup log testdb with no_log

 

### 트랜잭션 로그파일을 10메가로 생성합니다.

dbcc shrinkfile (testdb_log, 10)

 

### mdf와 ldf파일이 제대로 잘 리사이징 되었는지 확인합니다.

exec sp_helpfile

 

모든 작업이 잘 마무리 되었다면, 이제는 갑작스런 트랜잭션로그의 증가로 문제가 되는 것을 방지하기 위해
트랜잭션로그파일의 최대크기를 지정해놓는 방법도 좋습니다.

alter database testdb

modify file ( name = testdb_log, maxsize = 200 mb )

go

 

위 의 과정을 진행하시면 트랜잭션로그는 위에서 지정한데로 200메가를 한계치로 생성 삭제 됩니다.
트랜잭션로그의 용량은 데이터의 중요도 및 규모에 따라 정책적으로 유지하셔야 하는 부분입니다.
200메가는 예제로 적어놓은 사이즈입니다.

또한 위의 과정 중 축소와 삭제는 둘 중 원하시는 방법을 선택적으로 사용하시면 됩니다.

 

////////////////////////////////////////////////////////////////////////////////////////

 

추가적으로 하다가 트랜잭션 로그 파일을 10메가로 생성할때,

메시지 8985, 수준 16, 상태 1, 줄 1
sys.database_files에서 데이터베이스 'testdb'의 파일 'testdbLog'을(를) 찾을 수 없습니다. 파일이 없거나 삭제되었습니다.

 

메시지가 발생되어 에러가 났었지만,

select * from sys.database_files 

쿼리를 실행시킨후 나온 DB이름을 대입하니 문제없이 실행 되었습니다.

 

저작자 표시 비영리 변경 금지
TAG MS-SQL
Posted by 考えの進化 RosaGigantea

DB를 다른 컴으로 옮길일 이 있어서, 백업을 받은 다음, 그 파일을 다른 컴퓨터에서 복원을 시키려 하는데...

 

같은 이름의 데이터베이스 이름을 만들고, 복원시 덮어 써버리는 옵션을 이용해서 해보려니 가끔

 

restore database is terminating abnormally ... SQL error 3154

 

하면서 실행이 안됬다...

 

뭔가 지웠다 다시 하라니 글들을 실험해 봤지만.. 영...

 

그러다 해외 사이트에서 발견한 아주 간단한 방법..

 

Backup File = mydatabase.bak

 

1. Run Microsoft SQL Server Management Studio application.

2. If mydatabase is in Databases : delete mydatabase.

3. Right Click to Databases and select Restore Database ....

4.    Destination for restore -> To database: mydatabase

       Source for restore -> select From device -> Specify the backup media and select the backup sets to restore

       Select Options from Select a page and in Restore the database file as: type the fullpath for the mydatabase new location

(for initdb_Data line with .mdf extension and for initdb_Log line with .ldf extension 

ex.:

C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\mydatabase.mdf

C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\mydatabase.ldf).

 

5. Press OK button.

 

Have fun!

 

form Ramidava , in http://social.msdn.microsoft.com/Forums/en-US/sqldisasterrecovery/thread/d315d8f4-d7e2-4197-9cbb-4cbe086af33e

 

 

ㅡ_ㅡ.... 오전동안 뻘짓한 시간이.. ㅠㅠ

저작자 표시 비영리 변경 금지
TAG MS-SQL
Posted by 考えの進化 RosaGigantea

출처 : http://youngsam.kr/696

 

EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'

EXEC sp_MSForEachTable 'ALTER TABLE ? DISABLE TRIGGER ALL'

EXEC sp_MSForEachTable 'DELETE FROM ?'

EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'

EXEC sp_MSForEachTable 'ALTER TABLE ? ENABLE TRIGGER ALL'

 

하지만, 그대로 사용하기엔 좀 부족해 보인다.

아래 사항을 추가해서 사용해야겠다.

1.     삭제하지 않을 테이블을 지정할 수 있을 것.

2.     DELETE문 대신 TRUNCATE TABLE문을 사용할 수 있다면… TRUNCATE TABLE문을 쓰도록 분기할 것. (테이블이 VIEWschema binding되어 있다면… DELETE문을 쓸 수 없을 듯)

3.     IDENTITY 속성의 컬럼을 포함한 경우, Reseed 할 것.
이때, 한번도 데이터가 INSERT되지 않았거나 TRUNCATE한 테이블과그렇지 않은 테이블의 Reseed 값을 달리 적용해야 함에 유의!!!



 


EXEC sp_MSForEachTable 'TRUNCATE TABLE ?'
한줄 테이블 삭제
EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'
EXEC sp_MSForEachTable 'TRUNCATE TABLE ?'
EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'
외래키 제약조건 무시, 세줄 테이블 삭제

저작자 표시 비영리 변경 금지
TAG MS-SQL
Posted by 考えの進化 RosaGigantea
2012/03/21 13:07

프로그램 시간 측정 C/C++언어2012/03/21 13:07


아래 예는 윈도우에서 high resolution timer를 이용하여 0.5초를 측정하는 예입니다.
high resolution timer는 나노초 단위까지의 매우 정밀한 시간 측정이 가능합니다.


#include <stdio.h>
#include <windows.h>
main()

 LARGE_INTEGER ticksPerSecond;
 LARGE_INTEGER start_ticks, end_ticks, diff_ticks;  

//시작 하면서 시간 측정
 QueryPerformanceCounter(&start_ticks);
 
//여기서 원래 메인 루프를 돌림

    Sleep(500);    /*  500/1000=0.5 sec  */

 //처리가 끝난뒤의 시간 측정
 QueryPerformanceCounter(&end_ticks);

//cpu 클럭당 경과 시간?   
 QueryPerformanceFrequency(&ticksPerSecond);
 printf ("ticksPerSecond: %I64Ld\n",ticksPerSecond.QuadPart);

//시작했을때 Tick 이랑 끝났을때 tick의 차를 구함
 diff_ticks.QuadPart = end_ticks.QuadPart- start_ticks.QuadPart;
 printf ("ticks: %I64Ld\n",diff_ticks.QuadPart);

//알아보기 힘드니 초로 환산한다.
 printf("Elapsed CPU time:   %.12f  sec\n\n",
  ((double)diff_ticks.QuadPart/(double)ticksPerSecond.QuadPart));
}


출처
http://www.unb.ca/metrics/software/HRtime.html
저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea


그다지 책에는 설명이 안되어있는 virtual 키워드...
솔직히 일본에서 일할때 그쪽에선 특별히 뭔가 상속하면서 클래스를 만드는걸 꺼려서해 그런지...
virtual 키워드를 왠만하면 쓰지 않았기 때문에 프로그래밍으로 밥먹고 사는 저도 가물가물한 놈입니다.

잡설은 그만하고
virtual 과 override 차이만 얘기 합니다.
만약

/************************************************************************/
/*상위클래스                                                            */
/************************************************************************/
class upClass
{
public:
  void print(){
  cout<<"up class"<<endl;
 }
};
/************************************************************************/
/* 상속 A                                                               */
/************************************************************************/
class aClass : public  upClass
{
public:
 void print(){
  cout << "aClass"<<endl;
 }
};
/************************************************************************/
/* 상속 B                                                               */
/************************************************************************/
class bClass : public upClass
{
public:
 void print(){
  cout <<"bClass"<< endl;
 }
};

이렇게 해놓았다면
AClass 클래스를 만들어 print()를 호출할때 상속 받은 UpClass의 print함수가 아닌
AClass의 print 함수를 불러오는건 프로그래머로썬 상식입니다.

그리고 이걸 AClass함수가 원래 UpClass를 상속해 존재한 같은 이름의 print 함수를 덮어 씌었기 때문에 override라고 합니다.

보통 프로그래밍 할땐 문제가 없지만
만약 경우에 따라 A,B클래스를 따로 부르고 싶을땐 문제가 발생합니다.

예를들어
//메인 함수
void main()
{
 upClass *ptr;                       //aClass를 부를지, bClass를 부를지 모르니까 둘의 공통 분모 클래스의 포인터 생성
 int num = 100;                     //적당한 조건문
//조건에 따라서
 if(num < 10){
  ptr = new aClass();           //aClass를 생성
 }else{
  ptr = new bClass();           //bClass를 생성
 }
 ptr->print();                       //위 조건에 따라 분기된 클래스의 print()함수를 실행
 delete ptr;
}

이렇게 하면


이런 결과가 나옵니다.
분명히 조건에 따라 bClass를 만들었는데 bClass의 print에서 정의한 cout << "bClass" << endl; 의 결과가 나오지 않습니다.

이런 문제를 해결하기 위해 쓰는것이 virtual 키워드 입니다.
컴파일러에게, 만약 이녀석 함수가 불러질때, 상속된 녀석이 이 함수를 오버라이딩 했을 가능성이 있으니 상속된 함수라면
정말 오버라이딩 했는지 확인한뒤 처리 하란 뜻입니다.

즉, 위의 상위 클래스에서 virtual를 추가하고 다시 컴파일 해보면

/************************************************************************/
/*상위클래스                                                            */
/************************************************************************/
class upClass
{
public:
  virtual void print(){
  cout<<"up class"<<endl;
 }
};

 


의도한 대로 bClass에서 생성된 print 가 실행 되었습니다.

추가로, 가끔 소멸자에 virtual 키워드를 붙이라고 하는걸 들어본 적이 있을껍니다.
그 이유는 위와 같이, 소멸자에 virtual 를 붙이지 않고 단순히

upClass *a = new bClass 로 할때

a가 소멸할시 소멸자는 upClass의 소멸자를 호출하는거지 , new 로 선언한 bClass 의 소멸자가 불러오지 않기 때문이죠.
virtual 를 소멸자에 선언하면, 당연히 bClass 의 소멸자가 실행되고 upClass의 소멸자가 실행됩니다.

이게 중요한 이유는 의도치 않는 메모리 누수가 발생하기 떄문이죠.
특히 서버 프로그래밍경우 메모리 관리가 엄청 중요합니다. (이것때문에 서버가 크리티컬로 다운되면 게임 전체가 stop이죠... ㅠㅠ)

저작자 표시 비영리 변경 금지
TAG C++
Posted by 考えの進化 RosaGigantea

서버 개발하다 보면 DB에 내장 프로시저가 많아지는데
이것도 나중에 관리하는게 일이 되어 버린다.

하여튼, 특정 단어가 쓰였는지 확인하려면

--내장 프로시저 검색
SELECT * FROM sysobjects WHERE id IN (
 SELECT id FROM syscomments WHERE text LIKE '%검색할 단어%'
)

위 쿼리를 실행시키면 된다.

그대로 복사해서 검색할 단어에 프로시저에서 사용할법한
(SELECT 나 UPDATE 등 테이블 조작하는 필드명을 검색한다거나...)
단어를 넣어서 실행하면
결과 테이블이 뜨는데, 그 내장 프로시저를 찾아가면 된다.
저작자 표시 비영리 변경 금지
TAG SQL
Posted by 考えの進化 RosaGigantea
2012/01/05 15:01

Predefined Macros C/C++언어2012/01/05 15:01

쉽게 말해 코드상 디버그를 좀더 편하게 해주는 컴파일러 매크로임
그중 제일 잘 쓰는건 __FILE__ (현재 파일 이름), __FUNCTION__ (현재 함수 이름), __LINE__ (현재 실행 라인)
정도.
이하 msdn 을 긁어왔다.
http://msdn.microsoft.com/ko-kr/library/b0084kay(v=VS.90)

Names the predefined ANSI C and Microsoft C++ implementation macros.

The compiler recognizes predefined ANSI C macros and the Microsoft C++ implementation provides several more. These macros take no arguments and cannot be redefined. Some of the predefined macros listed below are defined with multiple values. See the following tables for more information.

ANSI-Compliant Predefined Macros

Macro

Description

__DATE__

The compilation date of the current source file. The date is a string literal of the form Mmm dd yyyy. The month name Mmm is the same as for dates generated by the library function asctime declared in TIME.H.

__FILE__

The name of the current source file. __FILE__ expands to a string surrounded by double quotation marks. To ensure that the full path to the file is displayed, use /FC (Full Path of Source Code File in Diagnostics).

You can create your own wide string version of __FILE__ as follows:

#include <stdio.h>
#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
#define __WFILE__ WIDEN(__FILE__)
wchar_t *pwsz = __WFILE__;

int main() {}

__LINE__

The line number in the current source file. The line number is a decimal integer constant. It can be altered with a #line directive.

__STDC__

Indicates full conformance with the ANSI C standard. Defined as the integer constant 1 only if the /Za compiler option is given and you are not compiling C++ code; otherwise is undefined.

__TIME__

The most recent compilation time of the current source file. The time is a string literal of the form hh:mm:ss.

__TIMESTAMP__

The date and time of the last modification of the current source file, expressed as a string literal in the form Ddd Mmm Date hh:mm:ss yyyy, where Ddd is the abbreviated day of the week and Date is an integer from 1 to 31.

저작자 표시 비영리 변경 금지
TAG C/C++, Debug
Posted by 考えの進化 RosaGigantea
2012/01/03 14:10

[C/C++] Debug Printf C/C++언어2012/01/03 14:10

밑의 인터넷에서 가져온 소스를 테스트 해봤지만, 몇몇 컴파일러에선 args... 이후 뭐냐고 에러를 뱉기도 합니다.
그럴경우엔

#define DEBUG_ON       (TRUE)

#if DEBUG_ON
   // 일반 printf처럼
  #define debugMes(fmt, ...) printf(fmt, __VA_ARGS__)
   // 이 메시지가 나올땐, 소스 파일의 이름, 함수, 라인수를 같이 출력
  #define debugInfoMes(fmt, ...) printf( "[%s %s %d]" fmt, __FILE__, __FUNCTION__,__LINE__, __VA_ARGS__ )
#else
  #define debugMes(fmt, ...)
  #define debugInfoMes(fmt, ...)
#endif

식으로 매크로를 써주면 왠만한곳은 다 먹힙니다.


이하 긇어온곳 원본
/* -------------------------------------------------------
Printf를 이용한 debug
-------------------------------------------------------
2005.12.08 hugman@postech.ac.kr
원본출처 : http://cafe.naver.com/drv.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=25

다음과 같은 좋은 debug 방법이 있습니다.
프로그램은 매우 간단하므로 한번 살펴보시면 쉽게 파악
하실 수 있습니다.
팁이 있다면,
      1) 프로그램을 다 완성하고, 이 debug를 아예 없애고 싶다면
         공통 해더파일에
          #define NDEBUG
          라고 해주면, 컴파일시 해당부분이 없어지게 됩니다.
      2) 컴파일 옵션으로 지울수도..
            컴파일 옵션에 -DNDEBUG 라고 추가합니다.
            예를 들어
            CFLAGS += -Wall -O2 -g
            라고 되어 있는 부분을
            CFLAGS += -Wall -O2 -g -DNDEBUG
            이렇게 말이죠.

  ++ 추가할점
  VC++ 에서도 잘 작동하는지는 확인해봐야 합니다.
-------------------------------------------------------
*/

#include "iostream"
//#define NDEBUG
//printf( "%s %s %d ", __FILE__, __FUNCTION__, __LINE__ );
// --> source 파일 이름, function 이름, line 번호를 출력해줍니다.

#ifndef NDEBUG
  #define dp(fmt,args...) printf( fmt, ## args )
  #define dlp(fmt,args...) printf( "[%s %s %d]" fmt, __FILE__, __FUNCTION__,__LINE__, ## args )
#else
  #define dp(fmt,args...)
  #define dlp(fmt,args...)
#endif

int func2()
{
  dlp(" ");
}
int main()
{
  dlp(" ");
  func2();
  func2();
}
저작자 표시 비영리 변경 금지
Posted by 考えの進化 RosaGigantea