
[ ์น์ 3. ํจํท ์ง๋ ฌํ ]
# Serialization #1
- ์ง๋ ฌํ๋ ๊ฐ์ฒด๋ฅผ ์ ์ฅ ๊ฐ๋ฅํ ์ํ ๋๋ ์ ์ก ๊ฐ๋ฅํ ์ํ๋ก ๋ณํํ๋ ๊ฒ์ ๋ปํ๋ค.
~> ์ฆ, ํจํท ์ง๋ ฌํ๋ ๋ฉ๋ชจ๋ฆฌ ์์ ์กด์ฌํ๋ ๋ฐ์ดํฐ๋ฅผ ํจํท์ ์ฐจ๊ณก์ฐจ๊ณก ์์ ๋ค ์ด๋ฅผ ํ๋์ ๋ฐ์ดํธ ๋ฐฐ์ด๋ก ๋ง๋๋ ๊ฒ์ด๋ค.
- ์ญ์ง๋ ฌํ๋ ํน์ ํฌ๋งท ์ํ์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๊ฐ์ฒด๋ก ๋ณํํ๋ ๊ฒ์ ๋ปํ๋ค.
- Session์ ์ถํ์ ๋ค์ํ๊ฒ ์กด์ฌํ ์ ์๊ธฐ ๋๋ฌธ์ Session์ ์ด๋ฆ์ ์ ํํ๊ฒ ์ง์ด์ฃผ๋ ๊ฒ์ด ์ค์ํ๋ค.
(์๋ฅผ ๋ค์ด ๋ถ์ฐ์๋ฒ์ธ ๊ฒฝ์ฐ ๊ฐ ๋ค๋ฅธ ๋ถ๋ถ์ ๊ด๋ฆฌํ๋ ์๋ฒ์ ๋๋ฆฌ์ ์ญํ ์ ํ๋ Session์ด ์ฌ๋ฌ๊ฐ ์กด์ฌํ๋ค.)
- ์ฐ์ Serialization ์ ํ๋ฆ๋ง ์ดํดํ ๋ค ์ถํ์ ์๋ํ ํ ์์ ์ด๋ค.
DummyClient์ ServerSession Class ์์ฑ ํ DummyClient์ Program Class ์ ๋ด์ฉ ๋ถ๋ฆฌ
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
class Packet
{
public ushort size; // ushort๋ 2Byte
public ushort packetId; // ushort๋ 2Byte
}
class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ
{
public long playerId;
}
class PlayerInfoOk : Packet // Server์์ Client๋ก ์์ฒญ์ ๋ํ ๋ต๋ณ์ ์ ๋ฌํ๋ ๊ฒ
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
for (int i = 0; i < 5; i++)
{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
// ์๋ ๋ถ๋ถ ์ถ๊ฐ (2๋ฒ์ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ์ผ ํ๋ ๊ฒ์ TryWriteBytes๋ฅผ ํตํด 1๋ฒ์ ๋จ๊ณ๋ฅผ ๊ฑฐ์น๋๋ก ์์ )
bool success = true;
ushort count = 0; // ์ง๊ธ๊น์ง ๋ช Byte๋ฅผ Buffer์ ๋ฐ์ด ๋ฃ์๋๊ฐ?
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size๋ ๋ชจ๋ ์์
์ด ๋๋ ๋ค ์ด๊ธฐํ
// ์๋ ๋ถ๋ถ ์ญ์
//byte[] size = BitConverter.GetBytes(packet.size);
//byte[] packetId = BitConverter.GetBytes(packet.packetId);
//byte[] playerId = BitConverter.GetBytes(packet.playerId);
//Array.Copy(size, 0, openSegment.Array, openSegment.Offset + count, 2);
//count += 2;
//Array.Copy(packetId, 0, openSegment.Array, openSegment.Offset + count, 2);
//count += 2;
//Array.Copy(playerId, 0, openSegment.Array, openSegment.Offset + count, 8);
//count += 8;
ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);
if (success) // success์ Send
Send(sendBuff);
}
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
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;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes : {numOfBytes}");
}
}
}โ
์์ ๋ DummyClient์ Program Class
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ServerCore; // Servercore ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฐธ์กฐ
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
// DNS (Domain Name System) : Domain์ IP ๋คํธ์ํฌ์์ ์ฐพ์๊ฐ ์ ์๋ IP๋ก ๋ณํํด ์ค๋ค.
string host = Dns.GetHostName(); // Local Computer์ host ์ด๋ฆ์ ๋ฐํ
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0]; // ip ์ฃผ์๋ฅผ ๋ฐฐ์ด๋ก ๋ฐํ (์๋ฅผ ๋ค์ด Google๊ณผ ๊ฐ์ด Traffic์ด ์ด๋ง๋ฌด์ํ ์ฌ์ดํธ๋ ์ฌ๋ฌ๊ฐ์ ip ์ฃผ์๋ฅผ ๊ฐ์ง ์ ์๊ธฐ ๋๋ฌธ)
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip ์ฃผ์์ port ๋ฒํธ๋ฅผ ๋งค๊ฐ๋ณ์๋ก ์
๋ ฅ
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new ServerSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}โ
Server์ ClientSession Class ์์ฑ ํ Server์ Program Class ์ ๋ด์ฉ ๋ถ๋ฆฌ
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
class Packet // ํจํท ์ค๊ณ์ ์ต๋ํ Size๋ฅผ ์์ถํ์ฌ ๋ณด๋ด๋ ๊ฒ์ด ์ค์ํ๋ค.
{
// Packet์ด ์์ ์ฒด๋ก ์๋์ง? ์๋ ค์ ์๋์ง? ๊ตฌ๋ถํ ์ ์์ด์ผ ํ๋ค.
public ushort size; // ushort๋ 2Byte
public ushort packetId; // ushort๋ 2Byte
}
class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ
{
public long playerId;
}
class PlayerInfoOk : Packet // Server์์ Client๋ก ์์ฒญ์ ๋ํ ๋ต๋ณ์ ์ ๋ฌํ๋ ๊ฒ
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
class ClientSession : PacketSession
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Packet packet = new Packet() { size = 100, packetId = 10 };
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, buffer.Length, buffer2.Length);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
// OnRecvPacket ์ฝ๋ ์์
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort count = 0; // ์ง๊ธ๊น์ง ๋ช Byte๋ฅผ Buffer์ ๋ฐ์ด ๋ฃ์๋๊ฐ?
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); // ToUInt16์ Byte ๋ฐฐ์ด์ ushort๋ก ๋ฝ์๋ฌ๋ผ๋ ๊ฒ
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); // ToUInt16์ Byte ๋ฐฐ์ด์ ushort๋ก ๋ฝ์๋ฌ๋ผ๋ ๊ฒ
count += 2;
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count); // ToUInt16์ Byte ๋ฐฐ์ด์ long์ผ๋ก ๋ฝ์๋ฌ๋ผ๋ ๊ฒ
count += 8;
Console.WriteLine($"PlayerInfoReq: {playerId}");
}
break;
}
Console.WriteLine($"RecvPacketID: {id}, Size: {size}");
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes : {numOfBytes}");
}
}
}โ
์์ ๋ Server์ Program Class
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
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 ClientSession(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}โ
# Serialization #2
- ์๋ํ ํ๊ธฐ์ ์์ ์ธํฐํ์ด์ค๋ฅผ ํตํ ์ฝ๋ ์์ ์ ํ ์์ ์ด๋ค.
ServerSession Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
public abstract class Packet
{
public ushort size;
public ushort packetId;
// ์ต์์ Class์ธ Packet์ ์ธํฐํ์ด์ค ์์ฑ
public abstract ArraySegment<byte> Write();
public abstract void Read(ArraySegment<byte> s);
}
class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ
{
public long playerId;
// ์์ฑ์
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
// ์ธํฐํ์ด์ค ๊ตฌํ
public override ArraySegment<byte> Write()
{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
bool success = true;
ushort count = 0; // ์ง๊ธ๊น์ง ๋ช Byte๋ฅผ Buffer์ ๋ฐ์ด ๋ฃ์๋๊ฐ?
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size๋ ๋ชจ๋ ์์
์ด ๋๋ ๋ค ์ด๊ธฐํ
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
// ์ธํฐํ์ด์ค ๊ตฌํ
public override void Read(ArraySegment<byte> s)
{
ushort count = 0;
//ushort size = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> ์ฌ์ฉํ ์ผ์ด ์์ด ํ์ X
count += 2;
//ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> Read๋ฅผ ์คํํ๋ค๋ ๊ฒ์ ์ด๋ฏธ ํจํท ๋ถํด ํ id์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ๋ค์ด๋ฏ๋ก ํ์ X
count += 2;
// ์์ ๋ถ๋ถ (Client๊ฐ ์
์์ ์ผ๋ก ์๋ชป๋ Packet Size๋ฅผ ๋ณด๋ธ ๊ฒฝ์ฐ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํจ)
this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
count += 8;
}
}
// PlayerInfoOk๋ ์ถํ์ ๊ตฌํ ์์
//class PlayerInfoOk : Packet // Server์์ Client๋ก ์์ฒญ์ ๋ํ ๋ต๋ณ์ ์ ๋ฌํ๋ ๊ฒ
//{
// public int hp;
// public int attack;
//}
// ...
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001 };
for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = packet.Write(); // ์ง๋ ฌํ
if (s != null) // success์ Send
Send(s);
}
}
// ...
}
}โ
ClientSession Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
public abstract class Packet
{
public ushort size;
public ushort packetId;
// ์ต์์ Class์ธ Packet์ ์ธํฐํ์ด์ค ์์ฑ
public abstract ArraySegment<byte> Write();
public abstract void Read(ArraySegment<byte> s);
}
class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ
{
public long playerId;
// ์์ฑ์
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
// ์ธํฐํ์ด์ค ๊ตฌํ
public override ArraySegment<byte> Write()
{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
bool success = true;
ushort count = 0; // ์ง๊ธ๊น์ง ๋ช Byte๋ฅผ Buffer์ ๋ฐ์ด ๋ฃ์๋๊ฐ?
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size๋ ๋ชจ๋ ์์
์ด ๋๋ ๋ค ์ด๊ธฐํ
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
// ์ธํฐํ์ด์ค ๊ตฌํ
public override void Read(ArraySegment<byte> s)
{
ushort count = 0;
//ushort size = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> ์ฌ์ฉํ ์ผ์ด ์์ด ํ์ X
count += 2;
//ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> Read๋ฅผ ์คํํ๋ค๋ ๊ฒ์ ์ด๋ฏธ ํจํท ๋ถํด ํ id์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ๋ค์ด๋ฏ๋ก ํ์ X
count += 2;
// ์์ ๋ถ๋ถ (Client๊ฐ ์
์์ ์ผ๋ก ์๋ชป๋ Packet Size๋ฅผ ๋ณด๋ธ ๊ฒฝ์ฐ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํจ)
this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
count += 8;
}
}
// PlayerInfoOk๋ ์ถํ์ ๊ตฌํ ์์
//class PlayerInfoOk : Packet // Server์์ Client๋ก ์์ฒญ์ ๋ํ ๋ต๋ณ์ ์ ๋ฌํ๋ ๊ฒ
//{
// public int hp;
// public int attack;
//}
// ...
class ClientSession : PacketSession
{
// ...
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
// ํจํท์ ๋ถํดํ์ฌ id ์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ๋ค
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
// ํด๋น id ์ ๋ง๋ ์ฝ๋๋ฅผ ์คํ
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
PlayerInfoReq p = new PlayerInfoReq();
p.Read(buffer); // ์ญ์ง๋ ฌํ
Console.WriteLine($"PlayerInfoReq: {p.playerId}");
}
break;
}
Console.WriteLine($"RecvPacketID: {id}, Size: {size}");
}
// ...
}
}โ
# UTF-8 vs UTF-16

- ์ปดํจํฐ๊ฐ ์ธ์์ ์ฒ์ ๋ฑ์ฅํ ๋น์์๋ ์์ด์ ๋ช๊ฐ์ง ํน์๋ฌธ์๋ง์ ์ฌ์ฉํ์๊ณ , ์ด๋ฅผ ์ ์ฅํ๊ธฐ ์ํด 1Byte๋ก ์ถฉ๋ถํ๋ค.
~> ASCII ์ฝ๋ (1Byte) ๋ฑ์ฅ


- ๊ทธ๋ฌ๋ ์ธํฐ๋ท ์๋ ๋์ ํ ์ธ์ด์ ๋ค์์ฑ์ผ๋ก ์ธํ์ฌ 1Byte ๋ง์ผ๋ก๋ ๋ชจ๋ ๋๋ผ์ ์ธ์ด๋ฅผ ํํํ ์ ์๋ค.
~> UNICODE (2Byte) ๋ฑ์ฅ


- Encoding์ ์ปดํจํฐ์์ ๋ฌธ์์ ๊ธฐํธ๋ฅผ ํํํ๊ธฐ ์ํด ๋ฌธ์๋ฅผ ์ด์ง ๋ฐ์ดํฐ๋ก ๋ณํํ๋ ๊ณผ์ ์ด๋ฉฐ ๋ฌธ์์ ์ด์ง ๋ฐ์ดํฐ ๊ฐ์
Mapping ๊ท์น์ ์ ์ํ๋ ๋ฐฉ๋ฒ์ด๋ค.
~> Variable-Width Encoding (๊ฐ๋ณ ๋๋น ์ธ์ฝ๋ฉ) : UTF-8, UTF-16
~> Fixed-Length Encoding (๊ณ ์ ๊ธธ์ด ์ธ์ฝ๋ฉ) : UTF-32
- UTF-8
~> ์๋ฌธ : 1Byte
~> ํ๊ธ : 3Byte
- UTF-16
~> BMP X : 2Byte
~> BMP O : 4Byte
~> ์๋ฌธ : 2Byte
~> ํ๊ธ : 2Byte
# Serialization #3
- ๋ฐ์ดํฐ์ ๊ธธ์ด๊ฐ ๊ฐ๋ณ์ ์ธ String์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํด์ผ ํ ๊น?
String ์ฒ๋ฆฌ๋ฅผ ์ํ ServerSession Class ์ ClientSession Class ์์
(์๋ ์ฝ๋๋ ServerSession Class์ง๋ง ClientSession Class๋ ๋์ผํ ์ฝ๋๋ก ์์ )
using ServerCore; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DummyClient { public abstract class Packet { public ushort size; public ushort packetId; public abstract ArraySegment<byte> Write(); public abstract void Read(ArraySegment<byte> openSegment); } class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ { public long playerId; public string name; // ๊ฐ๋ณ ๊ธธ์ด์ ๋ฉค๋ฒ ๋ณ์๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌ? // ์์ฑ์ public PlayerInfoReq() { this.packetId = (ushort)PacketID.PlayerInfoReq; } public override ArraySegment<byte> Write() { ArraySegment<byte> openSegment = SendBufferHelper.Open(4096); bool success = true; ushort count = 0; Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count); count += sizeof(ushort); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.packetId); // Slice๋ ์ค์ง์ ์ผ๋ก Span์ ๋ณํ๋ฅผ ์ฃผ์ง X count += sizeof(ushort); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.playerId); // Slice๋ ์ค์ง์ ์ผ๋ก Span์ ๋ณํ๋ฅผ ์ฃผ์ง X count += sizeof(long); // string ์ฒ๋ฆฌ #1 (Buffer์ 2Byte์ธ string len์ ๋จผ์ ์ฝ์ ํ string data ์ฝ์ ) //ushort nameLen = (ushort)Encoding.Unicode.GetByteCount(this.name); // GetByteCount()๋ UTF-16 ๊ธฐ์ค์ byte ๋ฐฐ์ด ํฌ๊ธฐ๋ฅผ ๋ฐํ //success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen); // Slice๋ ์ค์ง์ ์ผ๋ก Span์ ๋ณํ๋ฅผ ์ฃผ์ง X //count += sizeof(ushort); //Array.Copy(Encoding.Unicode.GetBytes(this.name), 0, openSegment.Array, count, nameLen); // GetBytes()๋ string์ ๋ฐ์ Byte ๋ฐฐ์ด๋ก ๋ณํ //count += nameLen; // string ์ฒ๋ฆฌ #2 (Buffer์ 2Byte์ธ string len์ ์ํ ๊ณต๊ฐ์ ๋จ๊ฒจ๋ ์ฑ๋ก string data๋ฅผ ๋จผ์ ์ฝ์ ํ string len ์ฝ์ ) ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, name.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer์ string data๋ฅผ ์ฝ์ ํจ๊ณผ ๋์์ string len์ ๋ฐํ success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen); count += sizeof(ushort); count += nameLen; success &= BitConverter.TryWriteBytes(span, count); // size๋ ๋ชจ๋ ์์ ์ด ๋๋ ๋ค ์ด๊ธฐํ if (success == false) return null; return SendBufferHelper.Close(count); } public override void Read(ArraySegment<byte> openSegment) { ushort count = 0; ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count); count += sizeof(ushort); count += sizeof(ushort); this.playerId = BitConverter.ToInt64(span.Slice(count, span.Length - count)); // Slice๋ ์ค์ง์ ์ผ๋ก Span์ ๋ณํ๋ฅผ ์ฃผ์ง X count += sizeof(long); // string ์ฒ๋ฆฌ ushort nameLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count)); count += sizeof(ushort); this.name = Encoding.Unicode.GetString(span.Slice(count, nameLen)); // GetString()๋ Byte ๋ฐฐ์ด์ ๋ฐ์ string์ผ๋ก ๋ณํ count += nameLen; } } // ... }โ
# Serialization #4
- ๋ฐ์ดํฐ์ ๊ธธ์ด๊ฐ ๊ฐ๋ณ์ ์ธ List๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํด์ผ ํ ๊น?
List ์ฒ๋ฆฌ๋ฅผ ์ํ ServerSession Class ์ ClientSession Class ์์
(์๋ ์ฝ๋๋ ServerSession Class์ง๋ง ClientSession Class๋ ๋์ผํ ์ฝ๋๋ก ์์ )
using ServerCore; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DummyClient { public abstract class Packet { public ushort size; public ushort packetId; public abstract ArraySegment<byte> Write(); public abstract void Read(ArraySegment<byte> openSegment); } class PlayerInfoReq : Packet // Client์์ Server๋ก Player์ ์ ๋ณด๋ฅผ ์๊ณ ์ถ๋ค๊ณ ์์ฒญํ๋ ๊ฒ { public long playerId; public string name; public struct SkillInfo { public int id; public short level; public float duration; public bool Write(Span<byte> span, ref ushort count) // span์ ์ ์ฒด Byte ๋ฐฐ์ด์, count๋ ์ค์๊ฐ์ผ๋ก ํ์ฌ ์ด๋ ๊ณณ์ ์์ ํ๋์ง { bool success = true; success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), id); count += sizeof(int); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), level); count += sizeof(short); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), duration); count += sizeof(float); return success; } public void Read(ReadOnlySpan<byte> span, ref ushort count) { id = BitConverter.ToInt32(span.Slice(count, span.Length - count)); count += sizeof(int); level = BitConverter.ToInt16(span.Slice(count, span.Length - count)); count += sizeof(short); duration = BitConverter.ToSingle(span.Slice(count, span.Length - count)); count += sizeof(float); } } public List<SkillInfo> skills = new List<SkillInfo>(); // ๊ฐ๋ณ ๊ธธ์ด์ ๋ฉค๋ฒ ๋ณ์๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌ? // ์์ฑ์ public PlayerInfoReq() { this.packetId = (ushort)PacketID.PlayerInfoReq; } public override ArraySegment<byte> Write() { ArraySegment<byte> openSegment = SendBufferHelper.Open(4096); bool success = true; ushort count = 0; Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count); count += sizeof(ushort); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.packetId); count += sizeof(ushort); success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.playerId); count += sizeof(long); // string ์ฒ๋ฆฌ ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, name.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer์ string data๋ฅผ ์ฝ์ ํจ๊ณผ ๋์์ string len์ ๋ฐํ success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen); count += sizeof(ushort); count += nameLen; // list ์ฒ๋ฆฌ success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)skills.Count); count += sizeof(ushort); foreach (SkillInfo skill in skills) success &= skill.Write(span, ref count); success &= BitConverter.TryWriteBytes(span, count); // size๋ ๋ชจ๋ ์์ ์ด ๋๋ ๋ค ์ด๊ธฐํ if (success == false) return null; return SendBufferHelper.Close(count); } public override void Read(ArraySegment<byte> openSegment) { ushort count = 0; ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count); count += sizeof(ushort); count += sizeof(ushort); this.playerId = BitConverter.ToInt64(span.Slice(count, span.Length - count)); count += sizeof(long); // string ์ฒ๋ฆฌ ushort nameLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count)); count += sizeof(ushort); this.name = Encoding.Unicode.GetString(span.Slice(count, nameLen)); count += nameLen; // list ์ฒ๋ฆฌ skills.Clear(); ushort skillLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count)); count += sizeof(ushort); for (int i = 0; i < skillLen; i++) { SkillInfo skill = new SkillInfo(); skill.Read(span, ref count); skills.Add(skill); } } } // ... }โ
# Packet Generator #1
- [ ์๋ฃจ์ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ์ถ๊ฐ ] - [ ์ ์๋ฃจ์ ํด๋ ] ๋ฅผ ํตํด ํด๋๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
- [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ์ถ๊ฐ ] - [ ์ ํญ๋ชฉ ] ์ [ C# ํญ๋ชฉ ] - [ ๋ฐ์ดํฐ ] ์์ XML ํ์ผ์ ์ถ๊ฐํ ์ ์๋ค.
~> ์์ฑ๋ XML ํ์ผ์ [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ํ์ผ ํ์๊ธฐ์์ ํด๋ ์ด๊ธฐ ] - [ bin ] - [ Debug ] - [ net ] ์์
์์นํ๋๋ก ํ๋ค. (์ฆ, ํด๋น ํ๋ก์ ํธ์ ์คํํ์ผ์ด ์๋ ๊ณณ์ ์์นํ๋๋ก)
- ํจํท์ ์ ์๋ฅผ ์ด๋ค ๋ฐฉ์์ผ๋ก ํ ์ง ๊ฒฐ์ ํด์ผ ํ๋ค. (JSON, XML, ์์ฒด ์ ์ IDL)
~> XML์ด JSON์ ๋นํด Hierarchy๊ฐ ์ ๋ณด์ธ๋ค๋ ์ฅ์ ์ ๊ฐ์ง๋ฏ๋ก XML์ ์ฌ์ฉํ ๊ฒ์ด๋ค.
~> XML์์ ์ ๋ณด๋ ์์ Tag์ ๋ Tag ์ฌ์ด์ ๋ด๊ฒจ์ง๋ฉฐ, Tag๋ ์์ ์ด๋ฃฌ๋ค.
(Tag ์ฌ์ด์ ์ฝ์ ํ ์ ๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ ์์ Tag ๋์ /๋ฅผ ์ถ๊ฐํ์ฌ ๋ Tag ์๋ต ๊ฐ๋ฅ)
- List๋ฅผ ์ ์ธํ ๋๋จธ์ง ๋ถ๋ถ๋ค์ ์๋ํํ๊ธฐ ์ํ Template์ ๋ง๋ค๊ณ ์ ํ๋ค.
PacketGenerator ํ๋ก์ ํธ ์์ฑ ํ PDL XML ํ์ผ ์์ฑ
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name="PlayerInfoReq">
<long name="playerId"/>
<string name="name"/>
<list name="skill">
<int name="id"/>
<short name="level"/>
<float name="duration"/>
</list>
</packet>
</PDL>โ
PacketGenerator ์ฝ๋ ์์
using System.Xml;
namespace PacketGenerator
{
internal class Program
{
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments= true, // ์ฃผ์์ ๋ฌด์
IgnoreWhitespace = true // ๊ณต๋ฐฑ์ ๋ฌด์
};
// XML ํ์ฑ
using (XmlReader reader = XmlReader.Create("PDL.xml", settings))
{
reader.MoveToContent(); // header๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ด์ฉ ๋ถ๋ถ์ผ๋ก ์ด๋
while (reader.Read()) // ํ์ค์ฉ ์ฝ์ด๋๊ฐ๋ค.
{
if (reader.Depth == 1 && reader.NodeType == XmlNodeType.Element) // Element๋ ์์ Tag, EndElement๋ ๋ Tag
ParsePacket(reader);
// Console.WriteLine(reader.Name + " " + reader["name"]); // Name์ Type์ ๋ฐํ, []๋ Attribute๋ฅผ ๋ฐํ
}
}
// reader.Dispose(); ~> using ์ฌ์ฉ์ using ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฒฝ์ฐ ์๋์ผ๋ก Dispose()๋ฅผ ํธ์ถ
}
public static void ParsePacket(XmlReader reader)
{
if (reader.NodeType == XmlNodeType.EndElement)
return;
if (reader.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = reader["name"];
if (string.IsNullOrEmpty(packetName) )
{
Console.WriteLine("Packet without name");
return;
}
ParseMembers(reader);
}
public static void ParseMembers(XmlReader reader)
{
string packetName = reader["name"];
int depth = reader.Depth + 1;
while(reader.Read())
{
if (reader.Depth != depth)
break;
string memberName = reader["name"];
if (string.IsNullOrEmpty(memberName) )
{
Console.WriteLine("Member without name");
return;
}
string memberType = reader.Name.ToLower();
switch (memberType)
{
case "bool":
case "byte":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
case "string":
case "list":
break;
default:
break;
}
}
}
}
}โ
PacketFormat Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// ์ฌ๋ฌ์ค์ ๊ฑฐ์ณ ๋ฌธ์์ด์ ์ ์ํ๊ณ ์ถ์ ๊ฒฝ์ฐ @""
// ๊ณ ์ ์ ์ธ ๋ถ๋ถ์ ์ ์ธํ ๋ณ๊ฒฝ๋๋ ๋ถ๋ถ์ {}๋ก ํ์(์ผ๋ฐ์ ์ธ ์๊ดํธ๋ { { } }๋ก ํ์)
// {0} ํจํท ์ด๋ฆ
// {1} ๋ฉค๋ฒ ๋ณ์๋ค
// {2} ๋ฉค๋ฒ ๋ณ์ Read
// {3} ๋ฉค๋ฒ ๋ณ์ Write
public static string packetFormat =
@"
class {0}
{{
{1}
public void Read(ArraySegment<byte> openSegment)
{{
ushort count = 0;
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}
public ArraySegment<byte> Write()
{{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
bool success = true;
ushort count = 0;
Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)PacketID.{0});
count += sizeof(ushort);
{3}
success &= BitConverter.TryWriteBytes(span, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}}
}}
";
// {0} ๋ณ์ ํ์
// {1} ๋ณ์ ์ด๋ฆ
public static string memberFormat =
@"public {0} {1}";
// {0} ๋ณ์ ์ด๋ฆ
// {1} To๋ณ์ํ์ (ex : ToInt16, ToInt32, ToSingle ...)
// {2} ๋ณ์ ํ์
public static string readFormat =
@"
this.{0} = BitConverter.{1}(span.Slice(count, span.Length - count));
count += sizeof({2});
";
// {0} ๋ณ์ ์ด๋ฆ
public static string readStringFormat =
@"
ushort {0}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(span.Slice(count, {0}Len));
count += {0}Len;
";
// {0} ๋ณ์ ์ด๋ฆ
// {1} ๋ณ์ ํ์
public static string writeFormat =
@"
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.{0});
count += sizeof({1});
";
// {0} ๋ณ์ ์ด๋ฆ
public static string writeStringFormat =
@"
ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer์ string data๋ฅผ ์ฝ์
ํจ๊ณผ ๋์์ string len์ ๋ฐํ
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), {0}Len);
count += sizeof(ushort);
count += {0}Len;
";
}
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://velog.io/@mercurios0603/%ED%8C%8C%EC%8B%B1Parsing%EC%9D%B4%EB%9E%80)
- Parsing์ ์ปดํจํฐ ๊ณผํ ๋ฐ ํ๋ก๊ทธ๋๋ฐ์์ ํน์ ํ์์ผ๋ก ๊ตฌ์ฑ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ๊ณ ๊ทธ ์๋ฏธ๋ฅผ ์ดํดํ๋ ๊ณผ์ ์ ์๋ฏธ
- Parsing์ ์ฃผ๋ก ํ ์คํธ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฅผ ํด์ํ๊ฑฐ๋, ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ์์ค ์ฝ๋๋ฅผ ์ดํดํ๊ฑฐ๋, ๋ฌธ์๋ฅผ ๊ตฌ์กฐํํ๊ณ
๋ด์ฉ์ ์ถ์ถํ๋ ๋ฐ ์ฌ์ฉ
# Packet Generator #2
- ์ง๋์๊ฐ ์ ์ธํ List ๋ถ๋ถ์ ์๋ํํ๊ธฐ ์ํ Template์ ๋ง๋ค๊ณ , Packet Generator๊ฐ ์ ์คํ๋๋์ง ํ์ธํ๊ณ ์ ํ๋ค.
~> Packet Generator์ ๊ฒฐ๊ณผ๋ฅผ "GenPackets.cs" ํ์ผ์ ์ ์ฅํ์๋ค.
~> "GenPackets.cs" ํ์ผ์ [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ํ์ผ ํ์๊ธฐ์์ ํด๋ ์ด๊ธฐ ] - [ bin ] - [ Debug ] - [ net ]
์์ ํ์ธํ ์ ์๋ค.
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// ...
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
// {2} ๋ฉค๋ฒ ๋ณ์๋ค
// {3} ๋ฉค๋ฒ ๋ณ์ Read
// {4} ๋ฉค๋ฒ ๋ณ์ Write
public static string memberListFormat =
@"
public struct {0}
{{
{2}
public void Read(ReadOnlySpan<byte> span, ref ushort count)
{{
{3}
}}
public bool Write(Span<byte> span, ref ushort count)
{{
bool success = true;
{4}
return success;
}}
}}
public List<{0}> {1}s = new List<{0}>();
";
// ...
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
public static string readListFormat =
@"
this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);
for (int i = 0; i < {1}Len; i++)
{{
{0} {1} = new {0}();
{1}.Read(span, ref count);
{1}s.Add({1});
}}
";
// ...
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
public static string writeListFormat =
@"
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)this.{1}s.Count);
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
success &= {1}.Write(span, ref count);
";
}
}
PacketGenerator ์ฝ๋ ์์
using System.Xml;
namespace PacketGenerator
{
internal class Program
{
static string genPackets; // ์ค์๊ฐ์ผ๋ก ๋ง๋ค์ด์ง๋ ํจํท
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments= true, // ์ฃผ์์ ๋ฌด์
IgnoreWhitespace = true // ๊ณต๋ฐฑ์ ๋ฌด์
};
// XML ํ์ฑ
using (XmlReader reader = XmlReader.Create("PDL.xml", settings))
{
reader.MoveToContent(); // header๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ด์ฉ ๋ถ๋ถ์ผ๋ก ์ด๋
while (reader.Read()) // ํ์ค์ฉ ์ฝ์ด๋๊ฐ๋ค.
{
if (reader.Depth == 1 && reader.NodeType == XmlNodeType.Element) // Element๋ ์์ ๋ถ๋ถ, EndElement๋ ๋ ๋ถ๋ถ
ParsePacket(reader);
// Console.WriteLine(reader.Name + " " + reader["name"]); // Name์ Type์ ๋ฐํ, []๋ Attribute๋ฅผ ๋ฐํ
}
}
File.WriteAllText("GenPackets.cs", genPackets); // genPackets์ ๋ด์ฉ์ ํตํด GenPackets.cs ํ์ผ ์์ฑ
}
public static void ParsePacket(XmlReader reader)
{
if (reader.NodeType == XmlNodeType.EndElement)
return;
if (reader.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = reader["name"];
if (string.IsNullOrEmpty(packetName) )
{
Console.WriteLine("Packet without name");
return;
}
Tuple<string, string, string> tuple = ParseMembers(reader);
genPackets += string.Format(PacketFormat.packetFormat,
packetName, tuple.Item1, tuple.Item2, tuple.Item3);
}
// ๋ฉค๋ฒ ๋ณ์๋ค, ๋ฉค๋ฒ ๋ณ์ Read, ๋ฉค๋ฒ ๋ณ์ Write ์ ๊ดํ ์ฝ๋๋ฅผ ์๋ง๊ฒ ์ ์ํ ๋ค ์ด๋ฅผ string์ผ๋ก ๋ฐํ
public static Tuple<string, string, string> ParseMembers(XmlReader reader)
{
string packetName = reader["name"];
string memberCode = "";
string readCode = "";
string writeCode = "";
int depth = reader.Depth + 1;
while(reader.Read())
{
if (reader.Depth != depth)
break;
string memberName = reader["name"];
if (string.IsNullOrEmpty(memberName) )
{
Console.WriteLine("Member without name");
return null;
}
if (string.IsNullOrEmpty(memberCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
memberCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
if(string.IsNullOrEmpty(readCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
readCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
if(string.IsNullOrEmpty(writeCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
writeCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
string memberType = reader.Name.ToLower();
switch (memberType)
{
case "bool":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
break;
case "string":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readStringFormat, memberName);
writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
break;
case "list":
Tuple<string, string, string> tuple = ParseList(reader);
memberCode += tuple.Item1;
readCode += tuple.Item2;
writeCode += tuple.Item3;
break;
default:
break;
}
}
// ๊ฐ๋
์ฑ์ ์ํด Text๋ฅผ ์ ๋ ฌ
memberCode = memberCode.Replace("\n", "\n\t"); // Enter๊ฐ ์
๋ ฅ๋ ๊ณณ์ Enter ์
๋ ฅ ํ Tab ๊น์ง ์
๋ ฅ๋๋๋ก ์์
readCode = readCode.Replace("\n", "\n\t\t"); // Enter๊ฐ ์
๋ ฅ๋ ๊ณณ์ Enter ์
๋ ฅ ํ Tab Tab ๊น์ง ์
๋ ฅ๋๋๋ก ์์
writeCode = writeCode.Replace("\n", "\n\t\t"); // Enter๊ฐ ์
๋ ฅ๋ ๊ณณ์ Enter ์
๋ ฅ ํ Tab Tab ๊น์ง ์
๋ ฅ๋๋๋ก ์์
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}
public static Tuple<string, string, string> ParseList(XmlReader reader)
{
string listName = reader["name"];
if (string.IsNullOrEmpty(listName))
{
Console.WriteLine("List without name");
return null;
}
// memberListFormat์ {2}, {3}, {4}๋ ์์๋๋ก ๋ฉค๋ฒ ๋ณ์๋ค, ๋ฉค๋ฒ ๋ณ์ Read, ๋ฉค๋ฒ ๋ณ์ Write ์ด๋ฏ๋ก ParseMembers() ํจ์ ์ฌ์ฉ
Tuple<string, string, string> tuple = ParseMembers(reader);
string memberCode = string.Format(PacketFormat.memberListFormat,
FirstCharToUpper(listName), FirstCharToLower(listName),
tuple.Item1, tuple.Item2, tuple.Item3);
string readCode = string.Format(PacketFormat.readListFormat,
FirstCharToUpper(listName), FirstCharToLower(listName));
string writeCode = string.Format(PacketFormat.writeListFormat,
FirstCharToUpper(listName), FirstCharToLower(listName));
return new Tuple <string, string, string> (memberCode, readCode, writeCode);
}
public static string FirstCharToUpper(string input)
{
if (string.IsNullOrEmpty(input))
return "";
return input[0].ToString().ToUpper() + input.Substring(1);
}
public static string FirstCharToLower(string input)
{
if (string.IsNullOrEmpty(input))
return "";
return input[0].ToString().ToLower() + input.Substring(1);
}
public static string ToMemberType(string memberType)
{
switch (memberType)
{
case "bool":
return "ToBoolean";
case "short":
return "ToInt16";
case "ushort":
return "ToUInt16";
case "int":
return "ToInt32";
case "long":
return "ToInt64";
case "float":
return "ToSingle";
case "double":
return "ToDouble";
default:
return "";
}
}
}
}โ
# Packet Generator #3
- using ๊ณผ enum ๋ฐ byte ๋ถ๋ถ์ ์๋ํํ๊ธฐ ์ํ Template์ ๋ง๋ค๊ณ , ํจํท์ด ์ฌ๋ฌ๊ฐ ์กด์ฌํด๋ ์๋ํ๊ฐ ์ ๋๋์ง
ํ์ธํ๊ณ ์ ํ๋ค.
PDL XML ํ์ผ ์์ (ํจํท ๋ฐ sbyte ๋ฉค๋ฒ ๋ณ์ ์ถ๊ฐ)
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name="PlayerInfoReq">
<sbyte name="testByte"/>
<long name="playerId"/>
<string name="name"/>
<list name="skill">
<int name="id"/>
<short name="level"/>
<float name="duration"/>
</list>
</packet>
<packet name="Test">
<int name="testInt"/>
</packet>
</PDL>โ
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// {0} ํจํท ์ด๋ฆ/๋ฒํธ ๋ชฉ๋ก
// {1} ํจํท ๋ชฉ๋ก
public static string fileFormat =
@"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
public enum PacketID
{{
{0}
}}
{1}
";
// {0} ํจํท ์ด๋ฆ
// {1} ํจํท ๋ฒํธ
public static string packetEnumFormat =
@"{0} = {1},";
// ...
// {0} ๋ณ์ ์ด๋ฆ
// {1} ๋ณ์ ํ์
public static string readByteFormat =
@"
this.{0} = ({1})openSegment.Array[openSegment.Offset + count];
count += sizeof({1});
";
// ...
// {0} ๋ณ์ ์ด๋ฆ
// {1} ๋ณ์ ํ์
public static string writeByteFormat =
@"
openSegment.Array[openSegment.Offset + count] = (byte)this.{0};
count += sizeof({1});
";
// ...
}
}โ
PacketGenerator ์ฝ๋ ์์
using System.Xml;
namespace PacketGenerator
{
internal class Program
{
// ...
static void Main(string[] args)
{
// ...
static ushort packetId; // ๋ช๊ฐ์ ํจํท์ ์ฒ๋ฆฌํ์๋์ง
static string packetEnums; // Parsing ์ฒ๋ฆฌ๋ ํจํท์ ์ด๋ฆ๊ณผ ๋ฒํธ
// fileFormat์ ํตํด using๊ณผ enum ์ถ๊ฐ
string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);
File.WriteAllText("GenPackets.cs", fileText);
}
public static void ParsePacket(XmlReader reader)
{
// ...
Tuple<string, string, string> tuple = ParseMembers(reader);
genPackets += string.Format(PacketFormat.packetFormat,
packetName, tuple.Item1, tuple.Item2, tuple.Item3);
// ํจํท 1๊ฐ๋ฅผ Parsing ํ ๋๋ง๋ค packetEnums์ Parsing ์ฒ๋ฆฌ๋ ํจํท์ ์ด๋ฆ๊ณผ ๋ฒํธ๋ฅผ ์ ์ฅ
packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine + "\t";
}
// ๋ฉค๋ฒ ๋ณ์๋ค, ๋ฉค๋ฒ ๋ณ์ Read, ๋ฉค๋ฒ ๋ณ์ Write ์ ๊ดํ ์ฝ๋๋ฅผ ์๋ง๊ฒ ์ ์ํ ๋ค ์ด๋ฅผ string์ผ๋ก ๋ฐํ
public static Tuple<string, string, string> ParseMembers(XmlReader reader)
{
string packetName = reader["name"];
string memberCode = "";
string readCode = "";
string writeCode = "";
int depth = reader.Depth + 1;
while(reader.Read())
{
// ...
if (string.IsNullOrEmpty(memberCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
memberCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
if(string.IsNullOrEmpty(readCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
readCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
if(string.IsNullOrEmpty(writeCode) == false) // ์ด๋ฏธ ๋ด์ฉ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
writeCode += Environment.NewLine; // Enter๋ฅผ ์น๋ ๊ฒ๊ณผ ๊ฐ์ ๋์
string memberType = reader.Name.ToLower();
switch (memberType)
{
case "byte":
case "sbyte":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readByteFormat, memberName, memberType);
writeCode += string.Format(PacketFormat.writeByteFormat, memberName, memberType);
break;
case "bool":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
break;
case "string":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readStringFormat, memberName);
writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
break;
case "list":
Tuple<string, string, string> tuple = ParseList(reader);
memberCode += tuple.Item1;
readCode += tuple.Item2;
writeCode += tuple.Item3;
break;
default:
break;
}
}
// ...
}
// ...
}
}โ
# Packet Generator #4
- Packet Generator์ ๊ฒฐ๊ณผ๊ฐ ์ ์ฅ๋ "GenPackets.cs" ํ์ผ์ ๋ด์ฉ์ ์๋์ผ๋ก ServerSession ๊ณผ ClientSession class์
์ถ๊ฐํ์๋๋ฐ ํด๋น ๋ถ๋ถ์ ์๋ํํ๊ธฐ ์ํ Template์ ๋ง๋ค๊ณ ์ ํ๋ค.
- ์ถ๋ ฅ ๊ฒฝ๋ก๋ [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ์์ฑ ] - [ ๋น๋ ] - [ ์ผ๋ฐ ] ์ [ ์ถ๋ ฅ ] ์์ ์ค์ ๊ฐ๋ฅํ๋ค.
~> ๊ทธ ํ [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ํ์ผ ํ์๊ธฐ์์ ํด๋ ์ด๊ธฐ ] ์์
"ํ๋ก์ ํธ์ด๋ฆ.csproj" ํ์ผ์ ๋ฉ๋ชจ์ฅ์ผ๋ก ์ฐ ๋ค <PropertyGroup></PropertyGroup> ์ฌ์ด์
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> ๋ฅผ ์ถ๊ฐํ๊ณ ,
<BaseOutputPath>์ถ๋ ฅ ๊ฒฝ๋ก</BaseOutputPath> ๋ฅผ <OutputPath>์ถ๋ ฅ ๊ฒฝ๋ก</OutputPath> ๋ก ์์ ํ๋ค.
~> ๋ชจ๋ ์ค์ ์ด ๋๋ ๋ค [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ๋น๋ ] ์ ์ค์ ํ ์ถ๋ ฅ ๊ฒฝ๋ก์ ์คํํ์ผ์ด ์์ฑ๋๋ค.
- ๋ฐฐ์น ํ์ผ์ ๋ช ๋ น ์ธํฐํ๋ฆฌํฐ์ ์ํด ์คํ๋๊ฒ๋ ๊ณต์๋ ๋ช ๋ น์ด๋ค์ด ๋์ด๋ ํ ์คํธ ํ์ผ์ด๋ค.
~> .bat ๋๋ .cmd ํ์์ ํ์ฅ์ ํ์ผ์ ์ง์ ์คํํ๊ฑฐ๋ ๋ช ๋ น ํ๋กฌํํธ์์ ๋ฐฐ์น ํ์ผ์ ์ด๋ฆ์ผ๋ก ์คํํ ์ ์๋ค.
~> ๋ฐฐ์นํ์ผ์ ํตํด [ ํ๋ก์ ํธ ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ ํ์ผ ํ์๊ธฐ์์ ํด๋ ์ด๊ธฐ ] - [ bin ] ๋ด์ ํ๋ก์ ํธ ์คํํ์ผ์
๋์ ์คํํ ๋ค ์คํ ํ์ผ์ ๊ฒฐ๊ณผ์ ์ ์ฅ๋ ๋ด์ฉ์ ์๋์ผ๋ก ๋ณต์ฌํ ์์ ์ด๋ค. (์ถ๋ ฅ ๊ฒฝ๋ก๋ฅผ /bin ์ผ๋ก ์ค์ )
~> START๋ ์์ฉ ํ๋ก๊ทธ๋จ ์คํ ๋ช ๋ น์ด๋ก ์คํํ๊ณ ์ ํ๋ ํ์ผ์ ๊ฒฝ๋ก์ ์ธ์๋ฅผ ๋๊ฒจ์ค๋ค.
~> XCOPY๋ ํ์ผ ๋ณต์ฌ ๋ช ๋ น์ด๋ก ๋ณต์ฌ ๋์๊ณผ ๋ณต์ฌ ์์น๋ฅผ ๋๊ฒจ์ค๋ค.
(/Y ์ต์ ์ ๊ฐ์ ํ์ผ์ด ์๋ ๊ฒฝ์ฐ ๋ฌด์กฐ๊ฑด ๋ฎ์ด์ด๋ค๋ ๊ฒ์ด๋ค.)
GenPackets.bat ๋ฐฐ์น ํ์ผ ์์ฑ (๋ฐฐ์น ํ์ผ์ [ ์๋ฃจ์ ] - [ Common ] - [ Packet ] ๋ด์ ์กด์ฌ)
(๋ฐฐ์น ํ์ผ ์คํ์ PDL.xml์ ์ธ์๋ก ๋๊ธด Packet Generator๊ฐ ์คํ๋์ด DummyClient/Packet ๊ณผ Server/Packet ์ฐํ์ GenPackets์ Packet Generator์ ๊ฒฐ๊ณผ๊ฐ ์ ์ฅ๋ "GenPackets.cs" ํ์ผ์ ๋ด์ฉ์ด ์๋์ผ๋ก ๋ณต์ฌ๋๋ค.)
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml XCOPY /Y GenPackets.cs "../../DummyClient/Packet" XCOPY /Y GenPackets.cs "../../Server/Packet"โ
PacketGenerator ์ฝ๋ ์์
using System.Xml;
namespace PacketGenerator
{
internal class Program
{
// ...
static void Main(string[] args)
{
string pdlPath = "../PDL.xml"; // bin ํด๋๊ฐ ์๋ PacketGenerator ํด๋์ ์๋ PDL.xml ์ฐพ๊ธฐ ์ํ ๊ฒ (์คํ ํ์ผ์ด ์์นํ ๊ณณ ๊ธฐ์ค์ผ๋ก ์ด๋)
// ...
if (args.Length >= 1) // ํ๋ก๊ทธ๋จ ์คํ์ ์ธ์๋ก ๋ฌด์ธ๊ฐ๋ฅผ ๋๊ฒจ์ค ๊ฒฝ์ฐ
pdlPath = args[0]; // pdlPath๋ฅผ ์ ๋ฌ๋ฐ์ ์ธ์๋ก ์ด๊ธฐํ
using (XmlReader reader = XmlReader.Create(pdlPath, settings))
{
// ...
}
// ...
}
// ...
}
}โ
# Packet Generator #5
- ๊ฒ์ ๊ท๋ชจ๊ฐ ์ปค์ง์๋ก ํจํท์ ์ข ๋ฅ๋ ์๋นํ ๋ง์์ง๋ค.
~> ์ฆ, ClientSession Class์ OnRecvPacket() ํจ์ ์์ switch-case๋ฌธ ๊ธธ์ด๊ฐ ์๋นํ ๊ธธ์ด์ง ์ ์๋ค.
- switch-case๋ฌธ ๋ฐ OnRecvPacket() ํจ์ ์๋ํ๋ฅผ ์ํด ๋ชจ๋ ํจํท๋ค์ด base interface๋ฅผ ์์๋ฐ๋๋ก ํ๋ค.
~> ๋ชจ๋ ํจํท์ ๋ํด ๊ณตํต ์ธ์๋ก ๋๊ธธ ์ ์๊ธฐ ๋๋ฌธ์ ํธ๋ฆฌํ๋ค๋ ์ฅ์ ์ ๊ฐ์ง๋ค.
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// {0} ํจํท ์ด๋ฆ/๋ฒํธ ๋ชฉ๋ก
// {1} ํจํท ๋ชฉ๋ก
public static string fileFormat =
@"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
public enum PacketID
{{
{0}
}}
interface IPacket
{{
ushort Protocol {{ get; }}
void Read(ArraySegment<byte> openSegment);
ArraySegment<byte> Write();
}}
{1}
";
// ...
// {0} ํจํท ์ด๋ฆ
// {1} ๋ฉค๋ฒ ๋ณ์๋ค
// {2} ๋ฉค๋ฒ ๋ณ์ Read
// {3} ๋ฉค๋ฒ ๋ณ์ Write
public static string packetFormat =
@"
public class {0} : IPacket
{{
{1}
public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}
public void Read(ArraySegment<byte> openSegment)
{{
ushort count = 0;
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}
public ArraySegment<byte> Write()
{{
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
bool success = true;
ushort count = 0;
Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)PacketID.{0});
count += sizeof(ushort);
{3}
success &= BitConverter.TryWriteBytes(span, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}}
}}
";
// ...
}
}โ
DummyClient/Packet ๊ณผ Server/Packet ์ฐํ์ PacketHandler Class ์์ฑ
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class PacketHandler
{
// ํด๋น ํจํท์ด ์ ๋ถ ์กฐ๋ฆฝ๋ ๊ฒฝ์ฐ ๋ฌด์์ ํ ๊น?
// PacketHandler๋ ์๋ํ ์์ด ์๋์ผ๋ก ์ถ๊ฐ
public static void PlayerInfoReqHandler(PacketSession session, IPacket packet)
{
PlayerInfoReq p = packet as PlayerInfoReq;
Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");
foreach (PlayerInfoReq.Skill skill in p.skills)
{
Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
}
}
}
}โ
DummyClient/Packet ๊ณผ Server/Packet ์ฐํ์ PacketManager Class ์์ฑ
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class PacketManager
{
// PacketManager๋ Singleton ํจํด ์ฌ์ฉ
#region Singleton
static PacketManager _instance;
public static PacketManager Instance
{
get {
if (_instance == null)
_instance = new PacketManager();
return _instance;
}
}
#endregion
// ๊ตฌ๋ถํ๊ธฐ ์ํ Protocol ID, ์ด๋ค ์์
์ ์ํํ ์ง
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
// ๊ตฌ๋ถํ๊ธฐ ์ํ Protocol ID, ์ด๋ค Handler๋ฅผ ํธ์ถํ ์ง
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register() // ์ถํ ์๋ํํ ์์
{
_onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>);
_handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler);
}
public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
{
// ํจํท์ ๋ถํดํ์ฌ id ์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ๋ค
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
// ์ด์ switch-case๋ฌธ์ด ์๋ Dictionary์์ ์ฐพ์ Invoke()
Action<PacketSession, ArraySegment<byte>> action = null;
if (_onRecv.TryGetValue(id, out action))
action.Invoke(session, buffer);
}
void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{
T packet = new T();
packet.Read(buffer); // ์ญ์ง๋ ฌํ
// Dictionary์์ ์ฐพ์ Invoke()
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}
}
}โ
ClientSession Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
class ClientSession : PacketSession
{
// ...
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
// ...
}
}โ
Server ์ฝ๋ ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
// ...
static void Main(string[] args)
{
// MultiThread๊ฐ ๊ฐ์
ํ์ง ์๋ ๋ถ๋ถ์์ ์คํ
PacketManager.Instance.Register();
// ...
}
}
}โ
# Packet Generator #6
- PacketManager ๋ฅผ ์๋ํํ๊ธฐ ์ํ Template์ ๋ง๋ค๊ณ ์ ํ๋ค.
~> ์๋ฐฉํฅ ํจํท์ ๊ฑฐ์ ์กด์ฌํ์ง ์๋๋ค. (๋๋ถ๋ถ Client์์ Server ๋๋ Server์์ Client)
~> ๊ทธ๋ฌ๋ ๋ถ์ฐ Server์ธ ๊ฒฝ์ฐ Client์ Server๋ง ์ํตํ๋ ๊ฒ์ด ์๋ Server ๋ผ๋ฆฌ๋ ์ํตํ๋ค.
~> ํจํท ์ด๋ฆ์ ๊ท์น์ ์ค์ ํ๊ณ ์ด๋ฅผ ํตํด ํ์ผ์ ๋ถ๋ฆฌํ๋ค.
(ํจํท ์ด๋ฆ ์์ C_ ๊ฐ ๋ถ์ ๊ฒ์ Client์์ Server๋ก, S_ ๊ฐ ๋ถ์ ๊ฒ์ Server์์ Client๋ก)
(PacketManager ๋ฅผ ์๋ํํ ๋ Server ์ชฝ์ ์ถ๊ฐ๋๋ PacketManager์ Register์๋ C_๊ฐ ๋ถ์ ๊ฒ๋ค๋ง,
DummyClient ์ชฝ์ ์ถ๊ฐ๋๋ PacketManager์ Register์๋ S_๊ฐ ๋ถ์ ๊ฒ๋ค๋ง ๋ฑ๋กํ๋ค.)
(์ฆ, Register์ ์จ๊ฐ ํจํท์ ๋ฑ๋กํ๋ ๊ฒ์ด ์๋ ํ์ํ ํจํท๋ค๋ง ๋ฑ๋กํ๋ค.)
PDL XML ํ์ผ ์์
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name="C_PlayerInfoReq">
<sbyte name="testByte"/>
<long name="playerId"/>
<string name="name"/>
<list name="skill">
<int name="id"/>
<short name="level"/>
<float name="duration"/>
</list>
</packet>
<packet name="Test">
<int name="testInt"/>
</packet>
</PDL>โ
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// {0} ํจํท ๋ฑ๋ก
public static string managerFormat =
@"
using ServerCore;
using System;
using System.Collections.Generic;
class PacketManager
{{
#region Singleton
static PacketManager _instance;
public static PacketManager Instance
{{
get
{{
if (_instance == null)
_instance = new PacketManager();
return _instance;
}}
}}
#endregion
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register()
{{
{0}
}}
public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
{{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
Action<PacketSession, ArraySegment<byte>> action = null;
if (_onRecv.TryGetValue(id, out action))
action.Invoke(session, buffer);
}}
void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{{
T packet = new T();
packet.Read(buffer);
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}}
}}
";
// {0} ํจํท ์ด๋ฆ
public static string managerRegisterFormat =
@"
_onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>);
_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);
";
// ...
}
}โ
PacketGenerator ์ฝ๋ ์์
using System.Xml;
namespace PacketGenerator
{
internal class Program
{
// ...
static string clientRegister;
static string serverRegister;
static void Main(string[] args)
{
// ...
string clientManagerText = string.Format(PacketFormat.managerFormat, clientRegister);
File.WriteAllText("ClientPacketManager.cs", clientManagerText);
string serverManagerText = string.Format(PacketFormat.managerFormat, serverRegister);
File.WriteAllText("ServerPacketManager.cs", serverManagerText);
}
public static void ParsePacket(XmlReader reader)
{
// ...
if (packetName.StartsWith("S_") || packetName.StartsWith("s_"));
clientRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
else
serverRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
}
// ...
}
}โ
GenPackets.bat ๋ฐฐ์น ํ์ผ ์์
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"
[ ์น์ 4. Job Queue ]
# ์ฑํ ํ ์คํธ #1
- Server ๊ตฌํ์ ๋๋ถ๋ถ ์ฑํ ํ๋ก๊ทธ๋จ์ ํตํด ํ ์คํธ๊ฐ ์ด๋ฃจ์ด์ง๋ค.
- ์ฐ์ ServerCore์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ํ์ง ์์ ๋ถ๋ถ์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์์ ํ ์์ ์ด๋ค.
~> Disconnect() ์ค๋ณต ํธ์ถ์ ์๋ฐฉํ์์ผ๋, Disconnect() ํธ์ถ์ Send์ Receive๋ ๋๊น์ ๋ํ ์๋ฐฉ์ด ์๋ค.
(๋์๋ค๋ฐ์ ์ผ๋ก ๋๊ตฐ๊ฐ๋ Disconnect๋ฅผ ํตํด socket์ shutdownํ๊ณ , ๋๊ตฐ๊ฐ๋ Send๋ Receive๋ฅผ ํธ์ถ์ ๋ฌธ์ ๋ฐ์)
- ๋ํ ์ฑํ ํ ์คํธ๋ฅผ ์ํด Server ์ ์ฅ์์ ์ฝ๋ ์ถ๊ฐ ๋ฐ ์์ ์ ํ ์์ ์ด๋ค.
PDL XML ํ์ผ ์์
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name="C_Chat">
<string name="chat"/>
</packet>
<packet name="S_Chat">
<int name="playerId"/>
<string name ="chat"/>
</packet>
</PDL>โ
์์ธ ์ฒ๋ฆฌ๋ฅผ ์ํด Session Class ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
// ...
public abstract class Session
{
// ...
void Clear() // _sendQueue ์ _pendingList ๋ฅผ ์ด๊ธฐํํ๊ธฐ ์ํ ํจ์ ์ถ๊ฐ
{
lock (_lock)
{
_sendQueue.Clear();
_pendingList.Clear();
}
}
// ...
public void Disconnect()
{
// ...
Clear(); // _sendQueue ์ _pendingList ๋ฅผ ์ด๊ธฐํ
}
void RegisterSend()
{
if (_disconnected == 1) // ์ต์ํ์ ์๋ฐฉ์ฑ
return;
while (_sendQueue.Count > 0)
{
ArraySegment<byte> buff = _sendQueue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
// socket์ ๋ค๋ฃจ๋ ๋ถ๋ถ์ try-catch๋ฌธ์ผ๋ก ๊ฐ์ธ์ค๋ค. (MultiThread ํ๊ฒฝ์ ์ํ ์๋ฐฉ์ฑ
)
// ~> ๋๊ตฐ๊ฐ๋ ์์ if๋ฌธ์ ํต๊ณผํ์ฌ ์๋ ๋ถ๋ถ์ ๋ง์ ์คํํ๋ ค๊ณ ํ๋ ๋์ค์ ๋ค๋ฅธ Thread์์ socket์ disconnect์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ
try
{
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
catch (Exception e)
{
Console.WriteLine($"RegisterSend Failed {e}");
}
}
// ...
void RegisterRecv()
{
if (_disconnected == 1) // ์ต์ํ์ ์๋ฐฉ์ฑ
return;
_recvBuffer.Clean();
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
// socket์ ๋ค๋ฃจ๋ ๋ถ๋ถ์ try-catch๋ฌธ์ผ๋ก ๊ฐ์ธ์ค๋ค. (MultiThread ํ๊ฒฝ์ ์ํ ์๋ฐฉ์ฑ
)
// ~> ๋๊ตฐ๊ฐ๋ ์์ if๋ฌธ์ ํต๊ณผํ์ฌ ์๋ ๋ถ๋ถ์ ๋ง์ ์คํํ๋ ค๊ณ ํ๋ ๋์ค์ ๋ค๋ฅธ Thread์์ socket์ disconnect์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ
try
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
catch(Exception e)
{
Console.WriteLine($"RegisterRecv Failed {e}");
}
}
// ...
}
}โ
Server์ GameRoom Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class GameRoom
{
List<ClientSession> _sessions = new List<ClientSession>(); // GameRoom์ ์กด์ฌํ๋ session๋ค
object _lock = new object(); // List๋ Dictionary ๋ฑ ๋๋ถ๋ถ์ ์๋ฃ ๊ตฌ์กฐ๋ค์ MultiThread ํ๊ฒฝ์์ ์ ๋์๊ฐ๋ค๋ ๋ณด์ฅ์ด ์๊ธฐ ๋๋ฌธ์ lock ์์ฑ
public void Broadcast(ClientSession session, string chat) // ํ์ฌ session์ด ์ ์์ค์ธ ๋ฐฉ์ ์กด์ฌํ๋ ๋ชจ๋์๊ฒ chat์ ๋ฟ๋ฆฐ๋ค.
{
S_Chat packet = new S_Chat();
packet.playerId = session.SessionId;
packet.chat = chat;
ArraySegment<byte> segment = packet.Write();
lock(_lock)
{
foreach (ClientSession s in _sessions)
s.Send(segment);
}
}
public void Enter(ClientSession session) // ๋ฐฉ ์
์ฅ
{
lock (_lock)
{
_sessions.Add(session);
session.Room = this;
}
}
public void Leave(ClientSession session) // ๋ฐฉ ํด์ฅ
{
lock (_lock)
{
_sessions.Remove(session);
}
}
}
}โ
Server์ SessionManager Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Server
{
class SessionManager // SessionManager๋ Engine ์ชฝ์์ ๊ด๋ฆฌํด๋ ๋๊ณ , Content ์ชฝ์์ ๊ด๋ฆฌํด๋ ๋๋ค. (์ ํ์ ์ฐจ์ด)
{
// SessionManager๋ Singleton ํจํด ์ฌ์ฉ
static SessionManager _session = new SessionManager();
public static SessionManager Instance { get { return _session; } }
int _sessionId = 0; // session์ ๊ตฌ๋ถํ๊ธฐ ์ํ Id
Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession>(); // ํ์ฌ ์กด์ฌํ๋ session๋ค
object _lock = new object(); // List๋ Dictionary ๋ฑ ๋๋ถ๋ถ์ ์๋ฃ ๊ตฌ์กฐ๋ค์ MultiThread ํ๊ฒฝ์์ ์ ๋์๊ฐ๋ค๋ ๋ณด์ฅ์ด ์๊ธฐ ๋๋ฌธ์ lock ์์ฑ
public ClientSession Generate() // session ์์ฑ
{
lock (_lock)
{
int sessionId = ++_sessionId;
ClientSession session = new ClientSession();
session.SessionId = sessionId;
_sessions.Add(sessionId, session);
Console.WriteLine($"Connected : {sessionId}");
return session;
}
}
public ClientSession Find(int id) // sessionId๋ฅผ ํตํด session์ ์ฐพ๋ ํจ์
{
lock (_lock)
{
ClientSession session = null;
_sessions.TryGetValue(id, out session);
return session;
}
}
public void Remove(ClientSession session) // session ์ญ์
{
lock (_lock)
{
_sessions.Remove(session.SessionId);
}
}
}
}โ
Server ์ฝ๋ ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
static Listener _listener = new Listener();
public static GameRoom Room = new GameRoom(); // GameRoom ์์ฑ (๋ด ๊ณณ์์๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋๋ก public์ผ๋ก ์์ฑ)
static void Main(string[] args)
{
PacketManager.Instance.Register();
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ์๋์ ์
์ฅ์ํจ๋ค.
_listener.Init(endPoint, () => { return SessionManager.Instance.Generate(); }); // new๋ฅผ ํตํด Session์ ์์ฑํ๋ ๊ฒ์ด ์๋ SessionManager๋ฅผ ํตํด ์์ฑํ๋๋ก ์์
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}โ
ClientSession ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
class ClientSession : PacketSession
{
public int SessionId { get; set; } // Session ๊ตฌ๋ถ์ ์ํด
public GameRoom Room { get; set; } // ํ์ฌ ์ด๋ค ๋ฐฉ์ ์์นํ๋์ง ์๊ธฐ ์ํด
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Program.Room.Enter(this); // Client๊ฐ ์ ์์ ๋ฐฉ์ ์
์ฅ์ํจ๋ค. (Program ์ฐํ์ static์ผ๋ก Room์ ์์ฑํ์๊ธฐ ๋๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ด ํธ์ถ)
Thread.Sleep(5000);
Disconnect();
}
// ...
public override void OnDisconnected(EndPoint endPoint)
{
SessionManager.Instance.Remove(this); // ๋ด ์์ ์ (์ฆ, session์) sessionManager๋ฅผ ํตํด ์ญ์ ์์ฒญ
if (Room != null) //
{
Room.Leave(this);
Room = null;
}
Console.WriteLine($"OnDisconnected : {endPoint}");
}
// ...
}
}โ
Server์ PacketHandler ์์
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
class PacketHandler
{
// ํด๋น ํจํท์ด ์ ๋ถ ์กฐ๋ฆฝ๋ ๊ฒฝ์ฐ ๋ฌด์์ ํ ๊น?
// PacketHandler๋ ์๋ํ ์์ด ์๋์ผ๋ก ์ถ๊ฐ
public static void C_ChatHandler(PacketSession session, IPacket packet)
{
C_Chat chatPacket = packet as C_Chat;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
clientSession.Room.Broadcast(clientSession, chatPacket.chat); // ํ์ฌ clientSession์ด ์ ์์ค์ธ ๋ฐฉ์ ์กด์ฌํ๋ ๋ชจ๋์๊ฒ ์ฑํ
๋ฉ์์ง๋ฅผ ๋ฟ๋ฆฐ๋ค.
}
}โ
# ์ฑํ ํ ์คํธ #2
- ์ฑํ ํ ์คํธ๋ฅผ ์ํด Client ์ ์ฅ์์ ์ฝ๋ ์ถ๊ฐ ๋ฐ ์์ ์ ํ ์์ ์ด๋ค.
~> ํ์ฌ 1๋ช ์ ์ ์ ๋ง ์ ์ํ๊ณ ์๋ ์ํฉ์ด๋ฏ๋ก, ์ด๋ฅผ ๋ค์์ ์ ์ ๋ค์ด ์ ์ํ๋ ์ํฉ์ผ๋ก ๊ฐ์ ํ๊ณ ๋ณ๊ฒฝํ ์์ ์ด๋ค.
- ๊ธฐ์กด Server์ Register ๋ถ๋ถ์ ์์ฑ์๋ฅผ ํตํด ์๋์ผ๋ก ์์ฑํ๋๋ก ๋ณ๊ฒฝํ ์์ ์ด๋ค.
ServerSession Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
class ServerSession : PacketSession // Session์ด ์๋ PacketSession ์ ์์ ๋ฐ๋๋ก ์์
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnRecvPacket(ArraySegment<byte> buffer) // PacketSession ์ ์์ ๋ฐ์ผ๋ฏ๋ก OnRecv๊ฐ ์๋ OnRecvPacket์ผ๋ก ์์ (๋ฐํ๊ฐ๋ int๊ฐ ์๋ void๋ก)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
public override void OnSend(int numOfBytes)
{
// Console.WriteLine($"Transferred bytes : {numOfBytes}"); ~> session์ด ๋ง์์ง๋ฉด OnSend() ๊ฐ ์์ฃผ ํธ์ถ๋๋ฏ๋ก ์ผ๋จ์ ์ถ๋ ฅ๋์ง ์๋๋ก ์ฃผ์ ์ฒ๋ฆฌ
}
}
}
ServerCore์ Connector ์์
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
public class Connector
{
Func<Session> _sessionFactory;
public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory, int count = 1) // ๋ค์์ Client ํ๊ฒฝ์์ Test ํ๊ณ ์ถ์ ์๋ ์๊ธฐ ๋๋ฌธ์
{
for (int i = 0; i < count; i++) // ์
๋ ฅ๋ฐ์ ๋งค๊ฐ๋ณ์ count ๋งํผ ์๋์ ๊ณผ์ ์ ๋ฐ๋ณตํ๋๋ก ์์
{
// ํด๋ํฐ ์ค์
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory = sessionFactory;
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += OnConnectCompleted;
args.RemoteEndPoint = endPoint;
args.UserToken = socket;
RegisterConnect(args);
}
}
// ...
}
}โ
DummyClient์ SessionManager Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Text;
namespace DummyClient
{
class SessionManager
{
// SessionManager๋ Singleton ํจํด ์ฌ์ฉ
static SessionManager _session = new SessionManager();
public static SessionManager Instance { get { return _session; } }
List<ServerSession> _sessions = new List<ServerSession>(); // ํ์ฌ ์กด์ฌํ๋ session๋ค
object _lock = new object(); // List๋ Dictionary ๋ฑ ๋๋ถ๋ถ์ ์๋ฃ ๊ตฌ์กฐ๋ค์ MultiThread ํ๊ฒฝ์์ ์ ๋์๊ฐ๋ค๋ ๋ณด์ฅ์ด ์๊ธฐ ๋๋ฌธ์ lock ์์ฑ
public void SendForEach() // Server์ชฝ์ผ๋ก ์ฑํ
ํจํท์ ์ ์ก
{
foreach (ServerSession session in _sessions)
{
C_Chat chatPacket = new C_Chat();
chatPacket.chat = $"Hello Server!";
ArraySegment<byte> segment = chatPacket.Write();
session.Send(segment);
}
}
public ServerSession Generate() // session ์์ฑ
{
lock (_lock)
{
ServerSession session = new ServerSession();
_sessions.Add(session);
return session;
}
}
}
}โ
DummyClient ์ฝ๋ ์์
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ServerCore;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
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 SessionManager.Instance.Generate(); }, 10); // new๋ฅผ ํตํด Session์ ์์ฑํ๋ ๊ฒ์ด ์๋ SessionManager๋ฅผ ํตํด ์์ฑํ๋๋ก ์์ , ์ํ๋ Client ์๋ฅผ ์ธ์๋ก ๋๊น
while (true)
{
try
{
SessionManager.Instance.SendForEach(); // ๋ชจ๋ Session๋ค์ด Server์ชฝ์ผ๋ก ๊ณ์ํด์ ์ฑํ
ํจํท์ ์๋๋ก
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(250); // 0.25์ด ํด์
}
}
}
}โ
DummyClient์ PacketHandler ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
class PacketHandler
{
// C_Chat์ Server์ ๋ณด๋ธ๋ค, Server๋ ๋ฐฉ์ ์๋ ๋ชจ๋ ์ ๋ค์๊ฒ S_Chat์ผ๋ก ๋ต์ฅ์ ์ฃผ๋ ๋ถ๋ถ์ ๋ค๋ฃฌ๋ค.
public static void S_ChatHandler(PacketSession session, IPacket packet)
{
S_Chat chatPacket = packet as S_Chat;
ServerSession serverSession = session as ServerSession;
Console.WriteLine(chatPacket.chat);
}
}โ
PacketFormat Class ์์
(Main์์ PacketManager.Instance.Register(); ๋ฅผ ์ง์ ์ ๋ ฅํ๋ ๊ฒ์ด ์๋ ์์ฑ์๋ฅผ ํตํด ์๋์ผ๋ก ์์ฑ๋๋๋ก ์์ )
using System; using System.Collections.Generic; using System.Text; namespace PacketGenerator { class PacketFormat { // {0} ํจํท ๋ฑ๋ก public static string managerFormat = @" using ServerCore; using System; using System.Collections.Generic; class PacketManager {{ #region Singleton static PacketManager _instance = new PacketManager(); public static PacketManager Instance {{ get {{ return _instance; }} }} #endregion PacketManager() {{ Register(); }} Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>(); Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>(); public void Register() {{ {0} }} public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer) {{ ushort count = 0; ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); count += 2; ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); count += 2; Action<PacketSession, ArraySegment<byte>> action = null; if (_onRecv.TryGetValue(id, out action)) action.Invoke(session, buffer); }} void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new() {{ T packet = new T(); packet.Read(buffer); Action<PacketSession, IPacket> action = null; if (_handler.TryGetValue(packet.Protocol, out action)) action.Invoke(session, packet); }} }} "; // ... } }โ
- ์์ ๊ฒฐ๊ณผ๋ก GameRoom ์์ ์กด์ฌํ๋ 10๋ช ์ ์ ์ ๋ค์๊ฒ ์ฑํ ํจํท์ ๋ฟ๋ ค์ฃผ๊ณ ์๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
~> ๊ทธ๋ฌ๋ ์์ ๊ฐ์ ๋ฐฉ์์ MMORPG์ ๋์ ํ ๊ฒฝ์ฐ ์๋๊ฐ ์๋นํ ๋๋ ค์ง ์ ์๋ค.
(์ ์ ์ ์๊ฐ ์ฆ๊ฐํ ์๋ก, ํจํท์ ๋ฟ๋ฆฌ๋ ์์ด ๋ง์์ง๋ฏ๋ก)
- ์์ Thread๋ค์ ๋ชจ๋ Broadcast์ lock ๋ถ๋ถ์์ ๋๊ธฐ์ค์ด๋ค.
~> ์ด๋ ๋น์ฐํ ๊ฒฐ๊ณผ์ด๋ค. ์๋ํ๋ฉด Thread.Sleep(250) ์ ํตํด 0.25์ด์ 1๋ฒ ๋์ํ๋๋ก ์ค์ ํ์์ผ๋ฏ๋ก ๋ง์ฝ 100๋ช ์
์ ์ ๊ฐ ์๋ค๊ณ ๊ฐ์ ํ๋ฉด 100 * 100 = 10,000๋ฒ์ด๋ฏ๋ก 1์ด์ 40,000๋ฒ ๋์ํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค. (250 * 4 = 1000 = 1์ด)
์ด์ lock ๋ถ๋ถ์ ๋์๋ค๋ฐ์ ์ผ๋ก ์๋ง์ Thread๋ค์ด ๋ค์ด์ค์ง๋ง lock ๋๋ฌธ์ 1๋ฒ์ 1๊ฐ์ Thread๋ง ์ฒ๋ฆฌํ ์ ์๋ค.
~> ๋ฐ๋ผ์ ์์ ์๋ง์ ์์ ๋ค์ด ๋ฐ๋ฆฌ๊ฒ ๋๋ฉด์ Thread๋ฅผ ๊ด๋ฆฌํ๋ ์ ์ฅ์์๋ Thread๋ฅผ ๋ณด๋์ผ๋ ์ผ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋์ง
์์๊ธฐ ๋๋ฌธ์ ๋ค์ Thread๋ฅผ ๋ณด๋ด๊ณ ์๋ ์ํฉ์ด ๋ฐ์ํ๋ค. ์ด์ Thread๊ฐ ๊ณ์ํด์ ์์ด๊ฒ ๋๋ ๊ฒ์ด๋ค.
- ์ด๋ฌํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ Recv๋ฅผ ํ์๋ง์, lock์ ํตํด ํจํท์ ์ ์กํ์๊ธฐ ๋๋ฌธ์ด๋ค.
~> ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ ํ๋์ Thread๋ง Queue์ ์์ฌ์๋ ์ผ๊ฐ์ ์ฒ๋ฆฌํ๊ณ , ๋ค๋ฅธ Thread๋ค์ ์ผ๊ฐ์ Queue์ ๋ด์๋๋
๊ฒ์ด๋ค. (์ด๋ฅผ Job ๋๋ Task๋ผ๊ณ ํ๋ฉฐ, ์ค์ํ ๊ฒ์ ํจํท์ ์ ์ฅํ๊ณ ํ๋์ Thread์์ ์ฒ๋ฆฌํ๋ ๊ฒ)
# Command ํจํด
- ๋ค์ ์๊ฐ์ ๋ง๋ค์ด๋ณผ Job ๋๋ Task๋ฅผ ๊ด๋ฆฌํ๋ Queue๊ฐ ์ ํ์ ์ธ Command ํจํด์ ์์ ์ด๋ค.

- ์๋์ ๋๋ฆฌํ๋ ClientSession์ด ์ง์์ธ Thread์๊ฒ ์ฃผ๋ฌธ์ ํ๋ค.

- ๋ง์ฝ ์ง์์ด ์๋น, ์๋ฆฌ, ๊ณ์ฐ์ ์ ๋ถ ๋๋งก์ ํ ๊ฒฝ์ฐ ์ฃผ๋ฌธ์ ๋ฐ์๋ง์ ์ฃผ๋ฐฉ์ ๋ฌ๋ ค๊ฐ ์๋ฆฌ๋ฅผ ๋ฐ๋ก ์์ํ ๊ฒ์ด๋ค.
~> ์ง๊ธ๊น์ง ๊ตฌํ๋ ์ฝ๋๊ฐ ์์ ๋น์ทํ๋ค.

- ๋ง์ฝ ์ฃผ๋ฐฉ์ ํฌ๊ธฐ๊ฐ ๋๋ฌด ์์ ๋์์ 1๋ช ๋ง ์๋ฆฌ๊ฐ ๊ฐ๋ฅํ ๊ฒฝ์ฐ ์ฃผ๋ฌธ์ ๋ฐ์ ์ง์๋ค์ ์ฃผ๋ฐฉ ์์์ ์์ ์ ์๋ฆฌ ์ฐจ๋ก๊ฐ
์ฌ๋๊น์ง ๊ณ์ํด์ ๊ธฐ๋ค๋ฆฌ๊ฒ ๋๋ค.

- ๋ชจ๋ ์ง์๋ค์ด ์์ ์ ์๋ฆฌ ์ฐจ๋ก๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฃผ๋ฌธ์ ๋ฐ์ ์ง์์ด ๋ถ์กฑํ ๊ฒฝ์ฐ ์๋น์ ์ง์์ ๋ ๊ณ ์ฉํ๋ค.
~> ์ง๊ธ๊น์ง ๊ตฌํ๋ ์ฝ๋์ ๊ฒฐ๊ณผ๊ฐ ์์ ๋น์ทํ๋ค.

- ์์ ๊ฐ์ด 1๋ช ์ ์ง์์ด ์๋น, ์๋ฆฌ, ๊ณ์ฐ์ ๋ชจ๋ ๋ด๋นํ๋ ๊ฒ์ด ์๋ ์ง์๋ค ๊ฐ๊ฐ์ด ์ ๋ฌด๋ฅผ ๋ถ๋ด๋ฐ๋๋ก ํ๋ค.
~> ์๋น์ ๋ด๋นํ๋ ์ง์์ด ์ฃผ๋ฌธ์ ๋ฐ์ ์ฃผ๋ฌธ์๋ฅผ ์ฃผ๋ฐฉ์ฅ์๊ฒ ์ ๋ฌํ๋ค.
~> ์ด๋ฌํ ๋ฐฉ๋ฒ์ด command ํจํด๊ณผ ์ ์ฌํ๋ค.
+ ์ถ๊ฐ ๊ฒ์ (https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%BB%A4%EB%A7%A8%EB%93%9C-%ED%8C%A8%ED%84%B4Command-Pattern)
- command ํจํด์ด๋?
~> command ํจํด์ ๊ฐ์ฒด ์งํฅ ๋์์ธ ํจํด ์ค ํ๋๋ก, ๊ฐ์ฒด ๊ฐ์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๊ณ ์ ์ฐ์ฑ์ ๋์ด๋ ํจํด์ด๋ค.
~> command ํจํด์ ์ฃผ์ ๋ชฉ์ ์ ์ฌ์ฉ์๊ฐ ๋ณด๋ธ ์์ฒญ์ ๊ฐ์ฒด์ ํํ๋ก ์บก์ํํ์ฌ ์ด๋ฅผ ๋์ค์ ์ด์ฉํ ์ ์๋๋ก
์ด๋ฆ, ๋งค๊ฐ๋ณ์ ๋ฑ ์์ฒญ์ ํ์ํ ์ ๋ณด๋ฅผ ์ ์ฅ ๋๋ ๋ก๊น , ์ทจ์ํ ์ ์๋๋ก ํ๋ ํจํด์ผ๋ก, ์ด๋ฅผ ํตํด ๋ฉ์๋๋ฅผ
ํธ์ถํ๋ Class์ ๋ฉ์๋๋ฅผ ๊ตฌํํ๋ Class ์ฌ์ด์ ๊ฒฐํฉ์ ๋์จํ๊ฒ ๋ง๋ ๋ค.
~> ๋ฐ๋ผ์ Client๊ฐ ์์ฒญ์ ์์ ์๋ฅผ ์ ํ์ ์์ด ๋ค์ํ ์์ฒญ์ ๋ณด๋ผ ์ ์๊ฒ ๋๋ค.
+ ์ถ๊ฐ ๊ฒ์ (https://bamtory29.tistory.com/entry/%EC%BB%A4%EB%A7%A8%EB%93%9C-%ED%8C%A8%ED%84%B4-Command-Pattern)
Command ์ธํฐํ์ด์ค
public interface Command
{
public void execute();
}โ
ComputerOnCommand Class
public class ComputerOnCommand implements Command
{
private Computer computer;
public ComputerOnCommand(Computer computer)
{
this.computer = computer;
}
@Override
public void execute()
{
computer.turnOn();
}
}โ
ComputerOffCommand Class
public class ComputerOffCommand implements Command
{
private Computer computer;
public ComputerOffCommand(Computer computer)
{
this.computer = computer;
}
@Override
public void execute()
{
computer.turnOff();
}
}โ
Computer Class
public class Computer
{
public void Computer() {}
public void turnOn()
{
System.out.println("์ปดํจํฐ ์ ์ ์ผ์ง");
}
public void turnOff()
{
System.out.println("์ปดํจํฐ ์ ์ ๊บผ์ง");
}
}โ
Button Class
public class Button
{
private Command command;
public Button(Command command)
{
this.command = command;
}
public void setCommand(Command command)
{
this.command = command;
}
public void pressButton()
{
this.command.execute();
}
}โ
Main ๋ฉ์๋
public static void main(String[] args)
{
Computer computer = new Computer(); //์ปดํจํฐ๋ Receiver
//์ปดํจํฐ ๊ฐ์ฒด ์์ฑ
ComputerOnCommand computerOnCmd = new ComputerOnCommand(computer);
ComputerOffCommand computerOffCmd = new ComputerOffCommand(computer);
Button btn = new Button(computerOnCmd); //๋ฒํผ์ด Invoker ์ญํ
btn.pressButton();
btn.setCommand(computerOffCmd);
btn.pressButton();
}
# JobQueue #1
- ์ง๋ ์๊ฐ ํ์ตํ Comman ํจํด์ ํ์ฉํ์ฌ JobQueue๋ฅผ ๊ตฌํํ ์์ ์ด๋ค.
~> Queue์ ์์ธ ์ผ๊ฐ์ ์ฒ๋ฆฌํ๋ Thread๋ Push์ _jobQueue์ ์ฒ์์ผ๋ก ์ผ๊ฐ์ ๋ฐ์ด ๋ฃ๋ Thread๊ฐ ๋ด๋นํ๋ค.
ServerCore์ JobQueue Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
public interface IJobQueue
{
void Push(Action job);
}
public class JobQueue : IJobQueue // IJobQueue๋ฅผ ์์ ๋ฐ๋๋ค.
{
Queue<Action> _jobQueue = new Queue<Action>(); // ์ผ๊ฐ ๋ชฉ๋ก์ ๋ด๋ ๊ณณ
object _lock = new object(); // MultiThread ํ๊ฒฝ์ ์ํ lock ์ ์ธ
bool _flush = false; // Queue์ ์์ธ ์ผ๊ฐ๋ค์ ๋ณธ์ธ์ด ์ฒ๋ฆฌํ ๊ฒ์ธ์ง ์๋์ง
public void Push(Action job) // _jobQueue์ ์ผ๊ฐ์ ๋ฐ์ด ๋ฃ๊ธฐ ์ํ ํจ์
{
bool flush = false; // MultiThread ํ๊ฒฝ์์ ๋จ 1๊ฐ์ Thread๋ง ์ผ๊ฐ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋๋ก
lock (_lock)
{
_jobQueue.Enqueue(job);
if (_flush == false) // _flush๊ฐ false์ธ ๊ฒฝ์ฐ Queue์ ์์ธ ์ผ๊ฐ๋ค์ ๋ณธ์ธ์ด ์ฒ๋ฆฌ
flush = _flush = true;
}
if (flush)
Flush();
}
void Flush()
{
while (true)
{
Action action = Pop();
if (action == null)
return;
action.Invoke();
}
}
Action Pop() // ์ผ๊ฐ ์ฒ๋ฆฌ๋ฅผ ์ํด _jobQueue์์ ์ผ๊ฐ์ ๊บผ๋ด๊ธฐ ์ํ ํจ์
{
lock (_lock)
{
if (_jobQueue.Count == 0)
{
_flush = false;
return null;
}
return _jobQueue.Dequeue();
}
}
}
}โ
GameRoom Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class GameRoom : IJobQueue // IJobQueue๋ฅผ ์์ ๋ฐ๋๋ค.
{
// ...
JobQueue _jobQueue = new JobQueue();
public void Push(Action job)
{
_jobQueue.Push(job);
}
// ...
}
}โ
ClientSession Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
class ClientSession : PacketSession
{
// ...
public override void OnConnected(EndPoint endPoint)
{
// ...
// ๋ฐ๋ก ์คํํ๋ ๊ฒ์ด ์๋ ์ผ๊ฐ์ _jobQueue์ ๋ด๋ ๊ฒ
Program.Room.Push(() => Program.Room.Enter(this));
// Program.Room.Enter(this); // Client๊ฐ ์ ์์ ๋ฐฉ์ ์
์ฅ์ํจ๋ค. (Program ์ฐํ์ static์ผ๋ก Room์ ์์ฑํ์๊ธฐ ๋๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ด ํธ์ถ)
// ...
}
// ...
public override void OnDisconnected(EndPoint endPoint)
{
// ...
if (Room != null)
{
// ์คํ ๋์ค Client ์ข
๋ฃ์ Null Crash ๋ฐฉ์ง๋ฅผ ์ํ ๊ฒ & ๋ฐ๋ก ์คํํ๋ ๊ฒ์ด ์๋ ์ผ๊ฐ์ _jobQueue์ ๋ด๋ ๊ฒ
GameRoom room = Room;
room.Push(() => room.Leave(this));
//Room.Leave(this);
Room = null;
}
// ...
}
// ...
}
}โ
PacketHandler Class ์์
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
class PacketHandler
{
public static void C_ChatHandler(PacketSession session, IPacket packet)
{
// ...
// ์คํ ๋์ค Client ์ข
๋ฃ์ Null Crash ๋ฐฉ์ง๋ฅผ ์ํ ๊ฒ & ๋ฐ๋ก ์คํํ๋ ๊ฒ์ด ์๋ ์ผ๊ฐ์ _jobQueue์ ๋ด๋ ๊ฒ
GameRoom room = clientSession.Room;
room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
// clientSession.Room.Broadcast(clientSession, chatPacket.chat); // ํ์ฌ clientSession์ด ์ ์์ค์ธ ๋ฐฉ์ ์กด์ฌํ๋ ๋ชจ๋์๊ฒ ์ฑํ
๋ฉ์์ง๋ฅผ ๋ฟ๋ฆฐ๋ค.
}
}โ
# JobQueue #2
- ์ง๋ ์๊ฐ์ ๊ตฌํํ JobQueue์ ๋๊ฐ์ด ๋์ํ์ง๋ง ์๋์ ์ผ๋ก Task๋ฅผ ๋ง๋ค์ด์ฃผ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณผ ์์ ์ด๋ค.
~> ๋๋ค์ ๊ฐ๋ ์ด ๋ฑ์ฅํ์ง ์ผ๋ง ๋์ง ์์ ํ์ ๊ณผ ๊ฐ์ ์ค๋ฌด์์๋ ์๋์ ์ผ๋ก ์ผ ์ฒ๋ฆฌ๊ฐ ํ์ํ ํจ์๋ค์ ๊ตฌํํด์
์ฒ๋ฆฌํ๋ ํ์์ด ์์ฃผ ์ฌ์ฉ๋๋ค.
~> ๊ทธ๋ฌ๋ ์์ ๊ฐ์ด ์๋์ ์ธ ๋ฐฉ๋ฒ์ ์ผ ์ฒ๋ฆฌ๊ฐ ํ์ํ ํจ์๋ฅผ ๋ชจ๋ ๊ตฌํํด์ผ ํ๋ค๋ ๋จ์ ์ด ์กด์ฌํ๋ค.
Server์ TaskQueue Class ์์ฑ
(_queue์ ์์ธ ์ผ๊ฐ ์ฒ๋ฆฌ๋ ์ง๋ ์๊ฐ์ Flush() ์ ๊ฐ์ ๋ฉ์๋์์ ์ฒ๋ฆฌํ๋๋ก)
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Server { interface ITask { void Execute(); } // ์ผ ์ฒ๋ฆฌ๊ฐ ํ์ํ ํจ์์ ๋ง์ถฐ ํด๋์ค๋ฅผ ์์ฑ class BroadcastTask : ITask { // ํจ์ ์คํ์ ํ์ํ ๋ณ์๋ค์ ์ ์ธ GameRoom _room; ClientSession _session; string _chat; // ์์ฑ์๋ฅผ ํตํ ๋ณ์ ์ด๊ธฐํ BroadcastTask(GameRoom room, ClientSession session, string chat) { _room = room; _session = session; _chat = chat; } public void Execute() { _room.Broadcast(_session, _chat); } } class TaskQueue { Queue<ITask> _queue = new Queue<ITask>(); } }โ
๋ถํ Test๋ฅผ ์ํ Listener Class ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
public class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory;
// ๋ฌธ์ง๊ธฐ ์๋ฅผ 10๋ช
์ผ๋ก ์ฆ๊ฐ, ์ต๋ ๋๊ธฐ์๋ฅผ 100๋ช
์ผ๋ก ์ฆ์
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 100)
{
// ๋ฌธ์ง๊ธฐ ๊ณ ์ฉ
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory;
// ๋ฌธ์ง๊ธฐ ๊ต์ก
_listenSocket.Bind(endPoint);
// ์์
์์
// backlog : ์ต๋ ๋๊ธฐ ์
_listenSocket.Listen(backlog);
for (int i = 0; i < register; i++)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
}
// ...
}
}โ
- ๋ง์ฝ Client ์ ์ ์ธ์์ 500๋ช ์ผ๋ก ๋๋ฆฐ ๋ค์ ์คํํ ๊ฒฝ์ฐ ๋ฉ๋ชจ๋ฆฌ๊ฐ ๊ณ์ํด์ ์์นํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
~> ์ด๋ ์๋ง์ ์์ ๋ค์ด ๋ฐ๋ฆฌ๊ฒ ๋๋ฉด์ Thread๋ฅผ ๊ด๋ฆฌํ๋ ์ ์ฅ์์๋ Thread๋ฅผ ๋ณด๋์ผ๋ ์ผ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋์ง ์์๊ธฐ
๋๋ฌธ์ ThreadPool์์ ์๋ก์ด Thread๋ฅผ ๋ฝ์ ๋ณด๋ด๊ณ ์๊ธฐ ๋๋ฌธ์ ๋ฉ๋ชจ๋ฆฌ๊ฐ ๊ณ์ํด์ ์์นํ๋ ๊ฒ์ด๋ค.
- ๊ฐ์ฅ ์ฌํ ๋ถํ๋ฅผ ์ ๋ฐํ๋ ๊ณณ์ Broadcast์ foreach๋ฌธ ๋ด์ Send์ด๋ค.
~> ์๋ํ๋ฉด Thread.Sleep(250) ์ ํตํด 0.25์ด์ 1๋ฒ ๋์ํ๋๋ก ์ค์ ํ์์ผ๋ฏ๋ก 500๋ช ์ ์ ์ ๊ฐ ์๋ค๊ณ ๊ฐ์ ํ๋ฉด
500 * 500 = 250,000๋ฒ์ด๋ฏ๋ก 1์ด์ 1,000,000๋ฒ ๋์ํ๊ณ ์๊ธฐ ๋๋ฌธ์ด๋ค.
~> ์ด๋ N^2์ ์๊ฐ ๋ณต์ก๋๋ฅผ ๊ฐ์ง๋ค.
~> ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ ํจํท ์์ฒญ์ด ์ฌ๋๋ง๋ค ๋ฐ๋ก Send ํ๋ ๊ฒ์ด ์๋ ํจํท์ ๋ชจ์ ๋ค ์ถํ์ ํ๋ฒ์ ๋ณด๋ด๋ ๊ฒ์ด๋ค.
+ ์ถ๊ฐ ๊ฒ์ (https://shung2.tistory.com/1445)
- Zone ํ์์ ๊ฒ์์ ๋งต์ ๋์ด๊ฐ ๋ ๋ก๋ฉ ์๊ฐ์ด ์๊ธฐ๋๋ฌธ์ ํ๋์ Zone ๋จ์์ JobQueue๋ฅผ ๋ฃ์ด ์ฒ๋ฆฌํ๋๋ก ๋ง๋ค๋ฉด
๋๋ฏ๋ก ๋น๊ต์ JobQueue ํ์ฉ ์ฒ๋ฆฌ๊ฐ ์ฉ์ดํ๋ค.
- ๊ทธ๋ฌ๋ Seamless ๊ฒ์์ ๋์ ๋งต์์ ์์๋ก ์์ญ์ ๊ตฌ๋ถํ์ฌ JobQueue๋ฅผ ๋๊ณ ์ฒ๋ฆฌํ ์๋ ์๊ฒ ์ง๋ง, ์ด๋ ๊ฒ ๋๋ค๋ฉด
๊ตฌ๋ถ๋ ์์ญ ์ฌ์ด์์๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง์ ๋ํ ์๋ฌธ์ ์ด ์๊ธธ ์ ๋ฐ์ ์๋ค.
~> JobQueue๋ฅผ ๋ชจ๋ ๊ฐ์ฒด๋ค์๊ฒ ๋ฃ๋ ๋ฐฉ๋ฒ์ด ์๋ค. (์ฆ, User, Monster, Skill ...)
+ ์ถ๊ฐ ๊ฒ์ (https://drehzr.tistory.com/1683)

# ํจํท ๋ชจ์ ๋ณด๋ด๊ธฐ
- ํจํท์ ๋ชจ์ ๋ณด๋ด๋ ๊ฒ์ Engine ์ชฝ์์๋ ์ฒ๋ฆฌํ ์ ์๊ณ Content ์ชฝ์์๋ ์ฒ๋ฆฌํ ์ ์๋ค.
~> ๋ง์ฝ Engine ์ชฝ์์ ์ฒ๋ฆฌํ๊ณ ์ ํ ๊ฒฝ์ฐ Session์ Send() ๋ฉ์๋์์ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
(์ด๋์ ๋ ์์๋ ๋ค ์ผ์ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ณด๋ด๋ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ)
~> ๊ทธ๋ฌ๋ ์ด๋ฒ ์๊ฐ์๋ Content ์ชฝ์์ ํจํท ๋ชจ์ ๋ณด๋ด๊ธฐ๋ฅผ ๊ตฌํํ ์์ ์ด๋ค.
~> ํจํท์ด ๋ถ๊ท์นํ๊ฒ ์ ์ก๋ ๊ฒฝ์ฐ RecvBuffer์ ํฌ๊ธฐ๋ฅผ ๋๋ ค์ฃผ๋ฉด ๋๋ค.
Session Class ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
// ...
public abstract class Session
{
// ...
// List<ArraySegment<byte>> ๋ฅผ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ List์ ๋ด๊ธด ํจํท๋ค์ ์ ๋ถ ์ฒ๋ฆฌํ๋ Send ๋ฉ์๋ ์ถ๊ฐ
public void Send(List<ArraySegment<byte>> sendBuffList)
{
if (sendBuffList.Count == 0)
return;
lock (_lock)
{
foreach (ArraySegment<byte> sendBuff in sendBuffList)
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
// ๊ธฐ์กด์ Send ๋ฉ์๋
public void Send(ArraySegment<byte> sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
// ...
}
}โ
GameRoom Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class GameRoom : IJobQueue
{
// ...
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>(); // ํจํท์ ๋ชจ์ผ๊ธฐ ์ํ List
// ...
public void Flush() // ๋ชจ์๋ ํจํท์ ์ฒ๋ฆฌ
{
foreach (ClientSession s in _sessions)
s.Send(_pendingList);
Console.WriteLine($"Flushed {_pendingList.Count} items");
_pendingList.Clear();
}
// ..
}
}โ
Server ์ฝ๋ ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
// ...
static void Main(string[] args)
{
// ...
while (true)
{
Room.Push(() => Room.Flush());
Thread.Sleep(250);
}
}
}
}โ
# JobTimer
- GameRoom ๋ฟ๋ง์ด ์๋ ๋ค์ํ Room๋ค์ด ์ถ๊ฐ๋ ๊ฒฝ์ฐ ๊ฐ๊ฐ์ ๋ฃธ๋ค์ ์๋ก ๋ค๋ฅธ ๋๊ธฐ์๊ฐ์ ๊ฐ์ง๋ฉฐ ์คํ๋๋ค.
~> ์ฒซ๋ฒ์งธ๋ก๋ Tick์ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๋ค.
~> ๋๋ฒ์งธ๋ก๋ PriorityQueue๋ฅผ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๋ค.
Tick์ ํตํด ์ฒ๋ฆฌํ๊ธฐ ์ํ Server ์ฝ๋ ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
// ...
static void Main(string[] args)
{
// ...
int roomTick = 0;
// room์ด ์ถ๊ฐ๋ ๋๋ง๋ค Tick ๋ณ์๋ ์ถ๊ฐ๋๋ค.
while (true)
{
int now = System.Environment.TickCount;
if (roomTick < now) // ๊ทธ๋ฌ๋ ์ด์ ๊ฐ์ ๋ฐฉ๋ฒ์ ๋ถํ์ํ if๋ฌธ ์ฒดํฌ๊ฐ ๋ฐ๋ณต๋๋ค.
{
Room.Push(() => Room.Flush());
roomTick = now + 250;
}
// room์ด ์ถ๊ฐ๋ ๋๋ง๋ค if๋ฌธ๋ ์ถ๊ฐ๋๋ค.
}
}
}
}โ
ServerCore์ PriorityQueue Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Text;
namespace ServerCore
{
public class PriorityQueue<T> where T : IComparable<T>
{
List<T> _heap = new List<T>();
public int Count { get { return _heap.Count; } }
// 0 (logN)
public void Push(T data)
{
// ํ์ ๋งจ ๋์ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์ฝ์
ํ๋ค.
_heap.Add(data);
int now = _heap.Count - 1;
// ๋์ฅ๊นจ๊ธฐ๋ฅผ ์์
while (now > 0)
{
// ๋์ฅ๊นจ๊ธฐ๋ฅผ ์๋
int next = (now - 1) / 2;
if (_heap[now].CompareTo(_heap[next]) < 0) // ๋์๊ฐ์ด ๋น๊ต๊ฐ๊ณผ ๊ฐ์ ๊ฒฝ์ฐ 0, ์์ ๊ฒฝ์ฐ -1, ํฐ ๊ฒฝ์ฐ 1 ( ๋์๊ฐ.CompareTo(๋น๊ต๊ฐ) )
break; // ์คํจ
// ๋ ๊ฐ์ ๊ต์ฒดํ๋ค.
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// ๊ฒ์ฌ ์์น๋ฅผ ์ด๋ํ๋ค.
now = next;
}
}
// 0 (logN)
public T Pop()
{
// ๋ฐํํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ก ์ ์ฅ
T ret = _heap[0];
// ๋ง์ง๋ง ๋ฐ์ดํฐ๋ฅผ ๋ฃจํธ๋ก ์ด๋ํ๋ค.
int lastIndex = _heap.Count - 1;
_heap[0] = _heap[lastIndex];
_heap.RemoveAt(lastIndex);
lastIndex--;
// ์ญ์ผ๋ก ๋ด๋ ค๊ฐ๋ ๋์ฅ๊นจ๊ธฐ ์์
int now = 0;
while (true)
{
int left = 2 * now + 1;
int right = 2 * now + 2;
int next = now;
// ์ผ์ชฝ๊ฐ์ด ํ์ฌ๊ฐ๋ณด๋ค ํฌ๋ฉด, ์ผ์ชฝ์ผ๋ก ์ด๋
if (left <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
next = left;
// ์ค๋ฅธ์ชฝ๊ฐ์ด ํ์ฌ๊ฐ(์ผ์ชฝ ์ด๋ ํฌํจ)๋ณด๋ค ํฌ๋ฉด, ์ค๋ฅธ์ชฝ์ผ๋ก ์ด๋
if (right <= lastIndex && _heap[next].CompareTo(_heap[right]) < 0)
next = right;
// ์ผ์ชฝ/์ค๋ฅธ์ชฝ ๋ชจ๋ ํ์ฌ๊ฐ๋ณด๋ค ์์ผ๋ฉด ์ข
๋ฃ
if (next == now)
break;
// ๋ ๊ฐ์ ๊ต์ฒดํ๋ค.
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// ๊ฒ์ฌ ์์น๋ฅผ ์ด๋ํ๋ค.
now = next;
}
return ret;
}
public T Peek() // ์ผ๊ฐ ๋ด์ฉ ํ์ธ์ ์ํ ํจ์
{
if (_heap.Count == 0)
return default(T);
return _heap[0];
}
}
}โ
Server์ JobTimer Class ์์ฑ
using System;
using System.Collections.Generic;
using System.Text;
using ServerCore;
namespace Server
{
struct JobTimerElem : IComparable<JobTimerElem> // ํ๋์ ์ผ๊ฐ ๋จ์
{
public int execTick; // ์คํ ์๊ฐ
public Action action; // ์ผ๊ฐ (์ฆ, Job)
// IComparable ์ธํฐํ์ด์ค์ ํ์ ๊ตฌ์ฑ ์์์ธ CompareTo()
public int CompareTo(JobTimerElem other)
{
// ์คํ ์๊ฐ์ด ์ ์์๋ก ๋จผ์ ํ์ด๋์ค๋๋ก (์ฆ, ์ฐ์ ์์๊ฐ ๋์ ํญ๋ชฉ)
return other.execTick - execTick;
}
}
class JobTimer // Job์ ์์ฝํ ์ ์๋ ์์คํ
{
// ์ฐ์ ์์ ํ๋ ๋์๊ด๊ณ ์ฐ์ฐ ์ฒ๋ฆฌ ์๋๊ฐ ์๋นํ ๋น ๋ฅด๋ค.
PriorityQueue<JobTimerElem> _pq = new PriorityQueue<JobTimerElem>();
object _lock = new object(); // MultiThread ํ๊ฒฝ์ ์ํ lock ์ ์ธ
public static JobTimer Instance { get; } = new JobTimer();
public void Push(Action action, int tickAfter = 0) // ์์ฝํ๊ณ ์ ํ๋ ์ผ๊ฐ, ๋ช Tick ํ์ ์คํํด์ผํ๋์ง (์
๋ ฅ ๋ฐ์ง ๋ชปํ ๊ฒฝ์ฐ ๋ฐ๋ก ์คํํ๋๋ก 0์ผ๋ก ์ค์ )
{
JobTimerElem job;
job.execTick = System.Environment.TickCount + tickAfter;
job.action = action;
lock (_lock)
{
_pq.Push(job);
}
}
public void Flush() // ์ผ๊ฐ ์ฒ๋ฆฌ
{
while (true)
{
int now = System.Environment.TickCount;
JobTimerElem job;
lock (_lock)
{
if (_pq.Count == 0) // ์ผ๊ฐ์ด ์๋ ๊ฒฝ์ฐ
break; // while๋ฌธ์ ๋๊ฐ๋ค๋ ์๋ฏธ
job = _pq.Peek(); // ๊บผ๋ด์ง ์๊ณ ์ผ๊ฐ ๋ด์ฉ๋ง ํ์ธํ๋ ๊ฒ
if (job.execTick > now) // ํ์ฌ ์๊ฐ๋ณด๋ค ์คํ ์๊ฐ์ด ๋ง์ด ๋จ์ ๊ฒฝ์ฐ
break;
_pq.Pop();
}
// ์ผ๊ฐ์ ์คํ์ํจ๋ค.
job.action.Invoke();
}
}
}
}โ
Server ์ฝ๋ ์์
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Program
{
// ...
static void FlushRoom()
{
Room.Push(() => Room.Flush());
JobTimer.Instance.Push(FlushRoom, 250);
}
static void Main(string[] args)
{
// ...
JobTimer.Instance.Push(FlushRoom);
while (true)
{
JobTimer.Instance.Flush();
}
}
}
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://velog.io/@aenyoung/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90%EC%99%80-%ED%9E%99)
- Priority Queue๋? (์ฆ, ์ฐ์ ์์ ํ๋?)
~> ์ ์ ์ ์ถ(FIFO)์ ์์น์ ์ํ์ฌ ๋จผ์ ๋ค์ด์จ ๋ฐ์ดํฐ๊ฐ ๋จผ์ ๋๊ฐ๋ Queue์ ๋ฌ๋ฆฌ Priority Queue๋ ๋ฐ์ดํฐ๋ค์ด
์ฐ์ ์์๋ฅผ ๊ฐ์ง๊ณ ์์ด ์ฐ์ ์์๊ฐ ๋์ ๋ฐ์ดํฐ๊ฐ ๋จผ์ ์ถ๋ ฅ๋๋ ์๋ฃ๊ตฌ์กฐ์ด๋ค.
[ ์น์ 5. ์ ๋ํฐ ์ฐ๋ ]
# ์ ๋ํฐ ์ฐ๋ #1
- ์ ๋ํฐ ์ฐ๋ ์ ์ข์ ์์๊ณผ ๋์ ์์์ด ์กด์ฌํ๋ค.
~> ์ข์ ์์์ ๋คํธ์ํฌ ํต์ ์ ์ํด ๊ตฌํํ ์ฝ๋๋ค์ ์ด๋์ ๋ ์ฌ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ์ด๋ค.
~> ๋์ ์์์ Unity์ ์ ์ฑ ์ C#์์ ํ์ฉ๋๋ ๋ฌธ๋ฒ๊ณผ ํ์ฉ๋์ง ์๋ ๋ฌธ๋ฒ๋ค์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ์ GenPacket์
Span, ReadOnlySpan, TryWriteBytes ์ ๊ฐ์ ๊ฒ๋ค์ ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ์ด๋ค.
(Unity 2021 ๋ฒ์ ์ดํ๋ถํฐ๋ Engine์์ ์ง์ํ์ง๋ง ์ค๋ฅ๊ฐ ๋๋ค๋ ๊ฐ์ ํ์ ๊ฐ์๋ฅผ ๋ค์ ์์ )
- ๋ํ Unity์์ MultiThread ํ๊ฒฝ์ ์ ์ฝ์ฌํญ์ด ์กด์ฌํ๋ค.
~> Main Thread๊ฐ ์๋ Background Thread์์ Unity๊ฐ ๊ด๋ฆฌํ๋ ๊ฐ์ฒด๋ค์ ์ ๊ทผํ๊ฑฐ๋ ์ฝ๋๋ฅผ ์คํํ๋ ค๊ณ ํ๋ ๊ฒฝ์ฐ
Crash๊ฐ ๋ฐ์ํ๋ค.
~> ๋ฐ๋ผ์ Game Logic์ Main Thread ์์๋ง ์ ๊ทผ ๋ฐ ์คํํ๋๋ก ์กฐ์ ํด์ผ ํ๋ค.


- ์ฐ์ Unity Hub๋ฅผ ํตํด ์๋ก์ด Client ํ๋ก์ ํธ๋ฅผ ์์ฑํ ๋ค Scripts ํด๋ ์ฐํ์ ์ฌ์ฌ์ฉํ๊ณ ์ ํ๋ ์ฝ๋๋ค์ ๋ณต๋ถํ๋ค.
~> ํ๋ก์ ํธ์ ๊ฒฝ๋ก๋ ์ฌํ๊น์ง ์ค์ตํ๋ ํ๋ก์ ํธ์ ๊ฒฝ๋ก๋ก ์ค์
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// ...
// {0} ํจํท ์ด๋ฆ
// {1} ๋ฉค๋ฒ ๋ณ์๋ค
// {2} ๋ฉค๋ฒ ๋ณ์ Read
// {3} ๋ฉค๋ฒ ๋ณ์ Write
public static string packetFormat =
@"
public class {0} : IPacket
{{
{1}
public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}
public void Read(ArraySegment<byte> segment)
{{
ushort count = 0;
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}
public ArraySegment<byte> Write()
{{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
count += sizeof(ushort);
Array.Copy(BitConverter.GetBytes((ushort)PacketID.{0}), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
{3}
Array.Copy(BitConverter.GetBytes(count), 0, segment.Array, segment.Offset, sizeof(ushort));
return SendBufferHelper.Close(count);
}}
}}
";
// ...
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
// {2} ๋ฉค๋ฒ ๋ณ์๋ค
// {3} ๋ฉค๋ฒ ๋ณ์ Read
// {4} ๋ฉค๋ฒ ๋ณ์ Write
public static string memberListFormat =
@"public class {0}
{{
{2}
public void Read(ArraySegment<byte> segment, ref ushort count)
{{
{3}
}}
public bool Write(ArraySegment<byte> segment, ref ushort count)
{{
bool success = true;
{4}
return success;
}}
}}
public List<{0}> {1}s = new List<{0}>();";
// {0} ๋ณ์ ์ด๋ฆ
// {1} To~ ๋ณ์ ํ์
// {2} ๋ณ์ ํ์
public static string readFormat =
@"this.{0} = BitConverter.{1}(segment.Array, segment.Offset + count);
count += sizeof({2});";
// ...
// {0} ๋ณ์ ์ด๋ฆ
public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(segment.Array, segment.Offset + count, {0}Len);
count += {0}Len;";
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
public static string readListFormat =
@"this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
for (int i = 0; i < {1}Len; i++)
{{
{0} {1} = new {0}();
{1}.Read(segment, ref count);
{1}s.Add({1});
}}";
// {0} ๋ณ์ ์ด๋ฆ
// {1} ๋ณ์ ํ์
public static string writeFormat =
@"Array.Copy(BitConverter.GetBytes(this.{0}), 0, segment.Array, segment.Offset + count, sizeof({1}));
count += sizeof({1});";
// ...
// {0} ๋ณ์ ์ด๋ฆ
public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, segment.Array, segment.Offset + count + sizeof(ushort));
Array.Copy(BitConverter.GetBytes({0}Len), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
count += {0}Len;";
// {0} ๋ฆฌ์คํธ ์ด๋ฆ [๋๋ฌธ์]
// {1} ๋ฆฌ์คํธ ์ด๋ฆ [์๋ฌธ์]
public static string writeListFormat =
@"Array.Copy(BitConverter.GetBytes((ushort)this.{1}s.Count), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
{1}.Write(segment, ref count);";
}
}โ
GenPackets.bat ๋ฐฐ์น ํ์ผ ์์ (๋ฐฐ์น ํ์ผ ์คํ์ Unity Script ๋ํ ๊ฐ์ด ๋ณ๊ฒฝ๋๋๋ก)
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Client/Assets/Scripts/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ClientPacketManager.cs "../../Client/Assets/Scripts/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"โ
Unity Project์ NetworkManager Script ์์ฑํ ๋น ๊ฐ์ฒด์ Component๋ก ์ถ๊ฐ
using DummyClient;
using ServerCore;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
ServerSession _session = new ServerSession();
void Start()
{
// 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 _session; }, 1);
}
void Update()
{
}
}โ
ํต์ ์ด ์ํํ๊ฒ ์ด๋ฃจ์ด์ง๋์ง ํ์ธํ๊ธฐ ์ํด Unity Project์ PacketHandler Script ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
class PacketHandler
{
public static void S_ChatHandler(PacketSession session, IPacket packet)
{
S_Chat chatPacket = packet as S_Chat;
ServerSession serverSession = session as ServerSession;
if (chatPacket.playerId == 1)
Debug.Log(chatPacket.chat);
//if (chatPacket.playerId == 1)
//Console.WriteLine(chatPacket.chat);
}
}โ

- Test๋ฅผ ์ํด Server ์๋ฃจ์ ํ๋ก์ ํธ๋ฅผ ์คํ์์ผ Server์ Client๋ฅผ ๊ตฌ๋์ํจ ๋ค, Unity์ Play ๋ฒํผ์ ๋๋ฅด๋ฉด
Console ์ฐฝ์ Log๊ฐ ๋จ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
# ์ ๋ํฐ ์ฐ๋ #2
- ์ด๋ฒ ์๊ฐ์๋ ๋จ์ํ Log๋ง ๋์ฐ๋ ๊ฒ์ด ์๋ ์ค์ง์ ์ธ ์ก์ ์ ์ทจํ๋๋ก ๋ง๋ค ์์ ์ด๋ค.
~> ์ฐ์ Unity์์ [ Hierarchy ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ 3D Object ] - [ Cylinder ] ๋ฅผ ํตํด 3D ๊ฐ์ฒด ์์ฑ ํ ์ด๋ฆ์
Player๋ก ์ค์ ํด์ค๋ค.
Unity Project์ PacketHandler Script ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
class PacketHandler
{
public static void S_ChatHandler(PacketSession session, IPacket packet)
{
S_Chat chatPacket = packet as S_Chat;
ServerSession serverSession = session as ServerSession;
if (chatPacket.playerId == 1)
{
Debug.Log(chatPacket.chat);
// Unity ๋ด์ ๊ฐ์ฒด๋ฅผ ์ฐพ๋ Logic ์ถ๊ฐ
GameObject go = GameObject.Find("Player");
if (go == null)
Debug.Log("Player not found");
else
Debug.Log("Player found");
}
}
}โ
- ์์ ์ถ๊ฐํ Logic์ ์ ์์ ์ผ๋ก ์คํ๋์ง ์๋๋ค.
~> ๊ธฐ์กด์ ์ฐ๋ฆฌ๊ฐ ์์ฑํ Server ์๋ฃจ์ ์ Logic์ ๋น๋๊ธฐ๋ก ๋คํธ์ํฌ ํต์ ์ ํ๊ณ ์๋ค.
~> ๋ฐ๋ผ์ Unity๋ฅผ ๊ตฌ๋ํ๋ Main Thread์์ ๋คํธ์ํฌ ํจํท์ ์คํํ๋ ๊ฒ์ด ์๋, Thread Pool์์ Thread๋ฅผ ๊บผ๋ด์
์คํํ๊ณ ์๋ ๊ฒ์ด ๋ฌธ์ ๊ฐ ๋๋ค.
~> Unity๋ ๋ค๋ฅธ Thread์์ ๊ฒ์๊ณผ ๊ด๋ จ๋ ๋ถ๋ถ์ ์ ๊ทผํ์ฌ ์คํํ๋ ๊ฒ์ ์์ฒ์ ์ผ๋ก ์ฐจ๋จํด ๋์๊ธฐ ๋๋ฌธ์
์ ์์ ์ผ๋ก ์คํ๋์ง ์๋ ๊ฒ์ด๋ค.
~> ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ PacketHandler Class๊ฐ Main Thread์์ ์คํ๋๋๋ก ๋ง๋ค๋ฉด ๋๋ค.
(S_ChatHandler์์ Logic์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์๋ Queue์ ์ผ๊ฐ์ ๋ฑ๋กํ, ์ฒ๋ฆฌํ๋ Logic์ ๊ตฌ๋ถ ์ง์ด ์ฌ์ฉ)
Unity Project์ GenPacket Script ์์
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
// ...
public interface IPacket // ์ ๊ทผํ์ ์ public ์ถ๊ฐ
{
ushort Protocol { get; }
void Read(ArraySegment<byte> segment);
ArraySegment<byte> Write();
}
// ...โ
Unity Project์ NetworkManager Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Main Thread์ Background Thread (์ฆ, ๋คํธ์ํฌ๋ฅผ ์ฒ๋ฆฌํ๋ ์ ๋ค๋ผ๋ฆฌ) ๋ PacketQueue๋ผ๋ ํต๋ก๋ฅผ ํตํด ์ํตํ๋ค.
// ~> Background Thread๋ Pakcet์ Pushํ์ฌ ๋ฐ์ด ๋ฃ๊ณ , Main Thread์์๋ Packet์ Popํ์ฌ ์ฒ๋ฆฌํ๋ค.
public class PacketQueue // Component๋ก ์ฌ์ฉํ ๊ฑด X ~> MonoBehaviour ์์ X
{
public static PacketQueue Instance { get; } = new PacketQueue();
Queue<IPacket> _packetQueue = new Queue<IPacket>();
object _lock = new object();
public void Push(IPacket packet)
{
lock (_lock)
{
_packetQueue.Enqueue(packet);
}
}
public IPacket Pop()
{
lock ( _lock)
{
if (_packetQueue.Count == 0)
return null;
return _packetQueue.Dequeue();
}
}
}โ
์ผ๊ฐ ๋ฑ๋ก์ ์ํ Unity Project์ ClientPacketManager Script ์์
using ServerCore;
using System;
using System.Collections.Generic;
class PacketManager
{
#region Singleton
static PacketManager _instance = new PacketManager();
public static PacketManager Instance { get { return _instance; } }
#endregion
PacketManager()
{
Register();
}
Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register()
{
_makeFunc.Add((ushort)PacketID.S_Chat, MakePacket<S_Chat>);
_handler.Add((ushort)PacketID.S_Chat, PacketHandler.S_ChatHandler);
}
// Action<PacketSession, IPacket> Type์ ๋งค๊ฐ๋ณ์์ธ onRecvCallback ์ ์ถ๊ฐ๋ก ์
๋ ฅ ๋ฐ๋๋ค.
public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession, IPacket> onRecvCallback = null)
{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
if (_makeFunc.TryGetValue(id, out func))
{
IPacket packet = func.Invoke(session, buffer);
if (onRecvCallback != null)
onRecvCallback.Invoke(session, packet);
else
HandlePacket(session, packet);
}
}
T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{
T pkt = new T();
pkt.Read(buffer);
return pkt;
}
public void HandlePacket(PacketSession session, IPacket packet)
{
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}
}โ
์ผ๊ฐ ๋ฑ๋ก์ ์ํ Unity Project์ ServerSession Script ์์
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
namespace DummyClient
{
class ServerSession : PacketSession
{
// ...
// Action<PacketSession, IPacket> Type์ ๋งค๊ฐ๋ณ์์ธ onRecvCallback ์ ์ถ๊ฐ๋ก ์
๋ ฅ
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer, (s, p) => PacketQueue.Instance.Push(p));
}
// ...
}
}โ
์ผ๊ฐ ์ฒ๋ฆฌ๋ฅผ ์ํ Unity Project์ NetworkManager Script ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
ServerSession _session = new ServerSession();
void Start()
{
// 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 _session; }, 1);
StartCoroutine("CoSendPacket");
}
void Update()
{
// ์ผ๊ฐ ์ฒ๋ฆฌ
IPacket packet = PacketQueue.Instance.Pop();
if (packet != null)
{
PacketManager.Instance.HandlePacket(_session, packet);
}
}
// 3์ด๋ง๋ค ํจํท์ ๋ณด๋ด๋๋ก (์ฆ, DummyClient์ ์ญํ )
IEnumerator CoSendPacket()
{
while (true)
{
yield return new WaitForSeconds(3.0f);
C_Chat chatPacket = new C_Chat();
chatPacket.chat = "Hello Unity !";
ArraySegment<byte> segment = chatPacket.Write();
_session.Send(segment);
}
}
}โ
PacketFormat Class ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// {0} ํจํท ๋ฑ๋ก
public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;
public class PacketManager
{{
#region Singleton
static PacketManager _instance = new PacketManager();
public static PacketManager Instance {{ get {{ return _instance; }} }}
#endregion
PacketManager()
{{
Register();
}}
Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register()
{{
{0}
}}
public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession, IPacket> onRecvCallback = null)
{{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
if (_makeFunc.TryGetValue(id, out func))
{{
IPacket packet = func.Invoke(session, buffer);
if (onRecvCallback != null)
onRecvCallback.Invoke(session, packet);
else
HandlePacket(session, packet);
}}
}}
T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{{
T pkt = new T();
pkt.Read(buffer);
return pkt;
}}
public void HandlePacket(PacketSession session, IPacket packet)
{{
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}}
}}";
// {0} ํจํท ์ด๋ฆ
public static string managerRegisterFormat =
@" _makeFunc.Add((ushort)PacketID.{0}, MakePacket<{0}>);
_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);";
// {0} ํจํท ์ด๋ฆ/๋ฒํธ ๋ชฉ๋ก
// {1} ํจํท ๋ชฉ๋ก
public static string fileFormat =
@"using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
public enum PacketID
{{
{0}
}}
public interface IPacket
{{
ushort Protocol {{ get; }}
void Read(ArraySegment<byte> segment);
ArraySegment<byte> Write();
}}
{1}
";
// ...
}
}โ
# ์ ๋ํฐ ์ฐ๋ #3
- ์ด๋ฒ ์๊ฐ๊ณผ ๋ค์ ์๊ฐ์ ํตํด Server์์์ Player ์์ฑ ๋ฐ ์์ง์์ ๊ตฌํ ์์ ์ด๋ค.
~> ์ฐ์ Server Logic ์์ ํ, DummyClient Logic์ ์์ ํ ๊ฒ์ด๋ค.
PDL XML ํ์ผ ์์
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name="S_BroadcastEnterGame">
<int name="playerId"/>
<float name="posX"/>
<float name="posY"/>
<float name="posZ"/>
</packet>
<packet name="C_LeaveGame">
</packet>
<packet name="S_BroadcastLeaveGame">
<int name="playerId"/>
</packet>
<packet name="S_PlayerList">
<list name="player">
<bool name="isSelf"/>
<int name="playerId"/>
<float name="posX"/>
<float name="posY"/>
<float name="posZ"/>
</list>
</packet>
<packet name="C_Move">
<float name="posX"/>
<float name="posY"/>
<float name="posZ"/>
</packet>
<packet name="S_BroadcastMove">
<int name="playerId"/>
<float name="posX"/>
<float name="posY"/>
<float name="posZ"/>
</packet>
</PDL>โ
ClientSession Class ์์
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
using System.Net;
namespace Server
{
class ClientSession : PacketSession
{
public int SessionId { get; set; }
public GameRoom Room { get; set; }
public float PosX { get; set; } // x ์ขํ ๊ฐ ์ ์ฅ์ ์ํด ์ถ๊ฐ
public float PosY { get; set; } // y ์ขํ ๊ฐ ์ ์ฅ์ ์ํด ์ถ๊ฐ
public float PosZ { get; set; } // z ์ขํ ๊ฐ ์ ์ฅ์ ์ํด ์ถ๊ฐ
// ...
}
}
GameRoom Class ์์
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class GameRoom : IJobQueue
{
// ...
public void Broadcast(ArraySegment<byte> segment)
{
_pendingList.Add(segment);
}
public void Enter(ClientSession session)
{
// ํ๋ ์ด์ด ์ถ๊ฐ
_sessions.Add(session);
session.Room = this;
// ์ ์
์ํํ
๋ชจ๋ ํ๋ ์ด์ด ๋ชฉ๋ก ์ ์ก
S_PlayerList players = new S_PlayerList();
foreach (ClientSession s in _sessions)
{
players.players.Add(new S_PlayerList.Player()
{
isSelf = (s == session),
playerId = s.SessionId,
posX = s.PosX,
posY = s.PosY,
posZ = s.PosZ
});
}
session.Send(players.Write());
// ์ ์
์ ์
์ฅ์ ๋ชจ๋์๊ฒ ์๋ฆฐ๋ค
S_BroadcastEnterGame enter = new S_BroadcastEnterGame();
enter.playerId = session.SessionId;
enter.posX = 0;
enter.posY = 0;
enter.posZ = 0;
Broadcast(enter.Write());
}
public void Leave(ClientSession session)
{
// ํ๋ ์ด์ด ์ ๊ฑฐ
_sessions.Remove(session);
// ํ๋ ์ด์ด ํด์ฅ์ ๋ชจ๋์๊ฒ ์๋ฆฐ๋ค
S_BroadcastLeaveGame leave = new S_BroadcastLeaveGame();
leave.playerId = session.SessionId;
Broadcast(leave.Write());
}
public void Move(ClientSession session, C_Move packet)
{
// ์ขํ๋ฅผ ๋ฐ๊ฟ์ฃผ๊ณ
session.PosX = packet.posX;
session.PosY = packet.posY;
session.PosZ = packet.posZ;
// ๋ชจ๋์๊ฒ ์๋ฆฐ๋ค
S_BroadcastMove move = new S_BroadcastMove();
move.playerId = session.SessionId;
move.posX = session.PosX;
move.posY = session.PosY;
move.posZ = session.PosZ;
Broadcast(move.Write());
}
}
}โ
Server์ PacketHandler ์์
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
class PacketHandler
{
public static void C_LeaveGameHandler(PacketSession session, IPacket packet)
{
C_LeaveGame chatPacket = packet as C_LeaveGame;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
GameRoom room = clientSession.Room;
room.Push(() => room.Leave(clientSession));
}
public static void C_MoveHandler(PacketSession session, IPacket packet)
{
C_Move movePacket = packet as C_Move;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
GameRoom room = clientSession.Room;
room.Push(() => room.Move(clientSession, movePacket));
}
}โ
DummyClient์ PacketHandler ์์
(๋น๋๊ฐ ํต๊ณผํ ์ ์๋๋ก ํจ์๋ง ๋ง๋ค์ด์ค ๋ฟ, ์ค์ง์ ์ธ ์์ ์ ์ ๋ํฐ ๋ด๋ถ์์ ์ฒ๋ฆฌ)
using DummyClient; using ServerCore; using System; using System.Collections.Generic; using System.Text; class PacketHandler { public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet) { S_BroadcastEnterGame pkt = packet as S_BroadcastEnterGame; ServerSession serverSession = session as ServerSession; } public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet) { S_BroadcastLeaveGame pkt = packet as S_BroadcastLeaveGame; ServerSession serverSession = session as ServerSession; } public static void S_PlayerListHandler(PacketSession session, IPacket packet) { S_PlayerList pkt = packet as S_PlayerList; ServerSession serverSession = session as ServerSession; } public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet) { S_BroadcastMove pkt = packet as S_BroadcastMove; ServerSession serverSession = session as ServerSession; } }โ
DummyClient์ SessionManager ์์
using System;
using System.Collections.Generic;
using System.Text;
namespace DummyClient
{
class SessionManager
{
// ...
Random _rand = new Random();
public void SendForEach()
{
lock (_lock)
{
// ์ฑํ
ํจํท์ด ์๋ ์ด๋ ํจํท์ ๋ณด๋ด๋๋ก ์์
foreach (ServerSession session in _sessions)
{
C_Move movePacket = new C_Move();
movePacket.posX = _rand.Next(-50, 50);
movePacket.posY = 0;
movePacket.posZ = _rand.Next(-50, 50);
session.Send(movePacket.Write());
}
}
}
// ...
}
}
# ์ ๋ํฐ ์ฐ๋ #4
- ์ง๋ ์๊ฐ์ ์ด์ด Server์์์ Player ์์ฑ ๋ฐ ์์ง์์ ๋ง์ ๊ตฌํํ ์์ ์ด๋ค.
Unity Project์ PacketQueue Script ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PacketQueue
{
// ...
public List<IPacket> PopAll()
{
List<IPacket> list = new List<IPacket>();
lock (_lock)
{
while (_packetQueue.Count > 0)
list.Add(_packetQueue.Dequeue());
}
return list;
}
}โ
Unity Project์ NetworkManager Script ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
// ...
public void Send(ArraySegment<byte> sendBuff)
{
_session.Send(sendBuff);
}
// ...
void Update()
{
// ์ผ๊ฐ ์ฒ๋ฆฌ
// ํ๋ ์๋ง๋ค 1๊ฐ์ ์ผ๊ฐ๋ง์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์๋, ํ๋ ์๋ง๋ค ๋ชจ๋ ์ผ๊ฐํ๋๋ก ์์
List<IPacket> list = PacketQueue.Instance.PopAll(); // ์ฆ, Pop()์ด ์๋ PopAll()์ ์คํํ๋๋ก ์์
foreach (IPacket packet in list)
PacketManager.Instance.HandlePacket(_session, packet);
}
}โ
Unity Project์ Player Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public int PlayerId { get; set; }
}โ
Unity Project์ MyPlayer Script ์์ฑ
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyPlayer : Player
{
NetworkManager _network;
void Start()
{
StartCoroutine("CoSendPacket");
_network = GameObject.Find("NetworkManager").GetComponent<NetworkManager>();
}
void Update()
{
}
IEnumerator CoSendPacket()
{
while (true)
{
yield return new WaitForSeconds(0.25f);
C_Move movePacket = new C_Move();
movePacket.posX = UnityEngine.Random.Range(-50, 50);
movePacket.posY = 0;
movePacket.posZ = UnityEngine.Random.Range(-50, 50);
_network.Send(movePacket.Write());
}
}
}โ
Unity Project์ PlayerManager Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerManager // ๊ธฐ์ํ๋ ๊ฒ์ด ์๋ ๋ฐ์ดํฐ๋ง ๋ค๊ณ ์๋๋ก MonoBehaviour ์์ X
{
MyPlayer _myPlayer;
Dictionary<int, Player> _players = new Dictionary<int, Player>();
public static PlayerManager Instance { get; } = new PlayerManager();
public void Add(S_PlayerList packet)
{
Object obj = Resources.Load("Player");
foreach (S_PlayerList.Player p in packet.players)
{
GameObject go = Object.Instantiate(obj) as GameObject;
if (p.isSelf)
{
MyPlayer myPlayer = go.AddComponent<MyPlayer>();
myPlayer.PlayerId = p.playerId;
myPlayer.transform.position = new Vector3(p.posX, p.posY, p.posZ);
_myPlayer = myPlayer;
}
else
{
Player player = go.AddComponent<Player>();
player.PlayerId = p.playerId;
player.transform.position = new Vector3(p.posX, p.posY, p.posZ);
_players.Add(p.playerId, player);
}
}
}
public void Move(S_BroadcastMove packet)
{
if (_myPlayer.PlayerId == packet.playerId)
{
_myPlayer.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
}
else
{
Player player = null;
if (_players.TryGetValue(packet.playerId, out player))
{
player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
}
}
}
public void EnterGame(S_BroadcastEnterGame packet)
{
if (packet.playerId == _myPlayer.PlayerId)
return;
Object obj = Resources.Load("Player");
GameObject go = Object.Instantiate(obj) as GameObject;
Player player = go.AddComponent<Player>();
player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
_players.Add(packet.playerId, player);
}
public void LeaveGame(S_BroadcastLeaveGame packet)
{
if (_myPlayer.PlayerId == packet.playerId)
{
GameObject.Destroy(_myPlayer.gameObject);
_myPlayer = null;
}
else
{
Player player = null;
if (_players.TryGetValue(packet.playerId, out player))
{
GameObject.Destroy(player.gameObject);
_players.Remove(packet.playerId);
}
}
}
}โ
Unity Project์ PacketHandler Script ์์
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
class PacketHandler
{
public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet)
{
S_BroadcastEnterGame pkt = packet as S_BroadcastEnterGame;
ServerSession serverSession = session as ServerSession;
PlayerManager.Instance.EnterGame(pkt);
}
public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet)
{
S_BroadcastLeaveGame pkt = packet as S_BroadcastLeaveGame;
ServerSession serverSession = session as ServerSession;
PlayerManager.Instance.LeaveGame(pkt);
}
public static void S_PlayerListHandler(PacketSession session, IPacket packet)
{
S_PlayerList pkt = packet as S_PlayerList;
ServerSession serverSession = session as ServerSession;
PlayerManager.Instance.Add(pkt);
}
public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet)
{
S_BroadcastMove pkt = packet as S_BroadcastMove;
ServerSession serverSession = session as ServerSession;
PlayerManager.Instance.Move(pkt);
}
}โ