지난번에 작업하던 코드를 조금 정리를 하고 넘어가도록 하겠다.
코드가
이렇게 Main안에 크게 다 때려박혀있다.
코드가 커지면 관리가 어려워지기 때문에
따로 빼서 관리하는 습관을 길러야 한다.
지금은 문지기 부분을 빼서 관리를 먼저 하자.
ServerCore > 우클릭 > 추가 > 새항목
이렇게 만들어 주도록 하겠다.
일단은 ServerCore에 있던 내용들을 하나하나씩 옮겨와야된다.
이렇게 listenSocket을 만들어서 밑에서
Bind, Listen, Accept, Receive등등을 해주고 있으니까
이렇게 만들고 작업 효율 높기 위해 화면 분할 하자.
그래서 이 만들어 주는 부분도 Listener.cs에 옮기기 시작해야되는데
Listener에서 init함수를 만들어서 초기화를 해주는 부분을 만들어보도록 하자.
여기서 결국 endPoint를 받아와서 listenSocket을 만들어 주고 있으니
init에서 endPoint를 받아 주도록 하자.
그리고
이렇게
옮겨오도록 하겠다.
그리고
이부분도 아무 문제 없이 옮겨오도록 하겠다.
그리고 이렇게 수정을 해주도록 하겠다.
그래서 초기화를 하는 부분은 이까지하면 되는 것이고
나머지
Accept하는 그런 부분들은 여기서 하는게 아니라
만들어주고
Accept보면은
이렇게
Accept하고 뱉어주는게 대리인의 휴대폰 (소켓)이라고 했는데
이녀석도 똑같이 Socket을 뱉어주도록 만든다음에
이렇게 뱉어 주면 될 것이다.
그리고 이녀석이 우리의 문지기 역할을 하는 녀석이니까 만들어 주고
이렇게 endPoint를 넣어주면
이부분이 호출이 되면서 다 셋팅이 될 것이다.
그리고 Accept부분은 이렇게 수정을 해주면된다.
그리고 실행을 해보면 이렇게 아까와 별반 다를것 없이 실행은 잘된다.
그래서 이렇게까지해서
Listner를 따로 빼서 작업을 한 셈이된다.
그리고
이러한 받고, 보내고 이런부분도 Session이라는 클래스를 따로 빼서 관리를 할 것인데
나중에 할 것이고
지금 당장 가장 마음에 안드는 부분은 바로
Listener > Accept에서 이렇게 블로킹 계열을 사용한다는 것이 가장 마음에 안든다.
결국 게임을 만들때는 블로킹 계열의 함수를 만드는 것을 최대한 피해야되는데
여기 ServerCore > Program에서 while문으로 무한루프를 돌고있는데
Socket clientSocket = _listener.Accept();
이부분이 실행이 되면은
이제는 "무한대"로 대기를 한다고 했었다.
그렇게 무한대로 대기를 하다가 실제로 클라이언트 쪽에서
Connect를 사용해서
Socket clientSocket = _listener.Accept();
이쪽에다가 입장요청을 하기 전까지는
영영 return을 하지않고 무한대로 대기를 할 것이다.
그래서 우리가 Main쓰레드를 만들어서 실행을 시켰는데
여기서 식물인간같은 상태로 대기를 하는 것이다.
그런데 Accept같은 부분은 이렇게 할 수 있다고 눈감아 주더라도
나중에 생각해보면
이런식으로 Receive, Send를 할 때
만약 블로킹 계열의 함수를 사용한다고 하면 어떤 일이 벌어질지 상상을 해보자.
게임이 만약 동접이 만명가까이 가서 ㅈㄴ 많다고 가정을 하면은
10000명 유저를 처리를 하려면 그 유저들과 계속 소통을 해야된다.
그러니까 Receive, Send를 호출을 계속 하면서 왔다갔다 할텐데
그때마다
int recvBytes = clientSocket.Receive(recvBuff);
이런식
clientSocket.Send(sendBuff);
이런식으로 무한정 대기를 한다면 상당히 큰 문제가 될 것이다.
그러니까
"누가" 실제로 "나"한테 메세지를 보낼 것인지
"누가" 메세지를 듣고있는지 모르는 상태인데
clientSocket.Receive(recvBuff); 이런식으로 "상대방"이 "나"한테 뭔가를 보내주거나
"내"가 clientSocket.Send(sendBuff); 이렇게 보냈을 때
"상대방"이 받을 때까지 이런식으로 대기를 한다는 것은 말이 안되는 상황이다.
그래서 결국
Accept, Receive, Send이런 모든 "미출력" 계열의 함수들은
무조건 "비동기 함수" == "None Blocking" 방식으로 체택을 해야된다는 의미이다.
그래서
그냥 이녀석을 NoneBlocking계열로 바꿔주기만 하면 된다는 얘기인데
이렇게 _listenSocket.AcceptAsync()라는 것이 있는데
여기서 Async가 붙었다는 것은 무조건 "비동기"라는 뜻이다.
즉, "동시에"처리되지 않고 "나중에" 처리 될 수 있다는 뜻이고
이 Async에서 sync라는 단어가 우리가
"싱크"를 맞추다 그런 얘기를 하는데, 그 얘기이고
영어에서 "A"가 붙으면 그 반대의 개념이되는 것이다.
그래서 Async == 비동기이다.
그렇다는 것은
Async 계열의 함수를 사용을 하면은 발생하는 것들
실제로 성공하든 아니든 == 일감이 처리되든 아니든 일단, return을 하고 올 것이다.
근데 바로 return을 하게 된다면 실제로 유저가 접속을 하지 않아서 아무런 완료 처리가 되지 않았는데도 일단은 빠져 나오게 된다는 얘기이다.
그렇다는 것은 실제로 유저가 우리한테 접속 요청을 하면은 뭔가 어떤식으로든 우리한테 "알려줘야 한다는 얘기이다"
그래서 일단 우리가 이런 비동기함수를 사용을 하면은
기존에 사용하던 블로킹 방식보다 조금더 어려워 지기는 한다.
return _listenSocket.Accept();
이녀석은 굉장히 직관적이라 위에서부터 아래로 한줄씩 따라 내려가면되는데
여기서 손님을 입장시킨다고 했는데
여기서 만약에 손님이 입장을 안했으면 멈추고,
입장을 했다면 아래로 쭉 내려가는 방식일텐데
비동기 함수를 사용을 하면은 Async 계열의 함수를 사용을 하면은
Socket clientSocket = _listener.Accept();
이부분에서 기다리지 않고 바로 내려온다음에
다른 처리를 하다가
나중에 유져가 들어오면 clientSocket을 어떻게든 콜백 방식으로 어떤 식으로든 연락을 줄 것이다.
그래서 그 부분을 한번 만들어 보도록 하겠다.
이 개념은 당연히 C#에만 있는게 아니고 c++에서도 서버 만들때 똑같이 사용된다.
그래서 우선 알아야 할 것이
Accept라는 애가 무작정 기다린다고 해서 처리가 보장이 되는 그런 방식이 아니기 때문에
이런식으로 Accept를 하겠다고 요청을 하는 부분과
실제로 처리가 되어서 완료를 해주는 부분으로 두부분으로 나뉘어서 만들어야 된다는 얘기이다.
위에다가
이런 함수를 하나 만들어주고 ( Accept를 등록을 한다 )
Accept완료됬다는 함수도 만들어 주자.
_listenSocket.AcceptAsync();
의 버젼이 여러개 있는데
이버젼을 사용 할 것이다.
그래서 이버젼으로 만들 것이다.
여 레지스터 억셉트에서 해야할 부분이
이렇게 들어와야되고
이녀석은 당장 완료를 한다는 보장은 없고
이렇게 요청을 하기는 할 것이다.
말그대로 등록을 한 것이다.
그리고 이런식으로 bool을 뱉는데
pending 여부를 뱉어준다.
여기서 pending을 체크를 해가지고
false이면 비록 비동기 버젼으로 호출을 하기는 했지만
정말 할일이 없어서 그런지 바료 완료가 됬다는 의미이다.
그러니까 이녀석을 뿅! 하고 실행하는 동시에
클라가 접속 요청이 와가지고 정말 눈깜짝할 사이에
if (pending == false)
pending없이 완료가 됬다는 의미이고
그럴 경우에는
이녀석을 막바로 호출해주면된다.
그런데 만약에
pending이 true라고 하면은 어떻게 해야하냐?
=> 어떻게든 나중에 우리한테 "통보"가 온다는 얘기이다.
그렇다면 결국
RegisterAccept를 할 때,
뭔가를 만들어 줘가지고
t.AcceptAsync(args);가 완료가 되었을 때 거꾸로 우리한테 뭔가를 알려줘야 할 것이다.
그래서 init에서
이녀석을 만들어 주자 (참고로 이녀석은 한번만 만들면 재사용이 가능한 어마어마한 장점이 있다)
args.Completed 눌러보면 EventHandler방식인것을 볼 수 있다.
즉, Event방식으로 우리한테 콜백방식으로 뭔가를 전달해준다는 의미인데
콜백함수로 new EventHandler< SocketAsyncEventArgs >();
인자에다가
OnAcceptCompleted를 받아주고 싶다고 넣어주면 된다.
그런데
지금
이렇게 EventHandler가 요구하는 방식이 object sender.. 이런식으로 뭔가를 받아주고 싶은데
우리는 < > 안에 SocketAsyncEventArgs를 넣어 줬으니까
OnAcceptCompleted의 형식을
이렇게 맞춰달라는 형식으로 맞춰 주고
콜백으로 받으면 된다.
그리고 이녀석은 이렇게 수정을 해주면된다.
그리고 여기까지 args라는 Event를 만들어 준것이고
이렇게 등록을 해줄 것이다.
아까는
이렇게 Accept하는 부분을 시작하고 끝날 때까지 기다렸다면
지금은 맨처음에 초기화 (init)을 하는 시점에서
여기서
이렇게 등록을 해줄 것이다.
그런데 이상태에서 클라가 Connect요청이 왔다고 하면은
콜백방식으로 OnAcceptCompleted가 호출이 된다는 말이다.
그래서
맨처음에 등록을 하고 클라가 Connect요청이 오면 OnAcceptCompleted를 호출을 하고 (== pending == false 일 경우)
그게 아니라 Pending이 true라면
밑줄친 부분이 호출되지 않겠지만
나중에라도 진짜 완료가 되었다면
이쪽에서 자동으로
OnAcceptCompleted를 호출을 해줄 것이다.
그래서 어떤 식으로든
이곳에 들어온다는 얘기이다.
중간에 질문글을 올렸었는데 나와 비슷한 질문을 한 글이다.
-> 답변
그래서
OnAcceptCompleted안에는
Accept()함수의 블로킹 버젼의 return을 한 값을 안에다가 넣어주면 되는데
그럼
OnAcceptCompleted안에 Socket에러가 있는지 부터 챙겨주자
Error는 실패를 했다는 에러도있지만 성공을 했다라고 알려주는 에러도 있기 때문에
이상태라면 진짜 에러없이 잘 처리가 되었다는 것이고
그게 아니라면 에러를 출력을 해보도록 하자.
그리고
이 안에는 뭘 넣을지는 조금있다가 살펴보고
Accept함수의 return 값을 넣어주어야 되는 부분이 아닌가?
그래서 만약에 유저가 Accept를 했다고해서 Accept까지 성공을 했다면
TODO부분에서 뭔가를 해주고
이부분이 다 끝났다면 이제 어떻게 해야되냐??
이렇게 다시
RegisterAccept(args)를 다시 던져 줄 것이다.
얘는 어떤 의미냐하면은
OnAcceptCompleted의 모든 일이 다 끝났으니까
다음 유져의 Connect를 위해?(다음 아이를 위해서) 이렇게 던짐(등록)을 해주는 것이다.
그래서 이 흐름이 처음에 봤을 때는 어려울 수 있다.
그런데 나중에 Receive하는 부분이랑 Send하는 부분도 다 이런식으로 되어있다.
그래서 init에서
RegisterAccept(args);는 최초로 강물에다가 낚시대를 던진거라고 할 수 있고
그리고 이 상태에서
Async 계열의 함수를 사용을 하는 것은 약간 랜덤성이 있는 것이다.
처리 될 수도 있고 처리가 안될 수도있고한것인데
pending이 false라는 것은 낚시대를 던지자마자 바로 물고기가 잡혔다! 라는 뜻이다.
그렇다면 아싸 가오리! 하고 완료된 부분
OnAcceptCompleted(null, args);
이부분을 그냥 처리를 해주면 되는 것이고
그게 아니라면 나중에 입질이 오면
args.Completed += new EventHandler< SocketAsyncEventArgs > (OnAcceptCompleted);
여기서 탁 하고 낚아채는 그런 부분이 될 것이다.
그리고 TODO부분
이부분은 잡은 물고기를 통안에다가 넣는 것이 될 것이고
물고기를 잡았으면 다시 낚시대를 바다에다가 던지는 작업이
if, else 다 통과하고 RegisterAccept(args);가 될 것이다.
이거 보면은
1) init에서 27번째 줄 RegisterAccept(args) == 낚시대를 던진다.
2) 36번째줄 OnAcceptCompleted == 물고기가 잡혔으니 낚시대를 끌어 올린다.
3) 45번째줄 RegisterAccept == 다시한번 바다에다가 낚시대를 던진다.
이러한 흐름이 이제 계속 반복되는다는 것이다.
이런식으로 계속 뺑뺑이를 돌것이고
처음에만 init에서
이런식으로 만들어 준 것이고
그다음은 알아서 계속 뺑뺑이를 도는 로직이다.
이렇게 한줄에 처리가 되던데
이제는 시작과 끝단계로 나뉘기는 했는데
어쨋든 장점은 굉장히 명확하다.
이제는 그래서 TODO를 채워야되는데 유저가 실제로 왔으면 뭘 해야할지 고민이다.
그래서 ServerCore > Program.cs가보면
31번째 줄에서 Accept로 clientSocket을 받은다음에
그다음에 이부분들을 실행을 하고 있었다.
그래서 이부분을 어떻게 실행을 할지 (맞춰 줄지) 곰곰히 생각을 해보면,
이럴때 또 가장 좋은 방식은 callback방식을 사용을 하는 것이다.
그러니까
init에서
얘내들이 이렇게 Event가 완료가 되엇다고 callback방식으로 던져준것 처럼
우리도 어떤 delegate든 Event든 Action이든 하나를 받아가지고
여기 인자로 받아가지고
요청한 그 아이가 실제로 완료가 되었다면
TODO에서 호출시켜주면 될 것이다.
그래서
Accpet가 완료가 되었으면은 어떻게 처리를 할것이냐 == Handler의 의미로 이렇게 만들어 준다.
그래서 init == 초기화를 할때 인자로 같이 받아주자.
이렇게 인자로 넣어주고
그래서 listenSocket을 만들때
이렇게 연결을 해주도록 하겠다.
그래서 이부분을 아래 TODO
TODO부분에서 호출을 해줘야 된다는 의미이다.
이렇게 Invoke로 알려준다 (호출을 해준다)
그렇다면 안에는 Socket을 넣어 줘야되는데
그러면 이제 += 하던 onAccpetHandler를 구현을 해야 할거같다
그래서 한마디로 어떻게 보면
SocketAsyncEventArgs 이녀석이 1군같은 느낌이다.
당장 Async계열을 쓰면 당장 값을 추출할 수 없었으니까
이런저런 정보들을 SocketAsyncEventArgs를 통해가지고 우리한테 전달을 해주고 있는 것이다.
그래서 여기서 args.AcceptSocket을 하게 되면,
아까
clientSocket을 뱉어 주는 부분을
여기 args.AcceptSocket에서 해주고 있는 것이다.
그래서 ServerCore > Program.cs에서
이렇게 OnAccpetHandler를 하나 만들어주고 인자로 clientSocket을 받아 주도록 하자.
그러면 결국
여기서 만들었던 이부분이
이쪽으로 이사를 시켜줘야 한다.
그리고
여기 try & catch도 OnAcceptHandler안에 있는게 맞을 테니
이렇게 안에 넣어주도록 하자.
그래서 결국
우리의 문지기 listener한테 얘기를 하는 것이다.
우리의 endPoint는 이 아이이고
혹시라도 누군가 들어오면
OnAcceptHandler라는 애로 알려줘! 라고 명령을 한 셈이다!
그러면
이렇게 while문은 그냥 필요가 없는데 프로그램이 종료가 되지않도록 하는 용도에서
이렇게 남겨 둘 것이고
사실은 이부분 때문에
뭔가가 계속 돌아 가고 있을 것이다.
이게 왜 while문 안에 안 넣어줘도 돌아가는 거냐면은
init에 초기화를 할때 endPoint던져주고, OnAcceptHandler 로 알려달라고 던져줌
그러면 init에서 보면
문지기 == _listenSocket만들고
_onAcceptHandler로 onAcceptHandler가 구독 중이고
그 다음 문지기 교육함.
그 다음 영업시작
그 다음 args만들어 주고 유저가 들어왔다면 args.Completed 한다는 것이고
RegisterAccept(args)를 등록을 함
등록을 한 부분에서는
pending이 true라면 아까 args.Completed를 실행을 하는 것이고
false라면 (물고기가 바로 잡힌 상태라면) OnAcceptCompleted 호출하는 방식이다.
그리고 물고기 처리 하는 부분
이부분에서는
에러가 없을 경우 알려달라고 했으니까 Invoke로 알려주고
에러가 있을 경우 에러 출력하고
작업이 끝났다면 다시 RegisterAccept를 해주는 것이다.
RegisterAccept에서는 다시 AcceptAsync로 일종의 대기? (낚시대를 던지고 물고기가 오기를 기다리는 상황) 일 반복하는 것이다.
그래서 만약
여기서 에러가 없어서 Invoke로 알려주면
ServerCore > Program.cs의
가 호출이 되어서
물고기가 잡혔을 때 처리 == 유져가 실제로 들어왔을 때의 처리를 해준다.
여기서 우리가 처음에 args를 만들어 줬는데
어떻게 보면은 "요정"같은 존재라고도 볼 수 있다.
필요할때마다 우리한테 쪼르르와서 메세지를 전달을 해주고
뭐 아주 좋은애인데 이녀석을
다 사용을 하고 없애는 것이 아니라,
RegisterAccept에다가 다시 한번 넣어주었다.
이런식으로 "재사용"하면 당연히 성능이 좋아지니까 굉장히 좋은 방법이라고 생각 할 수 있지만
이런식으로 "재사용"을 할 때 굉장히 조심해야 되는 부분이 있는데
여기서 시작을 할 때, 기존에 있던 "잔재"들을 다 없애야 된다는 것이 굉장히 중요하다.
여기서 OnAcceptCompleted가 완료가 되었으면은
args가 안으로 들어오면서
AcceptSocket에다가 연결된 클라이언트의 대리인의 소켓이 이부분에서 만들어 지는 것이였다.
그리고
이부분에서 clientSocket의 설정? 처리를 해준 것이고...
그런데
여기서
클라 대리인의 소켓을 만들어 줫는데 이것을 안지우고
RegisterAccept(args)에다가 넣어주면은
두번째 턴에서
여기서는 args가 null이 아닌 이미 들어있는 값으로 될 것이다.
근데 이러면은 Error가 난다.
그래서 여기서 args.AcceptSocket이 null이 아니라는 것은 이미
인자에 있는 Event가 깔끔한 초기화된 값이 아니라 엉뚱한 값을 가지고 있다는 얘기 이니까
이렇게 null로 한번 밀어줘야 된다.
그래서 이렇게 Event를 재사용 할때는
기존의 잔재들을 깨끗이 없애고 사용을 할 수 있도록 만들어 주는게 중요하다.
추가적으로 static 사용 부분
그래서 실행을 해보면
잘되는데
여러번 실행을 해보도록 하겠다.
이런식으로 더미 클라에서
while문안에서 넣어주고 Thread.Sleep(100)으로 0.1초마다 호출을 하도록 만들어 주겠다.
그러면 이런식으로 0.1초마다 계속 호출이 된다.
그래서 더미클라도 만든 이유가 나중에가서 스트레스 테스트도 계속 해봐야되니까
많은 유져들이 동시다발적으로 접속을 하거나 아니면
자주 접속을 할때 어떤일이 일어날지 궁금하니까 이런식으로 만들어 준것이다.
그래서 오늘 기억할 것은
Accept부분을 Async 방식 "비동기"방식으로 만들었다는 것을 기억을 해라