SendBuffer

CJB_ny·2022년 2월 23일
0

Unity_Server

목록 보기
32/55
post-thumbnail

SendBuffer 맨들어 주는데

RecvBuffer보다 조금더 어렵다.

우선 Server > Program으로 가보면

여기서 Send를 하고 있었다.

그런데,

외부에서 이렇게 바이트 배열을 만든 다음에

Send라는 인터페이스를 통해서 sendBuff를 넣어주고 있었다.


그 다음 Session > Send를 보면

이녀석은 다시 _sendQueue에다가 받은 sendBuff를 저장을 해둔 다음에

나중에 시간이 될 때 RegisterSend에 가가지고 순차적으로 보내는 작업을 하고 있었다.

SendBuffer 와 RecvBuffer의 가장 다른점

RecvBuffer의 경우

이렇게 안에 있었다.

그러니까 Session마다 자신의 고유 _recvBuffer를 가지고 있는것 이다.

그리고 이렇게 구현하는게 당연한 이유가 뭐냐하면은

클라가 보내는 모든 정보가 각기 전부다 다를것이다.

즉, 요청사항이 다 다들테니까 자기들만의 개인 정보가 있어가지고 유저들간에

무슨 정보를 보냈는지 추적을 한 다음에

이녀석을 한번씩 Clean()으로 정리를 하는 작업을 하고있었다.

그리고 이게 가능한 이유는

Session과 _recvBuffer가 1ㄷ1 관계였기 때문에 이렇게 내부적으로 관리를 할 수 있었던 것이다.


그런데 SendBuffer같은 경우에는 안에 있는게 아니라 밖에 빠져있다.

이렇게 ServerCore에 있지 않고 Server쪽에 sendBuff가 들어있다(켄텐츠 쪽에)

보내는 순간에 외부에서 바이트를 만들어주고 있다.

지금은 문자열을 바이트 배열로 받아서 보내고있는데

나중에는 "패킷"이라는 개념으로 보내게 될 것이다.

Knight라는 클래스가 있을 떄,

이런식으로 Knight라는 정보를 sendBuff에다가 보내주기는 해야되는데

이렇게하면 Knight클래스의 int형 값을 100이라는 값(4byte짜리 값)을

byte 배열로 바꿔주는 역할을 한다.

이런식으로 튀어 나올텐데

그러면 4byte짜리 buffer인데 이 buffer안에는 hp가 100이라는 수치가 들어가된다.

attack도 똑같이 추출을 한다음에 이 정보를 어떻게든 sendBuff안에다가 넣어 줘야된다.

Copy를 하는데 source > destination이다.

그래서 buffer의 offset 0 부터 sendbuff의 offset 0에 넣어주는데 buffer.Length만큼 복사를 sendBuff에 한다는 것이고

buffer2는 목적지의 offset에 buffer.Length를 받아서 buffer2.Length만큼 복사를 sendBuff에다가 해줘야 할 것이다.

그래서 최종적으로

sendBuff를 Send(sendBuff)로 밀어넣어주게 되는 것이다.

근데 좀 까다로운 것은 우리가 1024byte를 할당했는데 보내는 것은 8byte이다.

어쨋든 이런식으로 동작을 할 것이다.


1. 성능적 이슈 (Send)

그러면 애당초 왜 sendBuffer를 ServerCore쪽 (안에다가) 밀어 넣을 수 없냐?? 하는 부분이 궁금쓰한데

이거를 내부에서 "복사"를 하는 방식으로 채택을 하게되면은 문제가 되는 부분이 뭐냐하면은

성능적 이슈가 있다.

어떤 존에 100명이 있을 때 한명이 움직이면 이동패킷을 99명한테 다 보내야하는데

A라는 유져의 이동패킷이 대충 100명한테 전송이 되야 할 것이다.

근데 100명다 움직이고 스킬을 쓰고 하니까

이동패킷이 100 * 100 즉, 10000개가 전송이 되야 한다는 것이다.

그래서 Send를 하는 횟수가 ㅈㄴ 빈번한데

내부에서 놔둬서 복사를 하는 방식을하면 만번 복사를 하게된다.


근데, 그게 아니라 외부에서

이런식으로 한번만 만들어 준다음에

Send를 할때 루프를 돌면서 처리를 할 것인데

지금 존에 있는 유져들을 for로 다찾고 Send를 하게된다면 복사횟수가

"현저히"줄어들 것이다.

그래서 내부에서 작업하는 것보다

"외부에서 만들어서 꽂아 넣는 방식"이 훨씬더 효율적이다.


그런데 여기서 또 발생하는 문제가 뭐냐하면은

애당초 이 버퍼 사이즈를 뭐로 해야할지 궁금하다.

만약 Knight 클레스안에 string name; 이나 스킬같은게 있을떄

name같은 경우 가변적으로 (유져마다 닉네임이 다르기때문에) 변하기 때문에

sendBuff의 크기를 어떻게 해줄지 애매하다.

그래서 sendBuff에 넣어줄 데이터가 크기가 어느정도 인지 예측을 할 수 없으니까

무작정 버퍼의 사이즈를 크게 하는게 하나의 방법이 될 수 있는데

이렇게하면 낭비가 심하다는 생각이 들 수 있다.

그래서 그냥 엄청 큰 녀석을 만든다음에

이녀석을 칼로 잘라먹듯이 차츰차츰 사용해나가는 방식을 하면 조금더 좋을 거같다라는 생각이 든다.


2. 코드 구현

이렇게 byte, int 만들어준다.

처음에는 u가 여기서부터 시작을 하고,

계속 사용을 할 때마다 u가 우측으로 이동할 것이다.

RecvBuffer.cs에서 _writePos에 해당하는게 _usedSize라고 생각하면된다.

FreeSize를 하나 만들어 주는데

현재 "남은 공간"이 될 것이다.

u의 위치가 여기면은 _buffer.Length - _usedSize하면 밑줄 친 부분이 될 것이다.


그다음에는 이제 Open, Close라는 인터페이스를 파줄 것이다.

Open은 현재 상태에서 _buffer를 사용할 것이라고 Open을 하면서 내가 얼마만큼의 최대 사이즈를 사용할 것인지를 인자에 넣어주는 것이다. == reserveSize

그러면 지금 남아 있 공간 == FreeSize보다 reserveSize(예약하려는 공간)이

더 크다면,

이 버퍼는 고갈 되어서 더이상 사용할 수 없으니까 null return;

_buffer의 offset을 _usedSize 셋팅해서 return 하는데 예약한 크기만큼(사용할 크기만큼)을 return해준다.

reserveSize는 예상하는 최대 크기를 말하는거지 실제 사용할 Size는 아니다.

그러면 오른쪽에 있는

4096이라는 최대 크기를 reserveSize로 받는 다는 말이 될 것이다,

다쓴 다음에는 실제로 사용한 사이즈를

Close에다가 넣어 줄 것이다.

일단, Close를 했다는 것은 확정을 한것이다.

내가 이렇게 데이터를 찝어 줬는데 이래저래 데이터를 넣어주다 보니까

실제로는

이렇게 2byte만 사용을 했다고 컨펌을 해줘가지고 buffer를 최종적으로

다 썻다고 반환을 하는 부분이 될 것이다.

그럼 _usedSize += usedSize만큼 늘리는 개념이 되는데

(u를 이동을 시켜줘야되는데)

다시한번 "유효범위"를 찝어서 return을 해주긴 해야된다.

segment를 반환하는데

_buffer를 반환하는데 Offset이 _usedSize이고 크기는 "실제" 사용한 크기인

usedSize만큼을 넣어서 segment로 반환하는 것이다.

3바이트를 찝어 줬는데 실제로는 2바이트만 사용을 해서 2바이트를 usedSize로 찝어준것이다.

이렇게 퉤 한다.

이게

SendBuffer의 양식이고

ReceiveBuffer와 다르게

이녀석을 정리를 하는 개념은 없다.

그냥 간단하게 멀티쓰레드 환경에서 sendBuff를 누군가 참조를 계속 할 수 있어서

일회용으로 사용을 하고 Clean() 개념은 없다. (재사용 ㄴㄴ)


그리고 SendBuffer 클래스를 사용하기 쉽게

Helper class를 만들 것이다.

SendBuffer의 경우 한번만 만들어 준다음에 고갈 될 때까지 쭉 사용할 예정이기는 한데

이 SendBuffer를 전역으로 만들면 사용하기가 편리 할거같다!

=> 근데 멀티 쓰레드 환경이라면 "경합" 문제가 발생을 하니까

그것은 안될 것이고

이전에 배운 threadLocal로 만들어 주자.


즉, "전역"은 "전역"인데 나의 쓰레드에서만! "고유"하게 사용을 할 수 있는 녀석이라고 했다.


그래서

이렇게 만들어주고,

처음에 만들때 저기 new ThreadLocal< SendBuffer > ( ) 여기 인자에

맨처음에 만들어 줄때 뭘 할것인지 기입을 해줄 것이다.

일단

아무것도 안하고 null인 상태로 반환ㄱㄱ

청크 사이즈를 만들어서 나중에 외부에서 바꾸고 싶다면 이녀석 건들면된다.


아 중간에 buffer를 안 만들어줬었는데

이렇게 만들어 주자.

(왜 chunk냐? 뭉탱이 느낌으로 SendBuffer를 어마어마하게 크게 잡을거라는 의미에서)


그다음에 다시 들어가는 부분은

매핑을 해서 편하게 사용하기 위해서

똑같이 만들어 주자.

Close는

현재 사용하고 있는 Buffer에서 Value.Close(usedSize)로 매핑을 해서 보내 줄 것이고

Open은 조금 다른데

이녀석은 ThreadLocal에 있는 CurrentBuffer를 관리를 하기 시작해야 한다

null이라고 하면은 아직 한번도 사용을 안한 셈이되니까

이떄 SendBuffer를 만들어 준다.

그런데 지금 CurrentBuffer가 있기는 한데

니가 요구한 reserveSize보다 작다고 하면은

기존에 있던 SendBuffer날려버리고 chunkSize를 넣은 새로운 SendBuffer를 만들도록 하겠다.

그리고 이 if문 두가지를 다 통과했다면

현재 CurrentBuffer에 공간이 남아있다는 얘기이니까

이렇게 뱉어 주도록 하겠다.


이제 이것을 어떻게 사용을 해야되냐면

OnConnected에서 보내고 있던 부분을 조금 바꿔 보면은

Open을 할 것인데

즉, 내가 Buffer를 사용하고싶은데 최대 몇바이트를 사용할 것인가를 넣어주는 것이다.

근데 Open이 뱉어주는 것은

ArraySegment를 뱉어주니까

이렇게 받아서 segment에다가 공간을 찝어 줬으니까 여기다가 이제 값들을 넣어야 한다.

그래서 이부분들을 넣어 줄 것인데


이렇게 될 것이고 offset같은 경우는

이렇게 된다.

어쨋든 이렇게하면 우리가 열어준 openSegment에 정보가 들어 갈 것이고

그리고 여기서

우리가 몇바이트를 넣어줬는지 추적을 할 것이다.

지금 같은 경우는 buffer.Length, buffer2.Length를 넣어줬는데

즉 4바이트씩 총 8바이트를 사용한 셈이될 것이다.

그래서 이제 다사용을 해서 닫아 줄 것인데,

몇바이트를 사용했느냐? 이부분이

이만큼 사용을 했어! 라고 넣어주는 것이다.

그리고 값을 뱉어 줄 것인데

이 buffer가 우리가 실질적으로 보내줘야하는 sendBuffer가 되는 것이다.

그래서 이녀석 없애고

이렇게 될 것이다.


Session > Send를 보면은

이렇게 sendBuff를 받고있었는데

굳이 byte[] 형식으로 받을 필요는 없으니까

인터페이스를

이렇게 맞춰 주자.

그리고 올려서 _sendQueue도 똑같이

ArraySegment로 받을 수 있도록 맞춰 주자.

그리고 다른부분에서 에러가 나는 부분이 없는 지 확인을 하자.

RegisterSend에서 buff 받는 부분 바꿔주고

다시 ArraySegment를 만들어 줄 필요 없이

그냥 buff넣어 준다.


지금

Server > GameSession의 인터페이스가 지금은 직접 만들어서 사용을 하는데

이것을 직접만들어서 사용할 일은 없을 것이다.

나중에 당연히 "패킷 시리얼라이즈" 개념이 들어가게되면은 이런것들은

자동화해서 처리를 할 것이기 때문에

사용하기 어렵다고 지금걱정할 것은 아니다.


지금 중요했던것은

이녀석을 공용으로 관리를 해서 꺼내오는 부분이 중요했다.

그리고 굳이 ThreadLocal 사용한 이유는 컨텐츠 끼리의 경합을 없애기 위해서 ThreadLocal을 사용한 것이다.

쓰레드마다 자신의 청크를 크게 할당을 한다음에

그녀석을

여기서 계속해서 쪼개서 쓴다는 의미가 되겠다.

이렇게 데이터를 이래저래 받다가 실제로는

buffer.Lemgth + buffer2.Length 만큼밖에 안받았다고 컴펌을 한다.

이렇게 u를 이동을 시킨 셈이 될 것이다.

그래서 Bufffer를 열고 닫고하는 개념이 들어간 것이다.

실행하면 잘 돌아간다.


3. 눈치가 빠르면 알 수 있는거

두이 27?분 28분부터 뭔말 하는지 모르겠다 (다시 듣기)

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

0개의 댓글