Session #2

CJB_ny·2022년 2월 20일
0

Unity_Server

목록 보기
26/55
post-thumbnail

이번 시간에 이어서 session을 만들어 볼 것이다.

receive까지는 Async계열로 바꾸었었다.

그리고 그전에

else 부분 잠깐 Disconnect넣어주자.


RegisterRecv의 경우

Event를 이렇게 하나만 만들어서 등록을 하고있었다.

비록 비동기이기는 하지만

여러개의 쓰레드가 동시다발적으로

OnRecvCompleted로 들어오는 경우는 없을 것이다.

왜냐하면

이 Event가 하나밖에 없기 때문에

이녀석이 완료될때 마다 각기 다른 쓰레드에서 호출될 수 있을 지 언정

동시다발적으로 두개의(이상의)쓰레드가 여기 들어올 수 없다.

왜냐하면 낚시대가 하나라서 하나 던지고 끌어올리고 하나 던지고 끌어올리는 중이라 그렇다.


1. Send구현

Send같은 경우 많이 다르다고 했는데

어쨋든 Async계열을 사용한다고 가정하고 비슷하게 함 만들어보자.

그래서 뭔가 이런식으로 만들면 될거같다라는 생각이 들기는 한다.

그래서 OnSendCompleted를 이렇게 만들면될거같다라는 생각이 든다.

그리고 심지어

이부분도 BytesTransferred는 받을 때 뿐만아니라 보낼때도 사용이되고

에러도 똑같이 구현을 하면 된다.

이녀석도 else에서 상대방한테 보냈는데 뭔가 문제가 생기면 Disconnect를 해주면될거같고

그다음

try & catch도 똑같이 해주면 될거같다.

그런데 Receive와는 다르게 send하는 시점이 정해져 있지 않다는 것이다.

그리고

Receive의 경우

맨처음 이렇게 예약을 걸어두고

Receive가 되면 여기 OnReceiveCompleted가 자동으로 호출이 될 것이다.

ㅡ그리고 다시 예약을 하기위헤서 89번째줄에서 다시 호출을 하고있었는데

Send의 경우 RegisterSend를 하는순간 보내줄 Buffer랑 메세지를 설정을 해줘야 되는데

미래에 어떤 메세지를 보낼 줄 알고 이것을 설정한다는 것인가??

그래서 조금 다른 방식으로 가야되는데

여기서 Send하는 부분이 있었으니까

이녀석을 보내는게 아니라

이 시점에다가 RegisterSend를 하도록 만들어 보자.

이게 생각나는 첫번째 방법이고

그래서 이벤트 만드는 부분을 이렇게 만들어 주고 buffer같은 경우에는

미리 만드는 것이 아니라


뭐이런식으로 할 수 있을 것이다.


받아온 sendBuff에다가 Async이벤트를 연결을 해주고 등록을 해주는 작업까지다 이게

그다음 이벤트를 만들었으니 RegisterSend를 하는것을 고려를 해줄 수 있을 것이다.

Receive의 경우

receive가되면 이부분이 호출이되어서 다시 예약을 해주었는데

우리가 이부분에서 RegisterSend를 다시 해주는게 말이 안되는게

sendArgs에 들어가있는 Buffer는

연결해준 이 아이이다.

보내고 싶은 정보는 이 아이인데

똑같은 정보로

이렇게 다시 보낼 필요는 없을 것이다.

그러니까 애당초 재사용을 할 수 없다는 얘기이다.


그러면 저번처럼 그냥 sendArgs를 null값으로 밀어버리면 되는거 아니가?

RegisterSend를 할때


그래서 지금 이대로 Send를 보내게되면

이벤트를 보낼때마다 새로 이벤트를 만들고 있고, 재사용을 전혀 못하는 상태인데다가

게다가 멀티쓰레드 환경에서 동시 다발적으로 Send를 하게된다고 하면은

또 어떻게 될지 또 ㅈㄴ 궁금한데

다행인게 뭐냐하면은

Async계열함수가 멀티쓰레드 환경에서 동시다발적으로 호출이 된다고 뻑나지는 않는다.

그거는 다행인데 재상용을 못한다는 점이 마음에 안든다.

그래도 일단 실행을하면

일단 실행은 된다.

그래서 이부분에서 받는 경우에는 재 등록을 해서 다시 받기를 기다리면되는데

사실 보낸게 성공하고 나서는 크게 뭐 해줄게 없는것도 맞는 말이다.

지금 문제는

이 이벤트를 재사용할 수 없다는 것과 더 큰 문제는 RegisterSend를 매번마다 계속 호출을 하고 있다는 것이다.

예를들어 게임이 흥해가지고 천명이 동일한 존에 모여 있다고 가정을 해보자.

일반적인 상황에서 이게 어떻게 구현이 되냐하면은

A라는 유져가 한발자국 움직이기 시작을 하면은

A라는 유져가 움직였다는 정보를 천명한테 다 루프를 돌면서 한번씩 다 보내줄 것이다.

A라는 유저가 움직였다, A라는 유저가 움직였다, A라는 유저가 움직였다, 이런식으로 보낼것인데

그런데 A만 움직이고 그러는 것이 아니라 다른 유저들도 움직이고 스킬을 쓰고 난리를 칠것이니

Send의 호출이 ㅈㄴ 많을 것이다.

근데 그럴때마다

RegisterSend를 해가지고 매번마다

SendAsync를 호출을 하는것은 문제가 있다.

나중에 MMO에서 가장 느리고 부화가 심한 부분이 어딘지 성능 테스트를 해보면


이런부분 Send, Receive를 하는 송수신을 하는 네트워크 부분이 부화가 많이 걸리고 이게 굉장히 느리다.

그래서


우리가 이전시간에 커널에 대한 얘기도했었는데

usermode에서 이런식으로 네트워크 패킷을 보내는 것은 당연히 불가능하고

이런것은 다 운영체제가 커널딴에서 처리를 해주는 것이기 때문에

이 비동기녀석을 아무때나 쉽게쉽게 막 쓰는 것은 문제가 있는 것이다.

그렇다는 것은

이런식으로 이벤트를 만들어서 매번마다 보내는 것이 아니라

어떤식으로든 조금 뭉처가지고 보내면 좋겟다라는 생각이 든다.

이왕이면은 우리가 만들어준

AsyncEvent도 재사용하면 좋을 거 같다라는 생각이든다.

2. AsyncEvent 재사용

그래서 이렇게 밖?(안에다가)만들어 주도록 하겠다.

그리고 Send에있던 Completed부분 Start()로 옮겨주자. 여러번 할 필요없이 한번만!

그래서 멤버변수로 _sendArgs를 들고있으니까


Register( )안에 뭐 안넣어주어도 될 것이다.

그리고 RegisterSend는 이런식으로 고치면 될것이다.

그런데 이렇게 한다고해서 재사용이 되는 것이 아니고

멀티 쓰레드환경에서

이런식으로 Send를 호출하게 된다면은

지금 이렇게 고친것이 문제가 될 것이다.

별도의 새로운 이벤트를 만든것이 아니라 동일한 이벤트를 사용하고있는데

아직 완료가 되지도 않았는데

버퍼를 막 다른애로 바꿔주면은 분명히 에러가 날 것이다.

그리고 여기서 매번마다 보내는 것을

매번마다 Register하지 않고 어떤 큐에다가 차곡차곡 쌓아지고 한번에 한번씩만 보내도록 할 것이다.

SendAsync가 끝나가지고


이녀석이 완료되기 전까지는 보내지 않고 그냥

큐에다가만 차곡차곡 쌓아 놓다가

보내는 것이 모든게 완료가 되었으면

다시한번

여기로 돌아 와가지고

나머지 큐를 비우는 방식으로 고쳐보도록 하겠다.

3. 첫번째 수정

우리가

물고기를 낚아챈다음에 올려서 물고기를 빼낸다음에 다시한번 낚시대를 강물에 다시 던졌는데

이 작업을 계속 반복하는 것과 마찬가지로

Send함수도 그냥

하나만 사용을 하니까

try안에서 다시한번 낚시대를 던질지 체크를 하면 될거같다.

나중에 가면은

이런 byte배열을 보낼 것이 아니라 패킷이라는 것을 보낼 것인데

일단은 바이트 버젼의 배열로 만들어 주도록 하자.

그래서 이제는 Send를 했다고 해서 RegisterSend를 하는게 아니라

그래서 맴버변수로 sendQueue를 만들어주고

그리고 pending여부를 들고있을 Boolean만들자.

pending이라는 것이

우리가 만약에

여기서 한번이라도 RegisterSend를 했다면은

이 pending이 true상태가 될 것이고

즉, 뭔가를 보내고있다.

그리고 실질적으로 나중에

이녀석이 호출이 되가지고

작업이 다 끝났다면

try안에서 pending을 false로 해주어서 불을 꺼줄 것이다.

그래서 만약에 누군가가

RegisterSend를 하고있다면은

Send에서는 RegisterSend를 실행하는 게 아니라

Queue에다가만

이 byte배열을 넣어놓고 종료를 할 것이다.

설명만 하면 이해가 안갈 수 있으니 코드로 보도록 하겠다.

그래서 맨처음 할 것은

여기 send큐에다가 sendBuff를 넣어 줄 것이다.

그리고 pending상태 인지 아닌지를 테스트 해가지고

        if (pending == false)

== 내가 1빠로 전송을 했다.

라고하면은

REgisterSend를 막바로 할 것이고

그게 아니라고하면은 그냥 여기서 끝낼 것이다.

그래서

여기 아래에 있는 부분은 없어져야 된다.

이렇게

근데, 이게 우리가 이전에 했던 싱글 쓰레드 프로그래밍이라면 이정도만 해도 충분한데

우리는

이 Send라는 녀석을 멀티 쓰레드 환경에서 실행할 수 잇어야 될 것이다.

동시다발적으로 호출 할 수 있으니까

이제는 "락"의 개념이 당연히 들어가야 한다.

Lock

락을 쓰기위해서 이렇게 오브젝트를 하나 만들어 주자.

그래서

이렇게 _lock을 잡아가지고

한번에 한명씩만 여기로 들어오게되는 것이고

큐에 집어넣어주고

pending이 false이면 == 1빠다 라고하면은

RegisterSend() 막바로 호출 해보리기

그러면 이제 흐름상 RegisterSend에 이어서 만들어 주면 될텐데

이제 지금 같은 경우에 해야 될 것은

sendQueue에서 하나를 뽑아와야 할 것이다.

그러면 이녀석은

이런식으로 buffer가 튀어나오게 될텐데

이제 이녀석을 Event와 연결을 시켜줘야 할 것이다.

그래서 그래서 buffer를 _sendArgs에 연결시켜준 것이고

그다음에 이제 sendAsync를 호출을 해서

pending이 false라고 하면은

OnSendComplete가 막바로 호출이 될 것이고

그게 아니라면 == true라면

여기서 예약한것이 막바로 호출이 될 것이다.

그렇다는 것은 Completed가 되었다면

try에서 false로 바꿔줘야하고


까먹은 부분이 이전에

Register를 하는 순간에

true로 켜줘야지만

다른 쓰레드가 (다음애가)

여기서

혹시라도 들어왔을 때

현재 _pending상태를 봐가지고

true라고 하면은 RegisterSend를 스킵하고 안할 것이다.

그래서 _pending여부를 이용을 해서 누군가가

== 이전의 쓰레드가 Send를 예약한 상태인지 아닌지를 이렇게 판별을 하도록 하자.

그래서

_pending = true

Deueue -> buff 받아와서

SetBuffer 해주고 이런식으로 완료를 하였다.

그리고 RegisterSend같은 경우에는 이렇게 애초에 lock을 건 상태에서

처리를 해주고 있기 때문에

별도의 락처리를 안해주어도 되는데

이녀석같은 경우에는 RegisterSend를 통해서 호출이 되었으면

락이 필요 없겠지만!

또 다른 경우가 하나 더있었다.

Start에서

이런식으로 콜백방식으로 호출을 하고있는데

그 경우를 챙겨주기 위해서는 결국 OnSendcompleted도 락이 필요하다는 얘기가 된다.

그래서 락을 걸어주면 조금더 안전한 상황이 된 것이다.


참고로 멀티쓰래드 프로그래밍은 계속 크래쉬를 내면서 연습을 해보는 수 밖에 없다.

그래서 이까지 들어왔다면 성공적으로 들어온 것이니까

_pending을 false로 해주면 될 것이다.

그런데 팬딩을 false로 까지 하는 것은 좋은데

모든게 끝났다고 해서 더 이상 할일이 없다고 볼 수 없는게 뭐냐하면은

이부분으 조금 트리키 한 부분이기는 한데

만약에 패킷이 여기 처음으로 들어와서 RegisterSend를 했다고 가정을 해보자.

(지금은 _pending이 false인 상태)

그런데 RegisterSend에서

Async가 바로 완료가 되지 않아가지고

(Register들어왓으니 _pending이 true인 상태)

_pending이 true인 상태여가지고


이녀석이 조금 있다가 호출이 되는데

그 상태에서 다른 쓰레드가 Send를 했다고 가정을 하면은

현재 _pending이 true인 상태이기 때문에

이부분은 스킵을 하고 Enqueue(sendBuff);만 해준 상태일 것이다.

그렇다는 것은 결국에 누군가는 sendQueue에있는 것을 처리를 해야된다는 얘기이니까

그 부분을

여기 try안에서 챙겨주면 굉장히 아름답개 동작할 것이다.

그래서 만약에

sendQueue > 0 보다 크다고 하면은

이말이 내가 열심히 OnSendCompleted 를 하는 동안 누군가가

_sendQueue다가 열심히 열심히 넣어줬다는 말이다.

여기서

RegisterSend를 다시한번 호출을 해주면 될 것이다.

즉, 내가 예약을 하는 동안에 누군가가 또 예약을 했다면

그 녀석을 이제 RegisterSend를 통해가지고 그녀석을 처리를 해주게 될 것이다.

else일 경우 정말 운좋게 그 사이에 아무도 큐에다가 추가를 하지 않은 상태가 되니까

최종적으로 _pending을 false로 바꿔주면 된다.

첫번째 수정의 주 목적은 SendArgs를 재사용하기 위한 방법이였다.

이전처럼 매번 Send할때마다 new 로 만드는 방법이 아니라

(멤버변수로 만들어 놓았음)

이녀석을 하나만 만들어두고

이 Send를 언제할지 예측을 할 수 없으니까

미리 _sendArgs를 만들어 놓은 다음에

실제로 우리가 Send를 할 때 == _sendArgs가 필요할 때,

RegisterSend안에서 _sendArgs를 사용하고 있었다.

그리고 누군가가 이미 보내고 있는 작업이 완료가 되지 않았다고 하면은

그 녀석은

이렇게 그냥 큐에다가만 넣어주고 스킵을 한상태로 종료를 할 것이다.

그리고 RegisterSend가 실제로 _pending한 녀석이 완료가 되면은

이쪽으로 들어 올것인데

여기서 한번 체크를 해가지고

혹시라도 SendAsync가 지연이 되는 동안에

누군가가 큐에다가 뭔가를 넣어 놨으면은

처리를 하러 갈것이다 == RegisterSend();


그래서 결국에는 우리가 Receive를 한 것과 비슷하게 동작을 한 것이다.

다만, 처음에 얘기를 했던것은

가장큰 문제가 패킷을 한번 보낼 때마다 == Send를 한번 할때 마다,

.SendAsync(_sendArgs);를 한번씩 해주게 될 것이다.

어쨋든 큐에 들어가는 것을 조금 나중에 하도록 우리가 수정을 하기는 했지만

그럼에도 완벽하지는 않다.

Send를 100번 호출하게되면

SendAsync도 언젠가는 100번이 호출이 될 꺼니가

이게 뭐 완전한 해결책은 아니다.

우리가 나중에 진짜 테스트를 하려면

클라에서 세션을 몇백개를 쏴보고 서버에서도 멀티쓰레드 환경으로 돌려봐야

하는데

지금 상태에서는 버그가 있는지 없는지 모른다.

--- 첫번째 수정 완료 ---

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글