[ ์น์ 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 Classusing 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 Classusing 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 Classpublic class ComputerOnCommand implements Command { private Computer computer; public ComputerOnCommand(Computer computer) { this.computer = computer; } @Override public void execute() { computer.turnOn(); } }โ
ComputerOffCommand Classpublic class ComputerOffCommand implements Command { private Computer computer; public ComputerOffCommand(Computer computer) { this.computer = computer; } @Override public void execute() { computer.turnOff(); } }โ
Computer Classpublic class Computer { public void Computer() {} public void turnOn() { System.out.println("์ปดํจํฐ ์ ์ ์ผ์ง"); } public void turnOff() { System.out.println("์ปดํจํฐ ์ ์ ๊บผ์ง"); } }โ
Button Classpublic 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); } }โ