지난 게시글에서는 RecvBuffer를 만들었다. 이번에는 SendBuffer를 만들어 따로 빼내보겠다. SendBuffer는 크게 두 클래스로 나뉜다. SendBufferHelper와 SendBuffer다.
SendBufferHelper 먼저 살펴보자.
// ThreadLocal == 전역은 전역 변순데 내 스레드에서만 사용할 수 있는 전역변수
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
// 그냥 사이즈가 아니라 되게 큰 뭉탱이 Size. 여기서 필요한 만큼 뽑아다 쓸 예정.
public static int ChunkSize { get; set; } = 4096 * 100;
멀티 스레드 환경에서도 전역 변수가 필요한 일이 많다. 일반적으로 전역 변수를 선언하면 많은 스레드에서 동시에 같은 변수를 향해 손을 뻗게 된다. 누가 먼저 일을 마치느냐에 따라 변수에 값이 뒤죽박죽으로 변할 수밖에 없다. 이때 내 스레드에서만 사용 가능한 전역 변수를 만드는 방법이 바로 ThreadLocal을 활용하는 것이다.
SendBuffer는 크기를 정하기가 애매하다. 그때그때 전송하는 데이터에 따라 8byte가 필요할 수도, 100byte가 필요할 수도 있다. 모든 데이터를 아우르기 위해 무작정 큰 byte를 SendBuffer의 크기로 지정하기에도 낭비가 심하다.
그래서 우리는 ChunkSize라는 이름으로 매우매우 큰 공간을 만들 것이다. SendBuffer는 그 매우 큰 공간에서 필요한 만큼의 공간을 빼내와서 사용하면 된다.
public static ArraySegment<byte> Open(int reserveSize)
{
if (CurrentBuffer.Value == null) // 한 번도 사용 안 함
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize) // 있기는 한데 요구한 크기보다 칸이 작음
CurrentBuffer.Value = new SendBuffer(ChunkSize);
return CurrentBuffer.Value.Open(reserveSize); // 사용 가능.
}
public static ArraySegment<byte> Close(int usedSize)
{
return CurrentBuffer.Value.Close(usedSize);
}
Open과 Close는 버퍼의 문을 열고 닫을 때 사용한다. 아직 지정하지 않은 변수명이 나오지만 이름을 보고 사용을 짐작하리라 생각한다.
1️⃣ 버퍼를 한 번도 사용하지 않았을 경우에는 새로운 버퍼를 만들어 데이터를 담아준다.
2️⃣ 이미 만들어진 버퍼가 있지만 우리가 요구한 크기보다 남은 버퍼의 남은 여유 공간이 작을 경우 새로운 버퍼를 만들어 데이터를 담아준다.
➡ 일이 끝나면 요구한 크기에 따라 버퍼가 사용 가능하다는 것을 명시한다.
버퍼를 닫을 때는 사용한 양을 인자로 넘겨주고 버퍼를 닫는다.
이제 SendBuffer 클래스를 살펴보자.
byte[] _buffer;
int _usedSize = 0; // 얼마나 사용했나 (== RecvBuffer에서의 write)
public int FreeSize { get { return _buffer.Length - _usedSize; } }
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
usedSize는 지금까지 버퍼에서 사용한 양을 묻는다. FreeSize는 버퍼의 총 크기에서 사용한 양을 뺀, 자유롭게 이용 가능한 양이다. SendBuffer의 크기는 SendBufferHandler 클래스에서 지정한 매우 큰 양인 chunkSize가 맡는다.
public ArraySegment<byte> Open(int reserveSize) // 필요한 최대치 넣어서 예약하기
{
if (reserveSize > FreeSize)
return null;
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
}
public ArraySegment<byte> Close(int usedSize) // 실제로 다 쓴 양
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
SendBuffer도 열고 닫는 함수가 존재한다. 데이터를 보낼 때 인자로 넣은 요구한 크기가 여유 공간보다 클 경우, null을 리턴한다. 그 외의 경우에는 버퍼를 쪼개 지금까지 사용한 양의 칸부터 시작해서 요구한 양의 데이터를 버퍼에 넣어준다.
문을 닫을 때는 사용한 양을 인자로 받는다. 쪼개진 버퍼에 사용한 양을 정리해 넣고 그 정리된 배열(segment)를 반환한다.
📛 SendBuffer는 RecvBuffer와 다르게 Clean()의 개념이 없다. RecvBuffer는 일정 시간이 지나 버퍼에 받은 데이터가 뒤로 밀리면 다시 앞으로 땡겨서 여유 공간을 만들어도 문제가 없었다.
SendBuffer는 다르다. 보통 데이터를 전송할 때는 하나의 클라이언트에게만 전송하는 것이 아니라 굉장히 많은 클라이언트에게 내용을 전송한다. 이때 일부 클라이언트에게는 전송한 데이터가 다른 클라이언트에게는 전송되지 않았을 경우가 있다. 이를 무시하고 일부 클라이언트에게 전송됐다는 이유로 버퍼에 있던 데이터를 밀고 새로운 데이터를 그 자리에 넣어버리면 데이터를 제대로 받지 못한 클라이언트가 생긴다.
이때문에 SendBuffer는 재사용하지 않고 일회용으로 사용하는 것이 안전하다.
SendBuffer의 사용 예시를 살펴보자. 우리는 컨텐츠 단인 Session에서 사용했다.
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
byte[] buffer = BitConverter.GetBytes(knight.hp);
byte[] buffer2 = BitConverter.GetBytes(knight.attack);
Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
Send(sendBuff);
이때 BitConverter라는 게 등장한다. 서버가 보내는 int값을 byte값으로 변환시켜주는 함수다. 이를 이용해서 buffer에 변환된 값을 넣어준다. buffer와 buffer2에 값을 넣어주고, SendBufferHelper.Close를 이용해 사용한 값을 넣고 문을 닫아준다.
📛SendBuffer를 그냥 Session에 넣으면 안될까, 라고 생각할 수도 있지만 그러면 안된다.
만약 게임에서 같은 공간에 100명의 플레이어가 있다고 생각해보자. 모든 플레이어는 각기 다른 움직임으로 자리를 이동하고 있을 것이다. 그러면 모든 유저에게 서로의 움직임을 패킷으로 전송해야 한다. 10000개의 패킷이다. Session 내부에 SendBuffer가 있다면 모든 유저가 움직이는 순간순간 10000개의 패킷을 복사해야 한다.
외부에 SendBuffer가 있다면 루프를 사용해서 Session.Send만 해주면 불필요한 복사 작업 없이 패킷을 전송할 수 있다.
'공부 > 게임 서버' 카테고리의 다른 글
[게임 서버/암호학] 대칭키와 비대칭키 (0) | 2022.09.30 |
---|---|
[게임 서버] await 사용하기 (0) | 2022.07.11 |
[게임 서버] SetBuffer를 RecvBuffer, SendBuffer로 따로 빼내기 #1 (0) | 2022.06.20 |
[게임 서버] Listener, 네트워크 연결을 기다려보자 (0) | 2022.06.20 |
[게임 서버] Listener의 반대 개념, Connector에 대해서 (0) | 2022.06.20 |