본의 아니게 업계에 알려진 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

얼마전 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

원문 : 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

Epoll은 기존 select 모델의 한계를 뛰어넘는 새로운 스타일의 서버입니다.

기존 select의 경우 일정 비트열을 두고 그곳에 0 이냐 1이냐로 클라이언트의 응답을 기다리는 방식이라 약 300~500 클라이언트 이상 동접시 속도저하 문제가 나타난다고 합니다.

(이 말은 즉, 동접 300 가량 정도는 select 으로 써도 상관은 없다는 얘기죠;;)
 

그래서 이를 해결하기 위해 아예 하드웨어적으로 클라이언트의 접속 변화를 감지해서 recv send냐 를 판별해 주는 함수가 epoll 이라 합니다.

 

문제는 이게.. 리눅스에서 만든거라 윈도우에선 사용이 불가능합니다.

그리고 리눅스도 커널 2.6 이상에서만 된다고 합니다.

 

하여간 소스 들어갑니다.

소스가.. 일단 숙제용으로서.. 게임서버 시간의 숙제를 하다가 이것저것 조사하고 찾은걸
(인터넷과 man 메뉴얼)로 짜집기 한겁니다.. orz
 

chatServer.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <sys/epoll.h>                     // 이게 중요

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <netinet/tcp.h>

#include <fcntl.h>

#include <sys/ioctl.h>

#include <errno.h>

 

#define EPOLL_SIZE 60

#define EPOLL_EVENT_SIZE 100

#define OPENPORT 9000                       // open port 번호


// main

int

main (int argc, char **argv)

{

        struct epoll_event *events;          // epoll 관련

        struct epoll_event ev;        

        struct sockaddr_in addr, clientaddr;

        int clilen;

        int sfd, efd, cfd;                   // server 소켓, epoll 소켓, client 소켓

        int i,j;                             // for문을 위해

        int max_got_events;

        int result;

        int readn;

        int sendflags = 0;

        char buf_in[256] = { '\0' };         // 버퍼

        int count = 0;

 
        // epoll 소켓 생성

        if ((efd = epoll_create (EPOLL_EVENT_SIZE)) < 0)

        {

                perror ("epoll_create (1) error");

                return -1;

        }

 

        // init pool

        events = (struct epoll_event *) malloc (sizeof (*events) * EPOLL_SIZE);

        if (NULL == events)

        {

                perror ("epoll_create (0) error");

                return -1;

        }

 
        // 서버 소켓 생성

        clilen = sizeof (clientaddr);

        sfd = socket (AF_INET, SOCK_STREAM, 0);

        if (sfd == -1)

        {

                perror ("socket error :");

                close (sfd);

                return -1;

        }


        // 소켓 정보 넣고 bind, listen 처리 

        addr.sin_family = AF_INET;

        addr.sin_port = htons (OPENPORT);

        addr.sin_addr.s_addr = htonl (INADDR_ANY);

        if (bind (sfd, (struct sockaddr *) &addr, sizeof (addr)) == -1)

        {

                close (sfd);

                return -2;

        }

        listen (sfd, 5);

 

        if (sfd < 0)

        {

                perror ("init_acceptsock error");

                return 1;

        }


        // 서버시작, epoll 이벤트 설정 및 컨트롤 함수로 활성화 

        printf("Running Server port %d\n",OPENPORT);

        ev.events = EPOLLIN;

        ev.data.fd = sfd;

        result = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &ev);

        if (result < 0)

        {

                perror ("epoll_ctl error");

                return 1;

        }

 

        // 서버 실제 루트
       
while (1)

        {

                // epoll 이벤트 대기 (클라이언트의 반응(접속 or send) 감지)
               
max_got_events = epoll_wait (efd, events, EPOLL_SIZE, -1);

             
                // epoll 이벤트 받은만큼의 숫자만큼 for
               
for (i = 0; i < max_got_events; i++)

                {

                        // 이벤트 들어온 소켓이 연결 요청이면
                       
if (events[i].data.fd == sfd)

                        {

                                cfd = accept (sfd, (struct sockaddr *)

                                                &clientaddr, &clilen);

                                if (cfd < 0)

                                {

                                        perror ("Accept error");

                                        return -1;

                                }

 

                                ev.events = EPOLLIN;

                                ev.data.fd = cfd;

                                epoll_ctl (efd, EPOLL_CTL_ADD, cfd, &ev);

 

                                printf("Accepted socket : %d\n",cfd);

                                count++;

                        }

                        // 그게 아니면,
                       
else

                        {

                                cfd = events[i].data.fd;

 

                                memset (buf_in, 0x00, 256);

                                readn = read (cfd, buf_in, 255);

                                // if it occured ploblems with reading,

                                // delete from epoll event pool and close socket                                                                                                                        // readn 값이 0 이하이면 recv 처리

                                if (readn <= 0)

                                {

                                        epoll_ctl(efd, EPOLL_CTL_DEL, cfd, &ev);

                                        close (cfd);

                                        printf ("Close fd %d\n", cfd);

 

                                        count--;

                                }

                                else

                                {

                                // 아니면 send 처리
                                //      printf ("%s", buf_in);

                                        for(j=5; j<count+5; j++)

                                        {

                                          // 단 이벤트로온 client엔 send 하지말고
                                          // 다른 모든 클라이언트에 send 해서 채팅

                                               
if(cfd != j)

                                                        send (j, buf_in,

                                                         strlen (buf_in),

                                                         sendflags);

                                        }

                                }

 

                        }

                }

        }

        return 1;

}

 

 

맨 아래쪽의

For j=5 라고 했는데, 리눅스에선 소켓 번호가 윈도우와 달리 5번부터인가 시작되서 그렇습니다.

윈도우는 OS가 자기 맘대로 할당하지만 리눅스에선 0~4번이 정해져 있고 그 이후 차례대로 count 됩니다..
물론 저 소스는 야메로 짠거라 문제 발생 요지가 많습니다. Orz

 

참고로 클라이언트는

그냥 telnet으로 telnet 127.0.0.1 9000 으로 접속하면 됩니다.

아래 소스는 인터넷에 떠돌고 있는 리눅스용 select 서버를 가져와서 윈도우 용으로 수정하였습니다.

출처는 : http://vessel.tistory.com/95 입니다.

 

리눅스에선 서버만 만들어 놓고 클라이언트는 telnet으로 접속이 가능합니다만.

저는 이걸 윈도우 용으로 전환하면서 클라이언트 프로그램도 그냥 만들었습니다… orz

윈도우도 telnet 이 있긴하지만.. 이건 좀

 

하여간 소스 들어갑니다.

 

Server.cpp

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

 

#define MAXLINE 512

#define MAX_SOCK 64

char *escapechar = "exit";

int maxfdp1;

int num_chat = 0;                                                                               // 채팅참가자수

int client_s[MAX_SOCK];

 

// 채팅탈퇴처리

void removeClient(int)                              // i번째참가자탈퇴

{

        closesocket(client_s[i]);                   // 해당소켓닫기

        if(i !=num_chat -1)

               client_s[i] = client_s[num_chat -1]; 

        num_chat --;                                // 채팅참가자수1명줄이기

        printf("채팅참가자1명탈퇴. 현재참가자수= %d\n",num_chat);

}

 

// client_s[]내의최대소켓번호얻기(k는초기치)

int getmax(int k)

{

        int max = k;

        int r;

        for(r = 0; r < num_chat ; r++)            // 채팅참가자수만큼소켓배열탐색

               if(client_s[r] > max)

                       max = client_s[r];

        return max;

}

 

int main()

{

        char rline[MAXLINE], my_msg[MAXLINE];               // buffer 생성

        char *start = "Connetctd to chat_server\n";         // 최초연결시보내는메세지

        int i,j,n;

        SOCKET s, client_fd, clilen;

        fd_set read_fds;

        struct sockaddr_in client_addr, server_addr;        // 소켓주소구조체선언

        WSADATA wsa;                                        // 윈소켓

 

        // 윈속초기화

        if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)

               exit(1);

 

        // 초기소켓생성

        if((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)

        {

               printf("Server: Can't open stream socket.");

               exit(0);

        }

        // server_addr 구조체의내용세팅

        ZeroMemory(&server_addr, sizeof(server_addr));       //초기화(0으로채운다)

        server_addr.sin_family = AF_INET;

        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

        server_addr.sin_port = htons(9000);

         
        // bind()
       
if(bind(s,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0 )     
        {

               printf("Server: Can't bind local address.\n");

               exit(0);

        }

 

        //클라이언트로부터연결요청을기다림

        listen(s,SOMAXCONN);                    // listen 접속대기(큐크기5)

        maxfdp1 = s + 1;                        // 최대소켓번호+1

        while(1)

        {

               FD_ZERO(&read_fds);              // fd_set  초기화(0으로세팅) 

               FD_SET(s,&read_fds);       // 소켓에해당하는file discripter1로세팅

               for(i=0; i<num_chat; i++)        // ""

                       FD_SET(client_s[i], &read_fds);

               maxfdp1 = getmax(s) +1;         // maxfdp1 재계산

 

               if(select(maxfdp1,&read_fds, (fd_set  *)0, (fd_set *) 0, (struct timeval *) 0) < 0)     // select setting

               {

                       printf("select error =< 0 \n");

                       exit(0);

               }

               if(FD_ISSET(s, &read_fds))  // read_fds s에해당하는비트가세팅되있다면

               {              

                       int addrlen;        // 의미없음ㅡㅡ.. 단순주소길이저장변수

                       addrlen = sizeof(client_addr);

                      

                       // == 연결요청이있다면

                       clilen = sizeof(client_addr);

                       client_fd = accept(s, (struct sockaddr *)&client_addr, &addrlen);                 
                       
//  accept()

                       if(client_fd == -1)

                       {

                              printf("accept error \n");

                              exit(0);

                       }

 

                       // 채팅클라이언트목록에추가

                       client_s[num_chat] = client_fd;

                       num_chat++;                       // 채팅참가자수1 증가

                   
    // "Connetctd to chat_server" 를접속한클라이언트에보냄                    
                       
send(client_fd,start,strlen(start),0);     
                   
   printf("%d번째사용자추가,\n",num_chat);

               }

 

               //클라이언트가보낸메시지를모든클라이언트에게방송

               for(i = 0; i<num_chat; i++)              // 모든클라이언트검색

               {

                       memset(rline,'\0',MAXLINE);      // buffer 초기화

                       if(FD_ISSET(client_s[i],&read_fds)) 
                     
// read
해당소켓에서read 할것이있는지확인

                       {

                              if((n = recv(client_s[i],rline,MAXLINE, 0 )) <= 0)

                              {

                                      removeClient(i);

                                      continue;

                              }

                              // 종료문자처리

                              if(strstr(rline,escapechar) != NULL)
                              //"exit"
가입력되면종료시키기

                              {

                                      removeClient(i);

                                      continue;

                              }

                              // 모든채팅참가자에게메시지방송

                              rline[n] = '\0';

                              for(j = 0; j < num_chat; j++)

                                      send(client_s[j],rline,n,0);

                              printf("%s\n",rline);

                       }

               }

        }

        closesocket(s);                                                                                                                                                        // closesocket()

        WSACleanup();                                                                                                                                                  // 윈속종료

        return 0;

}

 

 

아래는 클라이언트 소스입니다.

Client.cpp

/***********************************************

        스레드를사용한채팅클라이언트

   원래루트는recv 전용이고

   스레드를생성해서사용자입력대기하다가,

   입력받게되면send 해주는클라이언트임.

***********************************************/

 

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

 

#define MAXLINE     512

 

DWORD WINAPI ProcessInputSend(LPVOID arg);

 

char *escapechar = "exit";

char name[10]; // 채팅에서사용할이름

char line[MAXLINE], message[MAXLINE+1];

struct    sockaddr_in   server_addr;

SOCKET s;      // 서버와연결된소켓번호

 

int main()

{

        WSADATA wsa;                          // 윈속

 

        // 채팅참가자이름구조체초기화

        printf("채팅ID 입력: ");

        scanf("%s",name);

       

        // 윈속초기화

        if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)

               exit(1);

 

        // 소켓생성

        if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)

        {

               printf("Client : Can't open stream socket.\n");

               exit(0);

        }

 

        // 채팅서버의소켓주소구조체server_addr 초기화

        ZeroMemory(&server_addr, sizeof(server_addr));

        server_addr.sin_family = AF_INET;

        server_addr.sin_port = htons(9000);

        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

       

        // 연결요청

        if(connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)

        {

               printf("Client : Can't connect to server.\n");

               exit(0);

        }

        else

        {

               printf("서버에접속되었습니다. \n");

        }

 

        // 스레드생성

        HANDLE hThread;

        DWORD ThreadId;

        hThread = CreateThread(NULL, 0, ProcessInputSend, 0, 0, &ThreadId);

        if(hThread == NULL)

               printf("[오류] 스레드생성실패!\n");

        else

               CloseHandle(hThread);

 

        while(1)

        {

               ZeroMemory(message, sizeof(message));

               int size;

               if((size = recv(s, message, MAXLINE, 0)) == SOCKET_ERROR)

               {

                       printf("recv()");

                       exit(0);

               }

               else

               {

                       message[size] = '\0';

                       printf("%s \n", message);

               }

        }

        closesocket(s);                       // closesocket()

        WSACleanup();                 // 윈속종료

}

 

 

// 사용자입력부분

DWORD WINAPI ProcessInputSend(LPVOID arg)

{

        while(true)

        {

               if(fgets(message, MAXLINE, stdin))

               {

                       sprintf(line, "[%s] %s", name, message);

                       if (send(s, line, strlen(line), 0) < 0)

                              printf("Error : Write error on socket.\n");

                       if (strstr(message, escapechar) != NULL )

                       {

                              printf("Good bye.\n");

                              closesocket(s);

                              exit(0);

                       }

               }

        }

        return 0;

}

 

아래의 클래스를 사용해서 뻘짓을 하는 쓰레드 서버보단 이쪽이 훨씬 직관적이지 않습니까?

>_<..

여튼 이것도 좀 여러군데 손을 봐야 하긴 하지만..

이걸로 간단한 채팅이 되는 소스입니다.

 

클라이언트에서 쓰레드를 나눈이유는
recv처리와 send 처리를 동시에 하기 위해서 랍니다. 쓰레드를 나눔으로서
윈도우 프로그램으로 보면, 언제던지 server로부터 메시지를 받을수 있는 recv 프로세서와
언제던지 사용자가 키보드 입력을 받을수 있는 send 프로세서가 동시에 실행이 되는셈이죠

자세한 설명은.. 네트워크 프로그래밍 책의 대부분 뒷부분에 있는 select 설명을 보시는게 낳으실껍니다 >_<;;

+ Recent posts