Session #3

CJB_ny·2022년 2월 21일
0

Unity_Server

목록 보기
27/55
post-thumbnail

지난 시간에 이어서 세션을 만들어 볼 것이다.

1. 잠깐 복습

지난 시간에 했던거 중에 중요한게

이런식으로 recvArgs, _sendArgs를 만들어서 사용했던것인데

나중에 C++에서도 서버에서 이렇게 진짜 비슷하게 만들 것이니까

유심히 살펴봐라.

그리고

Receive의 경우 SetBuffer로 초기에 버퍼를 셋팅을 하는데

버퍼에다가 어떤 값을 넣는게 아니라 빈 것으로 이렇게 recvArgs에다가 그냥 연결만 해주는 상태이다.

그러면 나중에 클라에서 데이터를 우리한테 쏴주면

그 데이터가 우리가 넣어준 buffer에 저장이 될 것이고요

반대로, Send하는 경우는 조금 달랐다.

이렇게 분명히 같은 인터페이스인 SetBuffer를 하기는 하는데

이녀석이 SetBuffer를 하는 거는 "빈"버퍼를 입력을 하는게 아니라

실제 우리가 보낼 "데이터"가있는 버퍼에다가 그 버퍼의 길이 (== buff.Length)를 넣어주었다.

그리고 _sendArgs는 재사용을 하기 위해서

이렇게 클래스 안에다가 저장을 하고있었다.

그리고 recvArgs도 클래스안에다가 넣어주어도 노상관이다.

다만 인터페이스 다시 맞춰주면됨 (c++이렇게 할 것이다)

이렇게 맞춰주고

이렇게 맞춰 주면된다.

그래서 이렇게 얼규먼트를 각각 하나를 들고있는데

_sendArgs같은 경우에는 이녀석을 계속 재사용을 하면서

이녀석이 실제로 완료될 때까지는 큐에다가만 넣어주고

그리고 실제로 send가 완료가 됬으면은

그다음은 그동안에 _sendQueue에다가 보내줄 정보를 넣어놨으면

이어서 처리를 하게되는 방식을 알아보았었다.

2. BufferList

우리가 지금까지 만든 것중에서 Send부분에서 경우에 따라서 조금더 최적화를 할 수 잇는 방법이 있기는 한데

이부분을 유심히 살펴보면은

Dequeue를 한번씩 해가지고 _sendQueue에있는 정보 "하나"당

SendAsync를 한번씩 해주고 있는데

이부분에서 조금더 우아한 인터페이스를 사용할 수 있다.

우리가 지금 사용하고있는게 Buffer라는 애인데

.Buffer를 해보면

SetBuffer를 하면 진짜 버퍼가 채워지는 것이고

그냥 .Buffer는 진짜 하나짜리를 사용한다는 의미인데,

묘하게도 아래에 BufferList라는 애가 하나 더있다.

애도 똑같은 기능을 하는데 우리가 보낼 정보들을 쭉 리스트로 넣어줘가지고

연결을 해주게되고 그상태에서

sendAsync를 하게되면

여기 BufferList에 있는 모든애들을 다 쫙 보내주는 그런 기능을 한다.

그러니까,

이런식으로 한번, 한번, 한번씩 Setbuffer를 보내기 보다는

BufferList를 만들어서 한번에 보내주면 조금더 효율적이라는 생각이 든다.

그런데 하나 유의해야 할 점이

BufferList도 Null이 아니고 그다음에 SetBuffer도 해줘가지고

동시다발적으로 setBuffer, BufferList를 셋팅을 해주면 에러가 난다.

그래서! 둘중 하나만 골라서 사용해야된다는 의미이다.

그래서 이부분을 조금 수정을 해보도록 하겠다.

그래서 여기서 뭘 수정해야되냐면

Dequeue를 하는 부분을 수정을 해야되는데

while을 하나 둬가지고 _sendQueue에 있는 것들을 다 추출을 하도록 하겠다.

즉, _sendQueue가 빌 떄까지 계속 반복을 할것인데

buffer를 추출한다음에

SetBuffer를 할게 아니라

아까말한 BufferList에다가 넣어줄 것이다.

그래서

이렇게 추출한 buff를 넣고 offset = 0, buff.Length를 Add해주면될 거같기는 한데

놀랍게도 이렇게 하면 안된다.

인터페이스가 Arraysegment로 맞춰 져있어서 이렇게 맞춰 주어야 한다.


ArraySegment

ArraySegment에 대해서 잠시 설명을 하자면,

Array가 우리가 사용한던 배열이였고

그런데 그냥 Array가 아니라 Array의 segment 즉, Array의 "일부"라는 표현으로 Segment를 붙인것이고

실제로 ArraySegment가 받고있는 것을 보면

array, offset, count를 받고있다.

그래서 말그대로 "어떤" 배열의 일부를 나타내는 그런 구조체이다.

이녀석은 타고 가보면

struct == 구조체 이다.

그러니까 class가 아니라 구조체라서 Heap영역에 할당되는 애가 아니라

"스택"영역에 할당되는 녀석이라서

실제로 Add를 할때 값이 복사가 되는 그런 식일 것이다.


그런데 왜 굳이 이녀석을 사용을 하느냐?

C#같은 경우에는 항상 배열을 사용할 때

만약 A = [ ][ ] [ ][ ] [ ][ ] [ ][ ] [ ][ ]

10byte짜리 배열이 있다고 할때

C++같은 경우는

Pointer라는 개념이 있어서 시작 주소를 A[3]으로 옮겨가지고

그 옮긴 주소를 건내 주면되는데

근데 C#같은 경우에는

정상적인 상황이라면은 포인터를 사용할 수 없으니까

애당초 무조건

이 "첫 주소"만 알 수 있다.

그래서

이렇게 index를 하나 넘겨줘가지고,

애가 몇번째부터 시작하는 지 따로 넘겨준 것이고

여기서 이렇게 3번 index부터 시작을 하고싶다고 하면은

Add(new ArraySegment< byte >(buff, 3, buff.Length));

이렇게 넣어주어야 할 것이다.

그리고 크기는 Length로 넣어주고 있었던 것이다.

그래서 기본적으로 어떤 buffer의 범위를 표현하고 싶다라고 하면은

배열(buff), 시작 Index(offset), 실제로 사용할 크기(Length) 이렇게 삼종 세트로 넣어주게 될 것이다.

그런데

여기서 조금 조심해야될 사실은 뭐냐하면은

BufferList에다가 Add를 해가지고 뭔가를 넣어주면 안되고

반드시 list를 다 만들어 준다음에

그다음에 마지막으로 BufferList에다가 equal을 해가지고 넘겨 주어야 된다.

이런식으로 list를 완성을 해서 이렇게 넘겨 줘야된다는 말이다.

이거는 딱히 이유는 없고 그냥 BufferList를 만들때 그렇게 만들어 놨다.

그래서 해결방법은

이렇게 List를 하나 만들어 줄것이고,

list에다가 이런식으로 다 하나씩 추가를 해준다음에

마지막으로 이렇게 연결을 해주어야 정상적으로 작동을 할 것이다.

그리고 list도 RegisterSend를 할때마다 만드는 것은 낭비같은 생각이 드니까

그래서 이녀석도

이쯤에다가 이름 바꿔서 수정, 넣어주도록 하자.

말그대로 "대기중인 목록이다!"라는 의미이다.

그리고

clear를 한번 해준다음에 이렇게 하면되겠다.

그리고 이부분을 유심히 살펴보면은

지금 _pending이라는 Boolean값을 사용하고 있었는데

애당초 _pendingList라는 것을 사용을 하면은 이 정보를 이용을 해가지고

실질적으로 대기중인 정보가 있는지 없는지를 "판별"할 수 있을 거같다라는 생각이 든다.

그래서 Send에서

_pending == false일 경우가 아니라

일 경우 대기중인애가 한명도 없다는 뜻이 되니까

그때 바로 RegisterSend를 해라! 라고 넘어오면 될것이다.

그다음에

이쪽으로 와보면은

_sendQueue가 > 0 이라면은 RegisterSend를 하고

그게 아니라면 == else == 0이라면,

_pending을 false로 하고있었는데

이 코드흐름을 조금 수정을 하자.

3. 코드흐름 수정 && 정리

이렇게 BufferList를 null로 밀어 버릴 것인데

why => 굳이 BufferList가 pendingList를 가지고 있을 필요는 없으니까

그리고 펜딩리스트도 비워주자.

OnSendCompleted는 우리가 예약한 Send가 완료되었다는 것이니까

여기서 비워주면 될 것이다.


그리고 몇바이트를 보냈는지 확인하기 위해서

이렇게 해주자 (나중에 지울 꺼임)

이렇게하면 _pending의 Boolean역할을 _pendingList.Clear()가 대신 해주고 있는 셈이다.

이부분도 사용을 이제 안해도 될거같고

Count == 0 일때 RegisterSend를 해주고있으니까

여기서도


RegisterSend를 했다는 것은

_pendingList를 비웠다는 뜻이니까

이부분이 없어도된다.

이렇게 까지 수정을 하고 코드 흐름을 조금 다시 보자면은

아까와는 조금 다른데,

우리가

1) Send

Send를 했다고 가정을 해보면

lock을 잡고 _sendQueue에다가 일감(== sendBuff)를 넣어 줄 것이다.

그리고 _pendingList가 없다라는 가정이 true라고 하면은, (== 내가 거의 1빠로 들어왔다고 하면은)

RegisterSend를 호출을 할 것이다.

그게 아니라 _pendingList에 예약된 목록이 있다고 하면은

RegisterSend를 Skip을 할테니까 그냥

_sendQueue에다가만 넣어주고 나가는 그런 상태가 된다.

그다음에

2. RegisterSend

까지 들어왔다면,

일단은 _pendingList는 null (== 0)인 상태이다.

_pendingList가 빈 상태일 때만 RegisterSend에 들어 올테고

그 상태에서 _sendQueue가 0보다 클동안 == 안에있는 녀석들을 하나씩 하나씩 다 뺴가지고

그녀석을 _pendingList에다가 Add(== 넣어둔다음에)한다음에

그녀석을 이제 BufferList에다가 넣어줄 것이다.

그러면 BufferList에는 지금까지 예약된 모든 목록들이 들어있을 것이고

그녀석을 이제 SendAsync를 하면은

이전에는 버퍼 하나만! 있으니까 그녀석만 보내주고있었는데, 지금은 _sendArgs의

buffer는 null인 상태고 BufferList는 _pendingList로 연결이 되어있으니까

이녀석을 한번에 싸그리 쫙 다 보내 줄 것이다.

그리고 바로 보낼 수 있다면

bool pending = _socket.SendAsync(_sendArgs); 의 값이 false로 retrun을 할테니까

OnSendCompleted를 실행을 할 것이고

그게 아니라고 하면은!

조금 있다가 OnSendCompleted가 완료가 될 것인데

어쨋든 어느 순간에는 OnSendCompleted가 완료가 뜰 것이고

3) OnSendCompleted

이 상태에서 만약에 성공적으로 (74번째 줄) 보냈다고 하면은

이렇게 다음 단계를 보낼 준비를 해야된다.

_pendingList는 Clear()를 해준다 => 왜냐하면 이때까지는 보낸것은 성공적으로 다 보냈다라는 의미가 되니까

  • 혹시모르니까 BufferList를 null로 밀어주고있다.

그 다음에

여기서 _sendCount가 0인지 아닌지를 체크를 해가지고

_sendCount가 0이 아니다 == 즉,

이렇게 열심히 보내는 동안에

누군가가 _sendQueue에다가 패킷을 넣어줬다고 하면은

다시한번

RegisterSend를 호출을 해가지고

아까와 마찬가지로

이부분으로 돌아 올 것이다.

그래서

1) -> 2) -> 3) 을 반복하게 될 것이다.


그래서 테스트를 해보면

아까와 마찬가지로 잘 보내지고 있다.


4. 수정해야 될 부분들 == 너무 많은 정보들

이렇게해서 1차적인 개선 사항을 넣어 준것이다. 그런데 이렇게 한다고해서 아직까지는 모든 문제가 완벽하게 해결되지는 않는다. 여기서 볼 수 있는 첫번째 문제는 RegisterSend를 할때

여기서 _sendQueue를 무조건 비워가지고

모든 정보를 다 보내고 있는데

여기도 마찬가지고 Receive를 할 때도 마찬가지지만,

이런식으로 무조건 받고 보내면 안되고

지금까지 내가 순식간에 == 일정한 짧은 시간동안 몇byte를 보냈는지 좀 추적을 해가지고

너무 심하게 많이 보낸다 싶으면은

조금 쉬면서 하는게 조금더 좋기는 하다.

왜냐하면은

진짜 동시다발적으로 패킷이 몰릴 때

여기서

어거지로 상대방이 다 받을 수도 없는데 다 보내는 것도 약간

문제가 될 것이다.

그리고 심지어 악의적인 유저가 디도스 공격을 하듯 막

정보를 막 뿌려대면은

그런부분들은 Receive를 할 때

여기서 체크를 해가지고

비정상적이다라고 판단이 되면은

Disconnect를 해서 쫒아 내야될 것이다.

그런부분들이 뭔가 추가되기는 해야 할 것이다.

5. 두번째 수정 == 패킷 보내기

지금까지 우리가 한것은

pending된 애들 ==

sendQueue에 예약된 애들을 뭉쳐가지고

한번에 보내는 방법으로 하기는 했는데

사실, "패킷 모아보내기"작업은 이거보다 조금더 많은 작업이 들어가기는 해야된다.

지난번에 얘기한것 처럼

어떤 Zone에서 1000명의 유져가 있다고 가정을 했을 때,

각각의 유저들이 움직이고 스킬을 쓰고 그럴것이다.

그렇다는 것은 그 하나의 유져가 하는 모든 행동들을

다른 999명의 유저들한테도 싸그리 다 뿌려가지고

계속 정보를 서로 공유를 하면서 진행이 될텐데

A라는 유저가 옆으로 움직였다! 라는 정보를 Send를 통해서 넣어주게 되면은

어떤 방식이든 RegisterSend까지 가가지고


bool pending = _socket.SendAsync(_sendArgs);

이렇게 뭐 보내주고 있을 것이다.

그런데!

경우에 따라서는

패킷 자체를

이렇게 작은 패킷으로 만들어서 보내는 것이 아니라,

걔내들 자체를 뭉쳐가지고 보내야 할 때가 생긴다.

예를 들면은

어떤 유져가 움직였다! 라는 행위

바로

이렇게 Send를 하는게 아니라

1000명의 유저들이 아주 짧은 시간동안 움직이고 스킬을 쓰고 하는 모든 "행위"들을

다 "기록"한 어마어마하게 큰 Buffer를 만든다음에

그녀석을 돌아가서면서

Send를 하게되면은

훨씬더 성능 개선이 있을 거같다라는 생각이 든다.

이런식으로도 사실 개선을 할수는 있는데

패킷을 모아 보내기

이렇게 하는것이 패킷을 모아 보내는 것인데

한번에 작은 단위들로 보내는게 아니라

일단, 많은 패킷들을 최대한 모아보내는 것을

과연 우리가 만들고 있는 ServerEngine에서 해줄 것인지

아니면 Contents딴에서 그것들을 모아서 Send를 한번만 하도록 요청할 것인지

조금 길이 갈리게된다.

그거는 프로젝트마다 하는 방법이 조금 다르긴 한데

엔진은 패킷을 모아서 보내지 않고

이정도 선에서만 끝내고

나중에 컨텐츠를 만들때

그 특정영역 Zone에 있는 모든 행위들을 다 기록을 했다가

한번에 모아서 보내는것이 조금더 낫다고 생각이 든다.


6. 정리

그래서 우리가 했던 것들이 의미가 아예 없는것은 아니고

패킷이 동시다발적으로 몰렸을 때

예약한 애들을 == _sendQueue에 있는 애들을

어떻게든 조금 한번에

SendAsync로 보낼 수 있도록 "개선"을 한것이다.

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

0개의 댓글