이번 시간은 Session을 만들어서
Receive, Send하는 부분을 비동기 방식으로 만들 것인데
이전시간에 했던 부분중에서 의문이 들만한 부분을 좀 고치고 가도록 하겠다.
우리가 Listener라는 클래스를 만들어서 Accept부분을 Aysnc로 만들었었다.
이 두부분을 왔다 갔다하면서 호출을 하는 그런 부분이였는데
만약 RegisterAccept에서
pending이 계속 false로만 되어서
계속 OnAcceptCompleted가 호출되고 다시 등록하고 다시 false여서 OnAcceptCompleted로 가고 다시 등록하고
이 작업이 반복이 계속 되면 어느 순간에서 StackOverFlow가 발생할 수도 있다는 생각이 든다..
(어떻게 이런것 까지 생각을 할 수 있나?)
그런데 여기서 AcceptAsync가 false로 왔다는 것은
이미 누군가가 클라가 Connect요청을 해가지고
그녀석이 이렇게 바로 입장을 할 수 있는 상황이다.
그런데 애당초 우리가
그런데 우리가
여기 backlog(10)으로 설정을 해두었기 때문에
현실적으로 계속 pending이 false로 계속 뜰 수가 없다.
많은 유져들이 작정을하고 하지 않는 이상 이런일은 발생하지 않는다고 보는 것이 합당 하고
그런데도 불안하다 싶으면 코드를 수정을 해야된다.
그리고 두번째는 뭐냐하면은
이런식으로 AysncEvent를 하나만 만들었었는데
결국에는
이렇게 위에서 문지기를 하나만 만들기는 했는데
문지기를 "딱" 한마리만 만든셈이 될 것이다.
그러니까 결국에는 유져들이 어마어마하게 많이 몰린다고 하면은
지금 이작업을 하는데 있어서 시간이 조금 오래 걸릴 수 있으니까
(그러니까 내 게임은 어마어마 하게 대박을 쳐가지고 수많은 유져들을 받아야된다고 가정을 하면은)
이부분을 늘려주면 된다.
그래서 이런식으로 여러개를 걸어두면 되는 부분이다.
이녀석들은 사실 독립적인 것이다.
한번 던져 놓으면
여기서 알아서 잘 돌아가기 때문이다.
낚시대를 여러개 던져놓은 셈이다
물고기가 많아서... 낚시대를 여러개 던져놓아도 크게 상관은 없다.
그런데 사실 아직 가장 중요한 얘기를 안했다.
프로그램으로 와보면
Main Thread는 여기서 계속 while(true)를 하고있다.
분명 여기서 등록 하는 작업을 실행시켜주기는 했는데
이녀석자체는 계속 무한 루프를 돌고있다.
그렇다는 것은
OnAcceptCompleted라는 콜백함수가 실행이 될 때,
나는 분명히 Program에서 while(true)를 실행하고 있었는데
이부분도 실행이 됬다는 것이다.
도대체 어떻게 알고 이녀석이 끼어들었는지 궁금하다.
그래서 여기서 breakPoint를 잡고 실행을 해보자.
그래서 쓰레드에 가보면
"작업자 쓰레드"
"주 쓰레드"
이렇게 있는데
주 쓰레드는 여기 잡혀있는 것을 볼 수 있다.
그래서 결국 우리는
Thread나 Task를 만들지 않았지만
콜백함수 OnAccpetCompleted는 별도의 쓰레드에서 실행이 되었다는 것을 알 수 있다.
그래서 결국에는 ThreadPool 에서 하나 꺼내와가지고
그녀석을 이용해가지고
이부분을 실행시켜주고있다는 것이다.
그렇다는 것은
AcceptCompleted에서 호출하고 있는 부분과
여기 Main에서 호출하고 있는 부분이 하필이면
"동시다발적으로 같은 데이터를 건드린다고 하면은"
어떻게 되냐면
"Race Condition" 문제가 일어난다는 말이다.
즉, 경합 조건 문제 == 멀티쓰레드에서의 문제
그렇다는 것은 이제
OnAccpetCompleted가 실행 되는 부분은
레드존 == dangerZone이 된다는 말이다.
그래서 항상 "멀티 쓰레드"로 실행이 될 수 있다는 점을 항상 머리에 두고
코딩을 해야 된다는 말이다.
지금 같은 경우에는
이렇게 자기의 소속된 애들만 건드리고있기 때문에
아무 문제가 없었지만
나중에 우리가 Receive or Send를 한다거나
좀더 복잡한 행위를 한다고하면은
"반드시" 락을 걸어두고 사용을 하거나 멀티쓰레드 환경에서 발생할 수 있는 문제들을 반드시 동기화를 하고 코딩을 해야된다는 의미이다.
결론은 이녀석은 신경을 곤두 세워서 해야된다는 것이다.
오늘은 여기 receive하는 부분을 따로 빼주도록 하겠다.
그리고 이렇게 Session이라는 이름으로 만들도록 하겠다.
그래서 이부분에서 뭐가 필요 할지 곰곰히 생각을 해보면
지금 이부분에서 성공적으로
clientSocket을 받아서 이녀석으로
받아주고, 보내주고 이런 작업을 했으니까
이렇게 소켓이 필요하다고 생각이 드니 이렇게하고
Listner에서 했던 방식과 비슷하게 하기위해서 초기화를 해주는 init을 만들도록 하자.
이런식으로 만들아주자 일단.
그리고
이부분을 이제 비동기 방식으로 Session에서 처리를 해주면 될 것인데
지난번에 말한것처럼 두단계로 나뉜다고 했었다.
그래서 Recv등록하는 부분과 완료된 부분 만들어주자
(인자같은 경우는 나중에 생각을 해주도록 하자)
그리고 _socket. 을찍으면 Accept와 같이 ReceiveAsync가 가 있다.
이녀석도 똑같이 SocketAsyncEventArgs를 받는다
그래서 init에서 이렇게 만들어주고
이렇게 연결을 해주면 될 것이다.
그담음에 우리가 Listener에서 했던것과 똑같이 작업을 해야될텐데
지금
recvArgs가 완료가 되면 Completed라는 녀석으로 알려주었었다.
여기다가 Event를 연동을 시켜줬었다.
그래서 여기다가 이렇게 한다음에
이렇게 연결을 해주고 싶다.
그런데 빨간줄을 형식을 안 맞춰주어서 그런것이니까
EventHandler 타입이 object sender, socketAsyncEventArgs 이니까 여기 맞추어 주자
이렇게 맞춰주면됨!
그리고 이전에는 완료가되면은
AcceptSocket으로 연결된, 새로만들어진 소켓을 뱉어주고 있었었다.
( 여기서
이런식으로 )
AcceptSocket은 Listener전용일 때 사용하는 것이고
우리는 다른애를 사용하게 될것인데
recvArgs는 SetBuffer를 이용을해서 새로운 Buffer를 만들어 줘야한다.
이 Buffer의 개념이 무엇이냐 하면은
이부분 인데
지금 여기서 recvBuff 를 byte[ ]로 만들어서
clientSocket.Receive(recvBuff); 여기서 Receive를 할 때 recvBuff를 건내 주었었다.
너가 Receive라는 함수로 받은 정보를 recvBuff에 있는 데이터에다가 받아주세요!
라고 우리가 요청을 했으니까
여기서도 어떻게든 Buffer를 연결을 시켜줘야지만,
Receive를 어떻게든 할 수 있을 것이다.
그래서 결국에는
이부분을 넣어 주어야된다는 얘기인데
우리는 이 3번째 버젼을 사용할 것이다.
그래서 이렇게 1024짜리 바이트 배열 넣고, offser은 0, count는 1024를 넣어주면된다.
이녀석이 하나의 세트라고 볼 수 있는데
buffer인데 어디서부터 시작할 거냐? (0부터 시작할 것이다 ( 어디부터 시작할 것인지) ) 그리고
이 버퍼의 사이즈는 1024였다고 마지막인자에 입력을 해준다.
우리는 세션을 하나 만들어 줄때마다 버퍼를 그냥 이렇게 0부터 시작하도록 이렇게 만들도록 하겠다 (이방법이 쉬우니까)
참고로 추가로 넘겨주고 싶은 정보가 있다면
UserToken이라는 녀석이 있는데
object 타입이기 때문에 숫자를 넣어줘도 되고
"이 세션에서 부터 온 녀석이다!"라는 뜻으로 this를 넣어줘도 되고
온갖 정보를 다 건내줄 수 있기는 하다.
그런데 딱히 하지않더라도 크게문제가 되는 부분은 없다.
이것은 나중에 식별자로 구분하고싶거나 연동하고 싶은 data가있을 떄 사용하는것이다.
그래서 이까지 했다면 준비는 된 것이다.
그래서 여기서 이제 똑같이 등록을 해주면된다.
그리고 Listener와 똑같은데
pending이 false라면
받을 데이터가 바로 딱! 있어가지고 Async계열을 실행하자마자
바로 retrun 해가지고 데이터를 지금 뽑아 올 수 있는 상태가 된 것이다.
똑같이 이렇게 넣어주면 될 것이다.
그다음
이제 이부분이 조금 달라질 수 있는데
BytesTransferred라고 내가 몇바이트를 받았느냐? 하는 부분이있는데
그런데 이게 경우에 따라서 0byte가 올 수도있는데 (말이 안된다고 생각할 수 있지만)
상대방이 연결을 끊는다거나 할 때 가끔 0으로 올 수도 있다.
그래서 이것이 반드시
0보다 큰지 체크를 해야되고
그리고 또
이렇게 에러가 없는지 체크를 해야된다,
그래서 이부분을 만족을 한다면 성공적으로 데이터를 가지고왔다는 얘기가 되는 것이고
아니라면 쫒아내야 된다.
TODO부분에서 나중에 패킷을 분석하고 조금 복잡하게 가기는 하는데
지금 우리가 했던 코드
이것은 굉장히 간단하게 받고 있었으니까
이런부분을 똑같이 넣어주도록 하겠다.
다면 이제 GetString안에 인자들의 값이 바뀌게 될 것이다.
그런데 지금 우리는
Buffer를
여기서 EventArgs에다가 recvArgs.SetBuffer로 Buffer를 넣어놨으니까
거꾸로도 추출을 할 수 있을것이다 당연히,
그래서 여기다가
우리가 SetBuffer한 Buffer넣어주고
offset넣어주고, 몇바이트를 넣어주었는지 BytesTrasnferred를 넣어주면된다.
이렇게하면 똑같이 정보를 긁어올 수 있다.
이렇게해주고,
그다음에 이제
이렇게 RegisterRecv를 툭 던져주어야 된다.
혹시라도 에러를 대비해서 try & catch넣어주고
그래서 Receive같은 경우에는 Listener와 비슷하게 되는데
Send같은 경우에는 좀 달라진다.
일단은 Send는 기존에 했던것과 비슷하게 일단은, 만들어 줄 것이다.
기존에 이런
블로킹 버젼을 사용하고있었다.
그래서 인터페이스만 똑같이 맞춰 주도록 하겠다.
이렇게 Send만 하도록 만들어 주자.
그리고 이부분은 내부에서만? 사용하는 부분이니까 region으로 감싸주도록 하겠다.
그래서 외부에서 사용하는 인터페이스는 이렇게 두개라는 것 정도만 알 수 있다.
그다음에 하나만 더 넣을 것인데
Disconnect를 넣어주도록 하겠다.
이경우는
딱 이부분이 들어가면 될 것이다.
이렇게.
그렇면 이제 session의 기본은 완성이 된거같으니까 다시 프로그램으로 돌아와서
여기서
이 부분만 남기도 다 없애버리도록 하겠다.
그래서
여기서 listener가 Accept가 되어서
성공적으로 OnAcceptHandler가 호출이 되면은
session을 만들어 줄 것이다.
그런데 경우에 따라서 세션을 여기서 만들게 아니라
미리 만들어 둔다음에 그녀석을 꺼내 쓰는 방식 == 풀링? (풀리언) 방식도 충분히 고려할 수 있는데
일단은 이렇게 간단하게 만들어 볼 것이고
그 다음 init에다가 socket을 넣어줘야되는데
이렇게 clientSocket를 넣어줄 것이다.
그래서 init을 하게되면
이렇게 호출이 되어서
RegisterRecv까지 호출이 되고있는데
이부분은 아래로 옮기자.
그다음 이제 보내는 것은 달라진 것이 없으니까
이렇게 session.Send로 보낸다.
"Welcom To MMORPG Server !"그럼 이 메세지를 보내 줄 것이고
1초정도만 있다가 쫒아내주도록 하겠다.
그리고 DummyClient에서
예의상 5번정도 보내도록 이렇게 수정을 하도록 하겠다.
그리고 실행을 해보면 이런식으로 잘 받아 오는 것을 볼 수 있다.
여기까는 그래서 비동기로 만들었다고 볼 수 있다.
이녀석은 멀티쓰레드 환경이여도 내부에서만 사용하는것을 사용하니까 별 문제는 없는데
Disconnect일 경우는 멀티쓰레드 환경에서 이녀석을 누군가 두번 호출한다거나 그러면
어떻게 될까?
이렇게하고 실행을 해보면
이런식으로 에러가 뜬다.
그래서 이녀석이 한번만 호출이 될 수 있도록 해주어야 되는데
지금 멀티 쓰레드 환경을 생각하면
이함수 안에서 _socket = null로 마지막에 해서
if (_socket == null) 이런식으로 하는것은 멀티쓰레드 환경일 때 위험하니까
제일위에 flag를 하나 놔두도록 하겠다.
이렇게.
그래서 Disconnect를 한번하면 이녀석을 1로 바꿔주기는 할텐데
멀티쓰레드시 그냥 1로 바꾸면 안되니까
실전에서 처음으로 InterLocked 계열을 써볼 것이다.
그래서 이렇게 1이 되면은 끊겼다! 이다.
그런데 한번더 봐야할 것이 뱉어주는 값이 만약에
!
1이라면은 다른애가 이미 1로 셋팅을 했다는 뜻이되니까
이렇게 retrun을 때려주도록 하겠다.
이상태에서 똑같이 실행을 해보면
DisConnect를 두번 연달아 했지만
잘 돌아간다.
그래서 이
비동기 방식이 Listener의 비동기 방식과 흐름이 비슷하다는 것을 알 수 있다.
특이사항은 뭐냐하면은
여기서 RegisterRecv를 한번 등록을 한다음에
OnRecvCompleted(null, args);
이게 실행이 되야지만
여기서 다시 재등록을 하는 흐름이 굉장히 중요하다고 했었다.
그래서 Receive같은 경우에는 좀 간단하게 되는데
Send의 경우에는 이렇게 간단하게되지가 않는다.
여기서 이렇게 등록을 해놓고 어떤 클라가 메세지를 던져주면 그때
여기서 처리를 하기때문에 굉장히 편리한데
Send의 경우 예약을 하는게 아니라
그때그때 내가 원하는 타이밍에 Send를 호출해야되기 때문에
Send는 많이 까다롭다.