<RecvBuffer>
: 클라이언트에서 서버로 메세지를 보냄 -> 서버는 메세지(패킷)을 Receive -> 컨텐츠단 ReceiveBuffer의 Byte를 읽어드린다.
1. TCP의 특징으로 인해 패킷이 분할되어 전달 받을 수 있다.
: 이전 코드에서 Receive Buffer를 bytes[1024] 만큼 읽어드릴 수 있다고 간접적으로 이야기한 것
1) TCP의 특성때문에 100바이트를 보내도 80바이트 만큼 우선 보내고 나중에 나머지 20바이트가 올 수 있는 것이다.
2) 만약 80바이트만 왔다고 가정을 하면 Receive Buffer에 담고 있다가 나머지 20바이트가 왔을 때 조립하여 한번에 처리해야 한다.
- 아래의 OnRecv 함수처럼 Buffer에 들어온 바이트를 무조건 한번에 처리하면 안된다.
-> 완벽한 Byte가 왔다고 가정하고 처리하면 안된다.
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
3) Receive Buffer의 offset을 무조건 처음부터 하는 것이 아닌 받은 메세지의 Bytes에 따라 Offset이 바뀌게 된다.
2. RecvBuffer 클래스 생성
1) ServerCore 새항목 -> 추가 -> RecvBuffer 클래스 추가
2) 처음 클래스의 변수로 ArraySegment<byte> _buffer를 만들고 RecvBuffer 함수로 bufferSize 인자 만큼 _buffer를 초기화 한다.
public class RecvBuffer
{
ArraySegment<byte> _buffer;
public RecvBuffer(int bufferSize)
{
// bufferSize를 받아서 초기화 한다.
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
}
3) readPos와 WritePos 존재
a) WritePos: 현재 write해야하는 배열의 인덱스. 만약 처음에 5byte가 들어오면 WritePos가 5번째 인덱스가 되고 그 이후에 들어온 메세지에 대해서는 5번째 인덱스부터 Write해야한다.
b) ReadPos: 반대로 컨텐츠 코드에서 읽지말지를 결정해야한다. 5 바이트가 전체 패킷이어서 코드를 처리해야한다면 5바이트까지 읽고 난 후 ReadPos의 인덱스가 5라고 한다.
* WritePos와 ReadPos의 처음 인덱스는 둘다 0
c) 상황 1: TCP 통신을 하여 전체 8바이트 패킷 중 5바이트를 먼저 보내면 WritePos은 5가 된다. 아직 전체 패킷이 오지 않았기 때문에 ReadPos은 0이 된다. 이후 3바이트를 맏으면 WritePos가 8이 되고 컨텐츠 코드에서 전체 하나의 패킷을 읽기 때문에 ReadPos 또한 8이 된다.
- 이후 중간 중간에 Read&WritePos를 0으로 돌려 초기화 시킨다.
d) 상황 2: 패킷이 2바이트라고 가정. (컨텐츠 단에서 2바이트씩 읽어드림) 전체 패킷 4바이트를 전달받아야하는데 혼잡 제어로 인해 3바이트만 전달받으면 WritePos은 3이 된다. 컨텐츠 단에서는 2바이트를 읽을 수 있기 때문에 ReadPos이 2가 되고 유효범위가 1바이트가 된다. (3바이트 중 2바이트는 읽었으므로) 이후 Write가 될때까지 대기를 해야함
- 이런 식의 상황이 계속되면 ReadPos와 WritePos가 뒤로 밀려 Buffer의 공간이 뒤로 밀리기 때문에 해당 포지션의 데이터를 복사하여 처음 인덱스로 붙여넣기를 한다.
ex) [r] [w] [] [] [] [r] [w] [] [] []
<--------------| |
4) 필요한 프로퍼티 및 함수 작성
a) DataSize: 컨텐츠 단에서 읽어들일 수 있는 데이터의 크기 -> _writePos - _readPos
b) FreeSize: 현재 Buffer에 Write할 수 있는 데이터 크기 -> 전체 Buffer 사이즈에서 _writePos의 차를 구하면 됨
c) ReadSegment: 읽어들일 수 있는 Data의 ArraySegment
- _buffer.Offset: 배열 세그먼트로 구분된 범위의 첫 번째 요소 위치를 가져온다.
- _buffer.Array에서 _buffer.Offset + _readPos(처음 인덱스 0에서 _readPos만큼 더하면 _readPos) 에서 DataSize 만큼 가져오면 됨
d) WriteSegment: Write할 수 있는 Buffer의 ArraySegment
- _buffer.Array에서 _writePos에서 FreeSize만큼의 ArraySegment
e) Clean: _readPos와 _writePos를 초기화하는 작업
- dataSize가 0인 경우: _readPos와 _writePos의 위치가 같은 경우 -> 그냥 둘다 인덱스를 0으로 하면된다.
- 그 외의 경우: _readPos와 _writePos 사이의 데이터를 복사하여 시작 위치로 옮겨야햔다. => Array.Copy 이용
- public static void Copy (Array sourceArray, long sourceIndex, Array destinationArray, long destinationIndex, long length);
-> sourceArray에서 destinationArray의 destinationIndex로 sourceIndex에서 부터 length만큼 복사한다.
- 시작위치는 _readPos가 되어야하고 destinationArray는 배열의 첫번째 요소인 _buffer.Offset이 되야한다.
// 현재 유효 DataSize(컨텐츠 단에서 읽어드릴 수 있는)
public int DataSize { get { return _writePos - _readPos; } }
// Buffer의 남은 사이즈: FreeSize
public int FreeSize { get { return _buffer.Count - _writePos; } }
// 읽어들일 수 있는, 유효한 Data의 ArraySegment
public ArraySegment<byte> ReadSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
// Write할 수 있는 Buffer의 ArraySegment
public ArraySegment<byte> WriteSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
// _readPos과 _writePos을 처음 인덱스로 초기화하는 작업
public void Clean()
{
int dataSize = DataSize;
// dataSize가 0 -> _writePos과 _readPos가 둘다 같다는 것
if (dataSize == 0)
{
_readPos = _writePos = 0;
}
else
{
// 남은 Data가 있으면 시작위치로 데이터 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
5) 커서의 위치 이동 함수
a) OnRead: 컨텐츠 단에서 성공적으로 Read를 하면 _readPos를 옮겨줘야한다.
- numOfBytes만큼 읽었다고 했을 때 _readPos += numOfBytes가 필요
- numOfBytes의 크기가 DataSize보다 현재 읽을 수 있는 데이터 크기보다 큰 경우는 문제가 있다.
b) OnWrite: 서버에서 buffer로 Write를 할 경우 _writePos를 numOfBytes만큼 옮겨줘야한다.
- FreeSize보다 많이 Write할 수 없음
- 그 외에는 numOfBytes만큼 _writePos를 증가
public bool OnRead(int numOfBytes)
{
if (numOfBytes > DataSize)
return false;
_readPos += numOfBytes;
return true;
}
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize)
return false;
_writePos += numOfBytes;
return true;
}
* RecvBuffer.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
public class RecvBuffer
{
// [] [] [] [] [] [] [] [] [] []
ArraySegment<byte> _buffer;
int _readPos; // Read를 하는 커서
int _writePos; // Write를 하는 커서
public RecvBuffer(int bufferSize)
{
// bufferSize를 받아서 초기화 한다.
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
// 현재 유효 DataSize(컨텐츠 단에서 읽어드릴 수 있는)
public int DataSize { get { return _writePos - _readPos; } }
// Buffer의 남은 사이즈: FreeSize
public int FreeSize { get { return _buffer.Count - _writePos; } }
// 읽어들일 수 있는, 유효한 Data의 ArraySegment
public ArraySegment<byte> ReadSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
// Write할 수 있는 Buffer의 ArraySegment
public ArraySegment<byte> WriteSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
// _readPos과 _writePos을 처음 인덱스로 초기화하는 작업
public void Clean()
{
int dataSize = DataSize;
// dataSize가 0 -> _writePos과 _readPos가 둘다 같다는 것
if (dataSize == 0)
{
_readPos = _writePos = 0;
}
else
{
// 남은 Data가 있으면 시작위치로 데이터 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
/* 커서의 위치를 이동시키는 함수 */
// 컨텐츠 코드가 성공적으로 Read를 하면 OnRead 함수를 실행시켜 _readPos의 위치를 바꾼다.
public bool OnRead(int numOfBytes)
{
if (numOfBytes > DataSize)
return false;
_readPos += numOfBytes;
return true;
}
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize)
return false;
_writePos += numOfBytes;
return true;
}
}
}
3. Session.cs에 RecvBuffer 적용
1) Session 클래스의 멤버 변수로 RecvBuffer 객체 선언한다.
-> Start 함수에서 _recvArgs.SetBuffer로 버퍼를 설정하지 않아도 되기때문에 해당 코드 삭제한다.
public abstract class Session
{
Socket _socket;
int _disconnected = 0;
// Receive Buffer 객체 선언
RecvBuffer _recvBuffer = new RecvBuffer(1024);
... 중간 생략
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
2) RegisterRecv 부분에서 RegisterBuffer 설정이 이뤄져야한다.
- RgisterSend에서 Queue에서 데이터를 꺼내어 BufferList를 설정해준 것과 같이 RegisterBuffer 또한 시작할때 설정하는게 아니라 RegisetRecv 에서 설정해줘야한다.
a) _recvBuffer.WriteSegment로 Write 가능한 segment를 가져온 후 해당 segment로 _recvArgs의 버퍼를 설정
b) 이전에 혹시 모르니 Clean을 통해 커서를 초기화한다.
void RegisterRecv()
{
_recvBuffer.Clean();
// _recvArgs의 Buffer를 설정해줘야함.
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // segment.Count가 FreeSize
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음
OnRecvCompleted(null, _recvArgs);
}
3) OnRecvCompleted 함수
a) Recv를 완전히 받았기 때문에 _recvBuffer의 Write 커서를 Receive 받은 만큼 옮겨줘야한다.
- _recvBuffer.OnWrite 함수를 이용하여 _recvArgs.BytesTransferred 만큼 커서를 옮겨준다. 만일 false가 반환된다면 버그 발생: Disconnect 후 return
b) 컨텐츠 단에 데이터를 넘겨주고 얼마나 처리했는지를 받아야한다. -> Receive Handler와 연결
- 기존에 OnRecv 함수를 통해서 컨텐츠 단에서 해야할 기능을 정의: 원래는 void 형이었지만 return값을 int로 하여 처리된 Bytes의 수를 return하도록 한다.
- 일반적으로 받은 buffer의 size만큼 반환하기 때문에 return buffer.Count로 반환하도록 한다.
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Write 커서 이동 -> false가 있다는 것은 버그
if(_recvBuffer.OnWrite(_recvArgs.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다. -> Receive Handler 연결
int processLen = OnRecv(_recvBuffer.ReadSegment);
if(processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
// Read 커서를 이동
if(_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
* Session의 OnRecv
public abstract int OnRecv(ArraySegment<byte> buffer);
* Server와 DummyClient에서의 OnRecv
public override int OnRecv(ArraySegment<byte> buffer)
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Client] {recvData}");
return buffer.Count;
}
public override int OnRecv(ArraySegment<byte> buffer)
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Server] {recvData}");
return buffer.Count;
}
* ServerCore 라이브러리의 Session.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
public abstract class Session
{
Socket _socket;
int _disconnected = 0;
// Receive Buffer 객체 선언
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
// 인터페이스
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
// Disconnected Handler 연결
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크통신
// 비동기는 두단계로 나눠짐
// Send
void RegisterSend()
{
while(_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendingList;
//
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// BufferList 와 _pendingList 초기화
_sendArgs.BufferList = null;
_pendingList.Clear();
// Send Handler 연결
OnSend(_sendArgs.BytesTransferred);
if(_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
// Receive
void RegisterRecv()
{
_recvBuffer.Clean();
// _recvArgs의 Buffer를 설정해줘야함.
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // segment.Count가 FreeSize
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Write 커서 이동 -> false가 있다는 것은 버그
if(_recvBuffer.OnWrite(_recvArgs.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다. -> Receive Handler 연결
int processLen = OnRecv(_recvBuffer.ReadSegment);
if(processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
// Read 커서를 이동
if(_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
#endregion
}
}
<SendBuffer>
1. RecvBuffer와 SendBuffer의 차이점
1) Send라는 작업은 Server의 Program.cs에서 OnConnected에서 sendBuff를 반든 후 Send 함수로 해당 sendBuff를 보내고 Send 함수에서 Queue에 모으는 작업을 진행 했다.
2) RecvBuffer는 Session 마다 자신의 고유의 RecvBuffer를 가지고 있었다. : 모든 Session, 클라이언트가 보내는 정보가 각기 다르다. 개인 공간이 있어 사용자마다 보낸 정보를 추적.
- Server의 Program.cs에서 Func<Session> 을 통해 Session 객체를 생성하는 것을 return한다. Listener.cs에서 클라이언트 Socket이 Connect를 요청한 후 완전히 Accept 하면 OnAcceptComplete 함수를 호출
-> 여기서 Func<Session>의 함수를 Invoke 시켜서 Session 객체를 생성한다.
-> 즉, 클라이언트 마다 각자의 Session을 갖게 되고 Session 마다 RecvBuffer가 존재하기 때문에 개개인의 RecvBuffer 공간을 가지고 있는다.
3) SendBuff는 Session에 안에 있는 것이 아니라 컨텐츠 단에 빠져있다.
a) OnConnected를 한 후 SendBuff를 만들어 Send 함수로 보내고 있다.
b) 문자열로 보내는 경우는 없고 패킷 단위로 보내게 된다.
- 예를 들어 다음 아래와 같이 Knight 클래스에 대한 정보를 보낸다고 하면 아래와 같이 byte 배열 buffer를 만든 후, Array.Copy 함수를 통해 sendBuff에 복사 하여 Send해야한다.
- 왜 SendBuffer를 Session 안에다가 넣을 수 없나? 인터페이스 정보들을 Send 함수로 받은 다음에 Send 함수에서 위에서 했던 Array.Copy 작업을 하면 된다.
-> 문제: 성능적 이슈가 존재: 하나의 존안에 100명의 유저가 서로 이동을 하기 때문에 이동 정보를 모든 100명에게 알려줘야함
100 * 100 = 만 개의 패킷이 전송되어야한다. 만일 위와 같이 Copy를 하게 된다면 만번의 Copy 작업을 해야함. 성능이 떨어진다.
- 한번만 sendBuff를 만든 후에 보내는 방식이 더욱 효율적 -> SendBuffer는 Session 내부가 아닌 밖에서 만들어줘야한다.
class Knight
{
public int hp;
public int attack;
}
// 컨텐츠 단
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// Knight 객체 선언 -> 보내야하는 상황
Knight knight = new Knight() { hp = 100, attack = 10 };
// SendBuff만 남겨둠
//byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
byte[] sendBuff = new byte[1024];
byte[] buffer = BitConverter.GetBytes(knight.hp);
byte[] buffer2 = BitConverter.GetBytes(knight.attack);
Array.Copy(buffer, 0, sendBuff, 0, buffer.Length);
Array.Copy(buffer2, 0, sendBuff, buffer.Length, buffer2.Length);
Send(sendBuff);
Thread.Sleep(1000);
Disconnect();
}
4) SendBuffer의 사이즈를 어떻게 해야하는 가?
a) 보내고자 하는 정보의 사이즈를 계산하여 SendBuffer를 만드는 것이 효율적. 하지만, 보내고자 하는 메세지의 크기가 가변일 수 있다.
- 아래와 같이 Knight 클래스는 가변으로 크기가 고정되지않고 변한다.
class Knight
{
public int hp;
public int attack;
// 가변 변수
public string name;
public List<int> skills = new List<int>();
}
b) 무조건 Buffer의 크기를 크게 잡기보다는 ReceiveBuffer에서 Write 커서를 옮겨서 write했던 것처럼 SendBuffer도 아주 크게 잡아놓은 다음에 잘라서 사용하면 효율
2. SendBuffer.cs
: ServerCore 우클릭 -> 추가 -> 새항목: SendBuffer.cs 추가
1) 변수와 프로퍼티
a) _buffer: SendBuffer
b) _usedSize: 현재 사용된 버퍼의 크기, 사용가능한 _buffer의 인덱스
- 만약 _buffer에 10 바이트 중 3 바이트만 사용했다면 => [] [] [] [_usedSize] [] [] [] [] [] []
c) FreeSize: 현재 _buffer에서 사용 가능한 사이즈 -> 전체 _buffer의 길이에서 사용한 만큼의 차를 구하면 된다.
public class SendBuffer
{
// [] [] [] [] [] [] [] [] [] []
byte[] _buffer;
int _usedSize = 0; // 현재 사용된 버퍼의 크기: 사용가능한 버퍼의 인덱스
// _buffer의 여유로운 크기
public int FreeSize { get { return _buffer.Length - _usedSize; } }
}
2) 함수
a) 생성자 함수: SendBuffer를 만든다.
// SendBuffer를 만든다.
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
b) Open 함수: reserveSize 만큼 _buffer를 예약하겠다는 의미로 _usedSize에서 reserveSize만큼을 반환한다.
- 예상하는 최대 크기가 reserveSize이다.
* 이전과 달리 ArraySegment가 null로 변환이 안되는 것같다. 강의해서는 그냥 null을 해도 에러가 뜨지않았는데 내 버전에서는 null로 하니 에러가 뜬다 -> new ArraySegment<byte>()를 반환하도록 함.
public ArraySegment<byte> Open(int reserveSize)
{
if (reserveSize > FreeSize)
return new ArraySegment<byte>();
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
}
c) Close 함수: usedSize 만큼 _buffer를 사용하였고 사용한 만큼의 ArraySegment를 반환한다. -> _usedSize 또한 사용한 만큼 더해준다.
// 사용한 사이즈 usedSize이고 사용한 ArraySegment 반환
public ArraySegment<byte> Close(int usedSize)
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
c) SendBuffer는 RecvBuffer와 다르게 커서를 처음 위치로 옮겨서 초기화하기 어렵다.
- 왜냐면 Send하는 것을 하나의 클라이언트에만 하는 것이 아닌 여러개의 클라이언트에 하기 때문에 내 Session에서는 해당 부분의 Buffer를 보냈다고 하더라도 다른 Session은 아직 못보냈을 수도 있기 때문
-> SendBuffer를 일회용으로만 사용
3) SendBufferHelper 클래스 : SendBuffer를 사용하기 쉽게 하는 클래스를 만든다.
a) SendBuffer를 한번만 만들어준 다음 고갈될때까지 재사용할 것 -> 전역으로 만들면 편리 but, 멀티 스레드 환경에서 전역 변수는 Race Condition이 발생하기 때문에 ThreadLocal로 만든다.
b) ChunkSize: sendBuffer의 사이즈
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
// ChunkSize를 설정한다.
public static int ChunkSize { get; set; } = 4096 * 100;
}
c) Close 함수는 현재 Buffer인 CurrentBuffer의 Close와 맵핑해준다.
public static ArraySegment<byte> Close(int usedSize)
{
// 현재 사용하고 있는 CurrentBuffer에서 Close를 맵핑해서 return 하면된다.
return CurrentBuffer.Value.Close(usedSize);
}
d) Open 함수에서는 Thread의 CurrentBuffer를 관리해야 한다.
- CurrentBuffer가 null 이면 처음 SendBuffer를 사용하는 것이므로 SendBuffer객체를 만들어준다.
- 만약 reserveSize가 현재 남은 buffer의 사이즈, FreeSize보다 크다면 다시 새롭게 SendBuffer를 만들어 준다.
- 위의 두개가 아니라면 현재 남아있는 버퍼의 공간이 있다는 뜻이므로 CurrentBuffer.Value.Open(reserveSize)를 해준다.
public static ArraySegment<byte> Open(int reserveSize)
{
/* Thread의 CurrentBuffer를 관리해야한다. */
// CurrentBuffer가 null이면 처음 SendBuffer 사용하는 것 -> ChunkSize 만큼 만들어준다.
if (CurrentBuffer.Value == null)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
// 현재 버퍼의 공간이 남아 있다는 것 -> SendBuffer의 Size를 Open
return CurrentBuffer.Value.Open(reserveSize);
}
** 이때, Open 함수와 Close 함수 둘다 static으로 선언하여 객체를 생성하지 않고 클래스 단위로 생성한다. -> 일회성으로 사용될 때 static 메서드 사용
- static 속성, 필드는 프로그램 실행 후 해당 클래스가 처음 사용될때 한번 초기화되어 계속 동일한 메모리를 사용하게 된다.
- static 클래스: 모든 클래스 멤버가 static 멤버로 되어 있으며, 클래스 명 앞에 static 키워드 사용 -> static 생성자 가질 수 있다.
* SendBuffer.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
// ChunkSize를 설정한다.
public static int ChunkSize { get; set; } = 4096 * 100;
public static ArraySegment<byte> Open(int reserveSize)
{
/* Thread의 CurrentBuffer를 관리해야한다. */
// CurrentBuffer가 null이면 처음 SendBuffer 사용하는 것 -> ChunkSize 만큼 만들어준다.
if (CurrentBuffer.Value == null)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
// 현재 버퍼의 공간이 남아 있다는 것 -> SendBuffer의 Size를 Open
return CurrentBuffer.Value.Open(reserveSize);
}
public static ArraySegment<byte> Close(int usedSize)
{
// 현재 사용하고 있는 CurrentBuffer에서 Close를 맵핑해서 return 하면된다.
return CurrentBuffer.Value.Close(usedSize);
}
}
public class SendBuffer
{
// [] [] [] [] [] [] [] [] [] []
byte[] _buffer;
int _usedSize = 0; // 현재 사용된 버퍼의 크기: 사용가능한 버퍼의 인덱스
// _buffer의 여유로운 크기
public int FreeSize { get { return _buffer.Length - _usedSize; } }
// SendBuffer를 만든다.
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
// 사용되어야하는 버퍼의 사이즈 reserveSize: 예상하는 최대 크기를 말한다.
public ArraySegment<byte> Open(int reserveSize)
{
if (reserveSize > FreeSize)
return new ArraySegment<byte>();
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
}
// 사용한 사이즈 usedSize이고 사용한 ArraySegment 반환
public ArraySegment<byte> Close(int usedSize)
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
}
}
3. Server의 컨텐츠단인 Program.cs에 어떻게 적용?
a) SendBufferHelper.Open을 통해 현재 최대로 사용될 reserveSize를 인자로 보내고 해당 사이즈의 버퍼의 ArraySegment를 반환
b) return받은 openSegment에 보내져야할 정보(buffer)를 Arry.Copy로 복사한다.
- 이때, 복사되어야할 openSegment의 인덱스는 0이 아닌 openSegment의 Offset이다.
c) SendBuffer를 다 썼으면 SendBufferHelper.Close: 사용한 바이트 사이즈를 인자로 넘기고 사용한 ArraySegment를 반환한다. -> 그게 sendBuff
- Send 함수의 인자로 ArraySegment<byte>를 넘길 수 있도록 Session에서 Send 함수의 인자를 byte[] 에서 ArraySegment<byte>로 바꿔준다.
* Server의 Program.cs 전체 코드
using ServerCore; // ServerCore의 라이브러리
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
class Knight
{
public int hp;
public int attack;
}
// 컨텐츠 단
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// Knight 객체 선언 -> 보내야하는 상황
Knight knight = new Knight() { hp = 100, attack = 10 };
// SendBuffer Open
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, buffer2.Length);
// 다 썼으니 닫아야한다.
ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
Send(sendBuff);
Thread.Sleep(1000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
// 얼마만큼의 데이터를 처리했는지에 대한 정보를 int값으로 return
public override int OnRecv(ArraySegment<byte> buffer)
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Client] {recvData}");
return buffer.Count;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening....");
while (true)
{
}
}
}
}
4. 실행
이전과 같이 결과를 내는 것을 알 수 있다.
-> 멀티 스레드 환경에서 괜찮은가?
1) Send 함수를 호출하면 멀티스레드 환경을 고려하여 lock으로 감쌌고 RegisterSend 함수를 호출: 멀티스레드를 고려하여 코딩
2) SendBuffer 부분은 별도의 멀티 스레드 처리가 안되어 있음
- TLS(Thread Local Storage)로 코딩했기 때문에 다른 스레드에서 내 스레드의 Open & Close 함수에 접근할 수 없다.
- 다수의 스레드가 SendBuffer에 접근한다고 해도 Session에서의 Send는 Write하는 것이 아닌 Read만을 하는 것이기 때문에 문제가 없다.
- _sendQueue에서 Dequeue를 하고 SendAsync를 하는 작업은 처음 sendBuff를 보낸 스레드가 아닌 다른 스레드가 할 수 도 있음.: Read만 하기 때문에 문제가 없다.
'Development > 게임 서버' 카테고리의 다른 글
[Part 4 게임 서버] 네트워크 프로그래밍 7 (PacketSession) (0) | 2022.01.21 |
---|---|
[Part 4 게임 서버] 네트워크 프로그래밍 5(Connector, TCP vs UDP) (0) | 2022.01.19 |
[Part 4 게임 서버 ] 네트워크 프로그래밍 4 (Session #3, Session #4) (0) | 2022.01.18 |
[Part 4 게임 서버] 네트워크 프로그래밍 3(Session #1, Session #2) (0) | 2022.01.16 |
[Part 4 게임 서버] 네트워크 프로그래밍 2 (Listener) (0) | 2022.01.16 |