Connector
Listener와 역할이 반대인 Connector를 만들어보자. 물론 공식적인 명칭은 아니다.
우리가 이전에 DummyClient에서 Connect하는 부분을 블로킹 함수로 구현하고 있었는데 게임서버에서는 블로킹 함수를 사용하는 것이 올바르지 않기 때문에 이를 바꿔보자.
그런데 Connector가 왜 필요할지 부터 생각해보자. 서버가 엄청 거대한 MMO 일 경우 NPC의 요청만, AI의 요청만, Player의 요청만 받는 각각의 부분서버가 있다. 그 서버끼리 통신을 하기 위해선 서버에서 다른 서버로 Connect 요청을 해야하기 때문에 단순히 Client만이 Connect를 요청하는 것이 아니다. 그러니 공용부분으로 같이 사용하면 좋기 때문에 만드는 것이다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
public class Connector
{
Func<Session> _sessionFactory;
public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory)
{
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory = sessionFactory;
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += OnConnectedCompleted;
args.RemoteEndPoint = endPoint;
args.UserToken = socket;
RegisterConnect(args);
}
void RegisterConnect(SocketAsyncEventArgs args)
{
Socket socket = args.UserToken as Socket;
if (socket == null) { return; }
bool pending = socket.ConnectAsync(args);
if (pending == false)
OnConnectedCompleted(null, args);
}
void OnConnectedCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke();
session.Start(args.ConnectSocket);
session.OnConnected(args.RemoteEndPoint);
}
else
{
Console.WriteLine($"OnConnectCompleted Fail : {args.SocketError}");
}
}
}
}
using ServerCore;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient
{
class Program
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
for (int i = 0; i < 10; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello, World!");
Send(sendBuff);
Thread.Sleep(100);
}
}
public override void OnDisConnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisConnected : {endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"From Server {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes : {numOfBytes}");
}
}
static void Main(string[] args)
{
// DNS(Domain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
}
TCP vs UDP
우리는 이전까지 단순히 특정 문자열을 주고 받고 하는 코드만 작성해 봤다. 이제 예를들어 플레이어가 움직이는 패킷을 보낸다고 해보자. 그러면 우리가 규약을 정해야한다.
이동 패킷 (3, 2) 좌표로 이동하고 싶다! 라고 하면
예로 패킷의 내용은 15 3 2 등의 데이터를 서버로 보내면 15번의 규약은 정수 두 개를 받아서 그 좌표로 이동이이기에 3,2 좌표로 이동 해주어야 한다.
우리가 사용하고 있는 TCP는 send를 100바이트를 보낸다고 바로 100바이트를 받는다는 보장이 없다. 그렇기에 15 3까지 만 왔다고 바로 폐기할 수 없고 정해진 규칙에 따라 처리해야한다.
그렇기에 우리가 먼저 TCP에 대한 이론을 알아보자.
TCP는 연결형 서비스이다.
1. 연결을 위해 할당되는 논리적인 경로가 있다.
2. 전송 순서가 보장된다.
UDP는 비연결형 서비스이다.
1. 연결이라는 개념이 없다.
2. 전송 순서가 보장되지 않는다.
TCP의 2,3,4번 특히 4번이 정말 중요하다.
RecvBuffer
그전에 우리가 _recvArgs.SetBuffer(new byte[1024], 0, 1024); 코드를 통해 리시브 버퍼의 크기를 1024로 설정해 주었다. 문제는 TCP 특성상 모든 데이터가 한번에 온다는 보장이 없다. 그렇기 때문에 일부 데이터만 왔다면 유저레벨로 바로 복사하는 것이 아닌 RecvBuffer에 잠시 보관해 두었다가 나머지 데이터가 도착하면 이전것과 같이 유저레벨에 보내야할 것이다. 그것을 수정해 보겠다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
public class RecvBuffer
{
ArraySegment<byte> _buffer;
int _readPos;
int _writePos;
public RecvBuffer(int bufferSize)
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
public int DataSize { get { return _writePos - _readPos; } }
public int FreeSize { get { return _buffer.Count - _writePos; } }
public ArraySegment<byte> ReadSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
public ArraySegment<byte> WriteSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
public void Clean()
{
int dataSize = DataSize;
if(dataSize == 0)
{
// 남은 데이터가 없으면 복사하지 않고 커서 위치만 리셋
_writePos = 0;
_readPos = 0;
}
else
{
// 남은 찌끄레기가 있으면 시작 위치로 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset , dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
public bool OnRead(int numOfByte)
{
if (numOfByte > DataSize)
return false;
_readPos += numOfByte;
return true;
}
public bool OnWrite(int numOfByte)
{
if (numOfByte > FreeSize)
return false;
_writePos += numOfByte;
return true;
}
}
}
SendBuffer
recvBuffer를 만들었으니 대칭적인 SendBuffer를 만들어보자. recvBuffer보다 더 어렵다. sendBuffer도 크기를 엄청 크게 잡고 잘라서 사용하는 형식으로 이용한다. sendBuffer를 recvBuffer처럼 Session이 들고 있으면 좋겠지만 그렇게 할 경우 send가 빈번히 일어나는 mmorpg경우에는 연산이 매우 많아져서(복사가 많음) 손해를 볼 가능성이 높다. 그러니 외부에서 관리하는 것이 제일 좋다.
실제로 2000년대 초반 네트워크 책을 읽어보면 sendBuffer도 Session당 하나씩 들고 있게끔 하는 방식이 더 많았다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(()=> { return null; });
public static int ChunkSize { get; set; } = 4096 * 100;
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);
}
}
public class SendBuffer
{
byte[] _buffer;
int _usedSize = 0;
public SendBuffer(int ChunkSize)
{
_buffer = new byte[ChunkSize];
}
public int FreeSize { get { return _buffer.Length - _usedSize; } }
// 현재 상태에서 얼마만큼의 사이즈 만큼 사용하겠다 선언, 예약
// 만약 최대 사이즈를 넘기면 실패
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;
}
}
}
PacketSession
패킷을 보내기 위한 사전 준비로 Session을 수정해보자.
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
int processLen = 0;
while (true)
{
// 최소한 헤더는 파싱할 수 있는지 확인
if (buffer.Count < HeaderSize)
break;
// 패킷이 완전체로 도착햇는지 확인
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize)
break;
// 여기까지 왔으면 패킷 조립 가능
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
processLen += dataSize;
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return processLen;
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
패킷을 처리할 수 있는 session을 만들어줬다. 서버에서 데이터를 보낼때 packet 클래스를 하나 만들고 그걸 보내면 packetSession에서 2바이트씩 짤라서 조립을 하고 데이터를 처리한다.
using System.Net;
using System.Text;
using ServerCore;
namespace Server
{
public class Packet
{
public ushort size;
public ushort packetId;
}
class GameSession : PacketSession
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Packet packet = new Packet() { size = 4, packetId = 2 };
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
byte[] buffer = BitConverter.GetBytes(packet.size);
byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
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(packet.size);
Send(sendBuff);
Thread.Sleep(1000);
Disconnect();
}
public override void OnDisConnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisConnected : {endPoint}");
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
Console.WriteLine($"RecvPacketId : {id}, Size = {size}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes : {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS(Domain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
'Unity > 온라인 RPG' 카테고리의 다른 글
[게임 서버] 패킷 직렬화 - Packet Generator (0) | 2024.01.30 |
---|---|
[게임 서버] 패킷 직렬화 - Serialization (1) | 2024.01.24 |
[게임 서버] 네트워크 프로그래밍 - Listener와 Session (0) | 2024.01.19 |
[게임 서버] 네트워크 프로그래밍 - 네트워크 기초 및 소켓 프로그래밍 (0) | 2024.01.17 |
[게임 서버] 멀티쓰레드 프로그래밍 - Context Switching, Event등 Lock의 다양한 기법 (1) | 2024.01.11 |