링 버퍼

2021. 11. 12. 00:29게임서버/namespace univ_dev

320x100

안녕하세요 대학생 개발자입니다.

오늘은 링버퍼에 대해서 다뤄볼 예정입니다.

 

우선 시작하기 전에 게임서버에 링버퍼가 왜 필요한지에 대해서 간단히 설명해야될 것 같습니다.

게임서버는 당연히 빠른 속도와 안정성이 생명입니다.

안정성의 경우에는 모든 케이스에 에러체크를 하고 꼼꼼히 코드를 짜도 터지는 문제긴 하지만... 속도의 경우에는 다른데요...

어쩔수 없이 사용해야 되는 큰 코스트를 가진 함수들 가령... 입출력에 관련된 함수들, 혹은 매우 큰 로직, 했던 코드의 반복 이런 다양한 경우가 있을건데요

 

이번에 생각해볼것은 어쩔수 없이 사용해야 되는 큰 코스트를 가진 함수들에 대해서생각을 해볼겁니다.

우리가 socket io를 하게되면 어쩔수 없이 호출하는 함수중 가장 많은 비중이 차지하는 함수가 아마 send나 recv일겁니다.

하지만 send와 recv의 속도는 매우 느리기 때문에... 문제죠... 하지만 아얘 안쓸순 없잖아요..? send를 안하면 어떤 함수를 쓰겠습니까... 혹은 recv를 안쓰면 어떻게 수신버퍼의 데이터를 긁어오겠습니까?

 

우선 send와 recv의 동작을 다시 한번 짚고 넘어가면...

send의 경우에는 L7의 버퍼를 L4로 copy하는 작업을 합니다.

반대로 recv의 경우에는 L4버퍼를 L7버퍼로 copy하는 작업을 하는함수입니다.

하지만 이녀석들은 매우 악질적인 녀석이죠... 매우 무거운 함수라는거에요...

 

그렇기 때문에 제가 말하려고 하는것은 보낼 것이있다면... 최대한 모아서 한번에 보내고 받을 것이 있다면 최대한 한번에 모아서 받자 이겁니다.

우선 보낼 데이터가 있으면 모두 sendRingBuffer에 저장할거고 프레임에 select를 호출하는 그순간 보낼수있는 클라이언트들에게 모조리 보낼겁니다.

반대로 recv의 경우에는 받을 데이터를 select를 하는순간 모조리 한번에 왕창 긁어와서 recvRingBuffer에 넣을거고 그걸 나중에 로직부분에서 마샬링을 해서 맛있게 구워먹을겁니다.

예전에 만들어놓은 select 서버 같은 경우에는 하나의 패킷이 만들어지게되면 send를 하는 케이스였죠...

반대로 recv의 경우에는 링버퍼가 없어서 일단 16바이트로 프로토콜크기도 통일했어야됬고... 그리고 16바이트씩 패킷이 없어질때까지 읽었어야 됬으니까요... recvByte / 16번 recv를 호출해야되는거죠...

하지만 이젠 그냥 링버퍼의 남은 바이트 만큼 잡아놓고 한번에 왕창 Enqueue하고 그걸 Dequeue하면서 읽어버릴겁니다. 단한번의 recv를 호출하겠다는 겁니다. 당연히 Enqueue Dequeue도 메모리 카피니까 늦습니다. 하지만 커널모드 전환되고 L4에서 L7으로 복사되는 속도보다는 매우 빠를것이라고 예상하고있습니다.

 

 

TCP 송수신 버퍼의 경우에는 window sliding 방식을 채택하는데요... 이게 곧 링버퍼의 형태입니다.

TCP에서는 ack에 대한 pointer, seq에 대한 pointer 두가지를 가지고 있는데 이 두가지를 가지고 순환하는 하나의 버퍼를 꾸리는거죠...

그럼 우리의 경우에도 똑같습니다. send할 데이터를 모두 sendRingBuffer에 저장하고 반대로 recv할 데이터는 모두 recvRingBuffer에 저장할겁니다.

대신 우리의 경우에는 writePointer와 readPointer 두가지를 두고 할겁니다

사실 말이 거창했습니다. 하지만 별거없습니다. 원형큐를 만들겠다는 의미입니다.

대신 하나의 type을 꺼내는 큐가 아니라 Dequeue에 입력된 바이트 수만큼 뽑아내겠다는겁니다.

반대로 Enqueue는 사이즈만큼 버퍼를 저장하겠다는 의미구요...

뭐... 다들 원형큐는 어떻게 생겼는지 아실거라고 생각합니다. 위에 gif파일로 첨부드린 저게 원형큐에요...

안타깝게도 wide charactor라서... 각 인덱스당 2바이트지만요... 저같은 경우에는 그냥 1바이트씩 꺼내겠다는거에요

 

그럼 바로 구현부로 넘어가시죠

 

링버퍼 클래스입니다.

먼저 기본생성자랑 int하나 받는 생성자가있습니다.

둘다 역할을 똑같습니다 다만 사이즈를 내가 정하느냐 아니면 디폴트 사이즈로 가느냐 차이입니다.

begin에 동적할당 하고

writePointer = readPointer = begin해주는겁니다.

end에는 begin + size를 할겁니다.

그럼 end는 실제 마지막 요소보다 1바이트 더 뒤에있는데요. 이렇게 한 이유는 제 링버퍼는 빼고 한칸가고 넣고 한칸가고 이런 방식으로 설계되다 보니까 end가 마지막 한바이트 더 뒤에있는게 코딩하기가 수월했기 때문입니다.

즉 10000을 입력하면 실제 사용하는 공간은 9999바이트가 되는거죠

 

음... 일단 주요 함수들 몇개 부터 글로 정리를 좀 하고 코드로 보도록하겠습니다.

너무 당연한건 그냥 한줄 정리하겠습니다.

ReSize -> 말그대로입니다. 리사이즈 하고 이전 버퍼 카피하는겁니다.

GetBufferSize -> end - begin -1입니다 즉 ringBufferSize - 1이랑 동일한 값이 리턴됩니다.

GetUseSize -> 사용중인 사이즈입니다.

GetFreeSize -> 미사용중인 사이즈입니다.

 

DirectEnqueueSize -> 만약 writePointer와 readPointer가 뒤집어져 있다면 바로 모든 데이터를 뽑아내려고하면 버퍼를 오버플로 하게될겁니다. 그래서 한번의 memcpy로 뽑아낼 수 있는 양입니다.

DirectDequeueSize -> 똑같은데 Dequeue에 적용되는 건입니다.

 

Enqueue -> 데이터 집어넣는겁니다.

Dequeue -> 데이터 빼는겁니다.

Peek -> 데이터 복사해오는겁니다.

 

MoveRear -> writePointer를 뒤로 밀때 쓰려고 만든겁니다. 매번 end까지 도착했는지 확인하기가... 귀찮았기때문에 그냥 만들었습니다.

MoveFront ->이건... Peek같은거 했을때 데이터가 맞으면 굳이 한번더 Dequeue를 할 필요가 없기때문에 그냥 그만큼 땡겨버릴려고 만든겁니다.

ClearBuffer -> writePointer = readPointer = begin 즉 데이터 다 날리는겁니다.

Get...Ptr -> 이것도... recv할때 버퍼에 복사했다가 그걸 Enqueue하는것보다 그냥 recv할때 바로 저기 writePointer를 전달하려고 한겁니다. 반대로 send할때 버퍼에 Dequeue했다가 그걸 복사하는게 낭비라서 만들었습니다.

 

 

생성자입니다... 설명과 동일합니다.

 

음... 여기도 뭐 정리할 부분이 따로 없는것 같습니다.

3분정도 고민하면 왜 이렇게 했는지 알수있을거니까요...

 

 

Enqueue입니다.

일단 남는 사이즈가 넣으려는 사이즈보다 적으면 그냥 넣지 않고 0을리턴하게 했습니다.

 

그리고 한번에 넣을 수 있는 사이즈가 사이즈보다 크다면 한번에 쫙 복사해버리고 writePointer를 확땡겨버립니다.

그리고 저장 성공한 사이즈를 리턴합니다.

반대로 한번에 저장할 수 있는 양보다 사이즈가 더 크다면

우선 가능한 사이즈만큼 넣고 남은 사이즈만큼 한번더 넣어줍니다.

 

Dequeue입니다.

음... 이것도 Enqueue랑 거의 동일합니다. 하지만 반대로 작동된다는 점이다른거죠.

 

Peek인데요...

Dequeue랑 동일한데 두번째 if문에서 true가 나오면 그냥 복사만 하고 리턴합니다. 따로 MoveFront같은건 호출하지 않습니다.

하지만 그 이후에는 두번에 걸쳐서 해야되기때문에 MoveFront를 한번 호출해서 readPointer를 begin으로 만들어주고 그리고나서 한번더 복사하고 복사해뒀던 readPointer로 돌려놓습니다.

 

MoveFront, MoveRear함수입니다.

들어온 사이즈만큼 더하고 그 값이 end를 넘어서면 그 오버플로만큼은 begin으로부터 더해주는 방식으로 했습니다.

 

수정:

아 제가 깜빡하고 검증단계를 안거칠뻔 했군요...

당연히 만든 모든 기능에는 검증이 필요합니다... 제가 한 검증은요...

실행을 하면 콘솔창이 뜨는데 콘솔창 한줄에 딱 맞는 사이즈의 문자열을 하나 정합니다.

"1abcdefgh 2abcdefgh 3abcdefgh 4abcdefgh 5abcdefgh 6abcdefgh 7abcdefgh 8abcdefgh 9abcdefgh 0abcdefgh 1abcdefgh 2abcdefgh0"

저같은 경우에는 이렇게 널문자 포함 121글자입니다. 즉 제 콘솔 가로 크기는 120이었던 겁니다.

그리고 이걸 링버퍼에 넣었다가 꺼냈다가 반복하면서 꺼낸걸 모조리 출력하는겁니다. 당연히 뉴라인 캐릭터가 들어가면 절대 안됩니다.

time(nullptr)이 1200으로 나눴을때 0이되는 시간 즉 매 20,40,00분마다 2부터 시작한 시드값을 1씩 증가시켜가면서(왜냐면 만들때 1로세팅해놓고 만들어서...) 랜덤한 사이즈로 자르면서 그걸 인큐하고 랜덤한 사이즈를 디큐합니다. 그리고 디큐한걸 출력하는거죠.

일단 저같은 경우에는 임시 버퍼에다가 디큐한걸 다 저장해놓고 한줄이 채워지면 그 한줄이랑 원래 문자열이랑 strcmp를 통해서 다르다고 판단되면 그때 시드값이랑 잘못된 문장이 뭔지 파일로 빼놓고 터뜨리는 식으로 검증프로그램을 만들었구요....

 

다행히 지금 2시간정도 돌린거같은데... 아직 안터지고 잘 살아있습니다... 일단 오늘 잘때는 이거 켜놓고 자고 아침에 일어났을때 문제 없다면 그냥 이걸가지고 바로 서버에 적용시키려고해요... 뭐 어쩔수없습니다... 시간이 빡빡하기 때문에 검증을 오래할 수없습니다...

 

네... 뭐 이게 전부다네요 나머지는 거의 게터거나 3줄 이하의 코드기때문에... 굳이올리지 않아도 될 것 같았습니다.

하하하... 사실 이글은 설명하는 글은 아니구요... 제가 공부하려고 만든 글이기 때문에... 다소 친절하지 않을 수도있습니다만... 그점 양해바랍니다...

 

오늘도 긴글 읽어주신 여러분들께 감사드립니다.

매번 저혼자 공부하려고 올린 글인데... 다른분들께도 도움이 되는지 모르겠습니다

째뜬... 다음 글에는 select서버에 링버퍼를 적용시킨 내용으로 돌아오겠습니다. 그럼 안녕히계세요!

320x100