select를 이용한 별움직이기 클라이언트

2021. 11. 8. 18:29게임서버/namespace univ_dev

320x100

 

안녕하세요 대학생 개발자입니다.

이번 글에서는 과제였던... select를 이용해서 별움직이기 클라이언트를 만들어서 서버에 연결을 해보려고합니다.(물론 잘되야겠죠 ㅋㅋㅋ)

 

먼저 당연히 서버와 통신을 하려면 L7에서의 프로토콜이 필요합니다.

그럼 프로토콜을 잠시 보겠습니다.

네 메세지는 이렇게 생겼습니다.

일단 우선 Header가 있는데... 이건 제가 구현을 할때 Header로 buffer를 가르키고 Msg...를 가지고 buffer+4부분을 가르키게 구현했기 때문에 모든 메세지에 헤더가 붙지 않았습니다...

 

아 그리고 헤더에 보시면 not_used라는 항목이있는데요... 이게 원래 프로토콜에 따라서 사이즈가 다른경우가 대부분이겠지만 지금의 경우에는 제가 링버퍼를 구현을 못한 상태입니다. 그래서 아직 프로토콜의 사이즈를 맞춰서 읽어야 되기 때문에 그래서 어쩔수없이 저렇게 해뒀습니다.

 

우선 당연하게도 메세지의 타입에 따라 우리가 받아야될 메세지가 달라질겁니다.

일단 메세지 종류는 총 4개가 있는데요. 플레이어가 들어왔을때 그 플레이어에게 자신의 ID를 알려주기 위한 프로토콜인 MsgID, 그리고 새 유저가 들어왔을때 그 유저에 대한 별을 생성하라고 지시하는 MsgCreateStar, 그리고 유저가 나가게 되었을때 별을 삭제하기 위한 MsgDeleteStar, 그리고 유일하게 클라이언트가 보낼수있는 메세지이자 클라이언트들의 좌표를 담는 MsgMove 이렇게 4개가 존재합니다.

 

우선 서버의 방식에 대해서 먼저 말씀을드리면... 서버는 전적으로 클라이언트를 신뢰하는 방식으로 코드를 작성했습니다. 그래서 움직일때 클라이언트가 먼저 움직이고 그다음 서버에게 나 여기로 이동했어 라고 통보하는 식의 코드로 되어있습니다. 그래서 사실상 서버가 만들어서 보낼 패킷은 id와 별생성, 별삭제 이렇게 3개밖에 없게되는거죠 나머지는 그냥 온 그대로 돌려보낼거니까요.  

음... 에코서버와 느낌이 비슷합니다. 하지만 모두에게 돌려보낸다는 다른점이 존재하죠.

우선 모두에게 돌려보낸다면 생길 문제점에 대해서 말을해봐야겠는데요.

아무리 LAN환경에서 실험을 한다고 해도 당연히 네트워크를 거쳐 서버의 로직을 진행한뒤 돌아온 메시지는 local에서의 지연보다는 늦을거에요. 그러면 내가 만약 보낸뒤 그 패킷이 돌아오기 전에 내가 한칸더 가버린다면 그제서야 서버의 메시지가 돌아오게 될것이구요. 그럼 나는 한칸 뒤로가게 되는 셈이죠... 과거의 내 위치로 가게되는 것입니다.

그렇기 때문에 이런 로직이 필요한데요

1. 클라이언트에서 내 id이면 무시한다

2. 서버에서 보낸사람에게는 돌려 보내지 않는다.

일단 서버는 2번을 택했습니다... 그럼 저는 뭐 2번에 맞춰서 개발해야겠죠.

 

네 우선 메인 로직은 이렇게 생겼습니다.

한프레임당

1. 네트워크를 통해온 모든 패킷들 처리

2. 키보드체크

3. 렌더

그리고 15ms간 쓰레드 스탑입니다.

 

초반 초기화 부분은 너무 기초적인 부분이라 따로 설명없이 넘어가도록 하겠습니다.

먼저 소켓은 TCP소켓으로 만들었고 ioctlsocket함수로 논블로킹으로 설정했습니다.

 

먼저 NetWorking함수에 대해서 좀 보겠습니다. 길기때문에 여러번 쪼개서 나온다는점...

 

우선 클라이언트의 경우에는 따로 writeSet이 필요가 없었습니다. 왜냐면... write set이란건 버퍼가 비어있으면 당연히 ISSET에서 true가 나올거니까요... 서버가 죽지않는다면 절대로 write set이 set 상태일거라고 가정을 하고 갔습니다. 그래서 write가 필요한 상황이면 그냥 그자리에서 바로 send하는 식으로 처리했습니다.

 

일단 저기서 FD_ZERO라는 메크로가 나오는데요... 음... 저 메크로는 그냥 set의 count를 0으로 만들어버리는 기능 외에는 아무것도 하지않습니다. 사실 그것만 해도 충분하구요.

네 그리고 두번째로 FD_SET이라는 메크로인데요 쟤는 좀 깁니다. 왜냐면 먼저 이미 저장되어있는 곳에 저 소켓이 들어있는지 확인을 해야될거구요 두번째로는 count번째에 인자로 들어온 g_ClientSocket을 넣어야되기 때문입니다.

 

네뭐 FD_ZERO와 FD_SET에 대한 설명을 드렸으니 더이상 여기서는 볼 코드가 없는 것 같습니다.

아... timeval 같은경우에는 long sec, long microsec 두개로 이루어진 구조체인데요. 뭐 말그대로입니다 select에 만약 Set상태가 된 소켓이 하나도 없을경우 얼마나 기다렸다가 리턴하겠냐고 묻는 구조체입니다.

그럼 select부분은 넘어가도록 하겠습니다.

select 밑의 바로 에러처리 구간인데요 일단 에러가 나면 에러를 받아서 WSAEWOULDBLOCK인지 확인을 해야됩니다.

인자로 time값을 전달했기 때문에 그 시간동안 기다려보고 돌아오는게 없다면 SOCKET_ERROR를 리턴합니다. 그럼 그 리턴값을 가지고 우리는 WSAGetLastError함수를 호출해서 WSAEWOULDBLOCK인지 확인을 해야됩니다.

만약 그런 에러가 아니라면... 뭐 일반적으로 소켓을 잘못 등록했을수도 있고 진짜 네트워크 문제일수도있는데 저같은 경우에는 코드에서 절대 g_ClientSocket이 잘못된 값이 아니라는걸 확신했기때문에 그냥 그외 에러가 뜬다면 그냥 더이상 실행되지 않게 터지게 만들어놨습니다.

 

리시브 하는 부분이구요. 아직 어떤 데이터가 들어올지 확정되지 않았기 때문에 버퍼를 전달했고 그걸 나중에 포인터 캐스팅을 통해서 접근할 계획입니다.

 

recv에 대한 에러처리구간입니다. recv는 당연히 select이후에 나온거니까 WSAEWOULDBLOCK이 걸릴 확률이 없지 않냐고 하실수 있는데요 이경우에는 한프레임에 들어온 모든 메시지를 모두 처리해야되다 보니까 recv부분이 while(true)로 묶여있습니다. 그래서 WSAEWOULDBLOCK의 케이스를 더이상 받을 것이 없는 케이스로 간주하고 break를 걸어둔겁니다.

그리고 그게아니라면 진짜 에런데... 위와 똑같습니다.

 

recv에러를 체크한다음 순서인데요 이까지 왔으면 이제 데이터도 문제없이 수신한거고 크게 문제될 상황이 없는것입니다. 사실 서버가 먼저 접속종료를 요구하는 케이스가 이 테스트에서는 없기때문에(+서버에선 무조건 fin을 보내지 않습니다) recv에서 0이 들어오는 케이스는 없애버렸습니다.

버퍼를 헤더로 캐스팅해서 그 부분을 보고 그거에 따라서 스위치문을 돌리는겁니다.

 

 

만약 아이디가 온케이스라면 내가 처음들어왔을때 서버가 보낸게 분명할겁니다.

일단 아이디를 저장해두고 그 아이디랑 지금 있는 모든 별중에 내거랑 같은 아이디가 있다면... 그 아이디를 가진 별은 제꺼가 되겠죠? 그런식으로 해서 MyPlayer변수에 플레이어를 집어넣은겁니다.(MESSAGE_ID부분을 처리하고 난 뒤부터 조작이 가능합니다 그전까지는 내꺼지만 내 케릭터가 아님.)

 

네 다음으로 별생성에 대한 패킷일경우인데요.

별생성하고 ... 뭐 이건 설명을 꼭드려야됩니까?

밑에부분에 아직 g_MyPlayer가 널일경우를 대비해서 널이라면 별들중에 내꺼를 찾는 과정을 실행하는겁니다.

왜이렇게 했냐면요... 별생성 패킷이 먼저올지 아니면 아이디 패킷이 먼저올지 확신이 없었기 때문에 뭐가 먼저오든간에 처리를 할 수있게 만들기 위해서입니다. 매 플레이어 접속시마다 g_MyPlayer가 널이아닌지 확인해야되는 부분이 불필요하긴 하지만 그래도 많이 접속해봐야 10명쯤 되는 게임에 저정도 로직은 괜찮다고 생각했습니다.

 

다음으로는 별삭제입니다... 뭐 크게 논의할 거리가 없습니다...

아이디가 같으면 별도 없에고 리스트에서도 삭제하는겁니다.

 

 

이동 메세지입니다. 뭐... 이것도 굳이 설명안해도 한번에 이해할수있을거같습니다...

대신 하나를 말씀드려야겠네요 Player::Move함수는 2개로 오버로딩 되어있는데요 첫번째는 서버가 보내주는 좌표로 바로 이동하는 int, int를 받는 하나와 하나는 Player::Direction enum type을 받는 하나입니다. 후자는 플레이어가 직접 이동할때 Move(GetXPos()+1,GetYPos()-1); 이런게 너무 객체지향이랑 거리가 먼것 같아서... 하나 따로 팠으니까 양해바랍니다.

저는 개인적으로 ... Player.Move(Left); 이런걸 좋아하는 편입니다. 이건 뭐 갠취니까 존중해주시기 바랍니다.ㅎㅎ

 

뭐 네트워크 쪽은 이렇게 정리가 되었군요... 큰 논의거리가 없습니다.

 

 

 

두번째함수로 KeyProcess인데요 이건 그냥 GetAsyncKeyState를 받아서 상하좌우 키 눌린거 받은다음에 if else if문 거치면서 방향 정해서 위치 잡아주는 겁니다. 만약 이동하지 않으면 send하지 않고 이동했으면 send하는 코드가 담겨있습니다.

send부분도 마찬가지로 에러가 발생하면 널포인터를 찔러서 터트리게 만들어버렸습니다.

 

 

 

뭐 렌더부분인데요... 뭐 사실 콘솔에서 렌더하는 방법이라 해봐야 printf같은걸 이용해서 출력하는 방법말고 따로 더 생각나는게 없었습니다. 그래서 그냥 printf 돌면서 쭉 맵을 찍어냈습니다 그냥.

 

 

네 그리고 결과입니다... 다행히 잘돌아가는군요... 4개의 클라이언트 접속했고 사실 아까 11개도 해봤는데 제pc 코어가 8코어라 그런지 ㅎㅎ... 캡쳐하는데도 죽어나려고 하길래 4개로 줄여서 했습니다... 양해부탁드리고요 동영상을 첨부하면 좋을것 같긴한데... 뭐 어차피 코드를 거의 대부분 다드렸으니... 직접 실행해보시면 아마 아실것 같아요 

 

자 여기까지 하겠습니다

긴글읽어주신 여러분들 감사드립니다 그럼 저는 다음에 더 좋은 정보로 돌아오겠습니다

그럼 안녕히계쎄요

320x100

'게임서버 > namespace univ_dev' 카테고리의 다른 글

WSAAsyncSelect로 게임클라이언트 제작  (0) 2021.11.19
WSAAsyncSelect 그림그리기 클라이언트  (0) 2021.11.13
WSAAsyncSelect  (0) 2021.11.13
링 버퍼  (2) 2021.11.12
select를 이용한 별움직이기 서버  (0) 2021.11.08