관리 메뉴

有希

Session#2 본문

프로그래밍/C# 게임서버

Session#2

有希. 2021. 9. 9. 15:57

스레드가 동시 다발적으로 RegisterRecv를 등록한다면 Lock으로 인해 OnRecvCompleted에 동시다발적으로 들어올 수 없지만, 지금 구현된 간단한 Send는 그렇지 않다. recv처럼 2가지로 쪼개줘야 한다.

1. Send하려는 순간 RegisterSend를 호출한다.

이렇게 하면 Send에서 args를 설정해주고, delegate에 이벤트를 설정해주고 RegisterSend를 설정해주면 된다. 하지만 이전처럼 OnSendCmpleted에서 Register를 다시 해주게 되면 Args를 매 번 다시 만들어서 사용하고, Completed에도 매 번 재등록한다.(Recv도 마찬가지. 이를 재사용하게 수정해줘야 한다.)

또한, MMORPG를 보면 한 보스를 잡을 때 몇십 명씩 모이는데 이럴 때 누가 움직였다는 정보를 시야 내의 모든 인원에게 보내야 하고 스킬도 마찬가지이다. 이 보내는 것을 시야 내의 모든 인원수 * 행동 수 만큼 한다면 연산 부하가 매우 크다. Send, Receive는 커널단에서 처리해주는 작업이기 때문에 안그래도 부하가 꽤 되는데 이런 작업을 곧이 곧대로 발생하는 만큼 수행하면 안된다.

Event를 만들어서 매 번 보내는 것이 아닌 여러 정보를 뭉쳐서 한 번에 보내는 방안을 생각해 볼 수 있다.

1번의 구현은 이전의 RegisterRecv처럼 하면 된다. 다만, Send는 성공적으로 보냈다면 그것으로 끝이기 때문에 성공했을 시에 뭔가 더 해줄 필요는 없다.

여기까지 구현했다면 Args들을 재사용 하기 위해 위치를 전역으로 옮겨준다.

또한, Send함수는 내용이 바뀌는 것이 아니기 때문에, Completed에 이벤트를  등록하는 것도 Start로 이동해준다.(Recv도 마찬가지로 없다면 이동)

이렇게 고치고 나면 재사용에 관한 부분은 어느정도 해결되었다.

2. 작업 Queue?

각 스레드마다 자신의 byte[]를 가지고 Send에 접근하게 되면 같은 버퍼를 여러 쓰레드가 사용하고 보내고 하게 되는데 이러면 각자의 값들이 섞인 엉망인 값이 전송된다.

따라서, 어떤 Queue에 차곡차곡 쌓아서 OnSendComplete가 호출되기 전까지는 Send를 하지 않고 Send를 하는 방식으로 고쳐야 한다. -> OnSendCompleted가 호출됐다는 말은 전송이 완료되어 전송 buff에 어떤 짓을 하더라도 상관없다는 의미이기 때문. 반대로 호출되지 않았다면 아직 전송해야할 값이 남아있으므로 멋대로 조정하면 안된다.

우선, 전역으로 공통 사용할 Queue를 만들어 준다. byte[]를 send할 것이므로 <T> 는 byte[]

그리고 ,누군가가 send를 하고 있는지를 지정할 bool _pending 값도 전역 생성한다.

그 후, Send를 멀티스레드 시스템에서 돌아가도록 수정해준다. 동시에 Queue를 수정하면 앞서 buff 예시처럼 엉망진창이 되므로 Lock을 써준다. 이전의 Register~ 함수처럼 큐에 집어넣기->send중이 아니라면 RegisterSend 호출 패턴을 실행해준다.

마찬가지로 RegisterSend도 수정해준다. 곧바로 들어오자마자 _pending을 true로 만들어준다. 이 함수는 가장 초기 상태에서 호출된다면 lock내에서 호출되기 때문에 내부에서는 lock을 사용하지 않아도 되고, 전송 중에는 _pending으로 인해 중간 접근이 되지 않는다. 버퍼에서 byte[]를 하나 꺼내와 SendAsync를 해준다. 이번엔 반환값인 bool을 받아서 즉시완료라면 OnSendCompleted를 호출해준다. 즉시 완료가 아니라면 알아서 OnSendCompleted가 Event delegate로 호출된다.

OnSendCompleted도 수정해줘야 한다. 여기서는 Queue에 대해 쓰거나 읽는 부분이 없지만, Send가 완료됐다면 _pending=false로 해서 다른 애들이 Send를 할 때 전송중이 아니면 RegisterSend를 할 수 있도록 해줘야 한다.

!문제는 비동기로 여러 Send를 실행했는데 동시에 끝나버린다면 Event로 인해, 여러 OnSendCompleted가 실행되버린다. 이러면 args의 에러 체크나, args의 전송바이트 수가 섞여버려서 에러 검출을 올바르게 할 수 없다. 그렇기 때문에 OnSendCompleted의 내부도 lock을 걸어줘야 한다.

다시 또 한가지 더, 내가 Send함수에 진입해서 Queue에 값을 집어넣고 RegisterSend()를 호출하고 lock을 풀어줬는데(아직 OnSendCompleted는 X) A가 Send와서 Queue에 byte[]를 넣었는데 아직 _pending이 true여서 RegisterSend()는 하지 않고 lock만 풀어줬다고 가정해보자. 그러면 다시 이후에 B가 와서 RegisterSend()를 해줘야 이것이 전송된다.

이러면 B는 A의 것을 전송한 것이고, B의 것은 C가 와서 전송해줘야 한다. 이것을 방지하기 위해 약간의 트릭을 쓴다.

OnSendCompleted에서 if문을 둬서 Queue의 사이즈가 0보다 크다면 계속해서 RegisterSend()를 호출해주고 0일 때에만 _pending=true로 해준다. 그러면 A의 것이 처리됐을때, B가 와서 넣은것이 있으므로 자동으로 B의 byte[]가 RegisterSend()로 보내지고, C의 것도 다시 B의 것이 완료됐을 때 보내진다.

다만, sendBuff를 100개 보내려고 하면 sendAsync도 100번 호출된다. 전송을 비동기로 했다는 것에 의의를 두자.

'프로그래밍 > C# 게임서버' 카테고리의 다른 글

Session#4  (0) 2021.09.09
Session#3  (0) 2021.09.09
Session#1  (0) 2021.09.09
소켓 프로그래밍 - 비동기 연결  (0) 2021.09.09
소켓 프로그래밍 기초  (0) 2021.09.07