이번 포스팅은 지난 포스팅에서 다뤘던 직렬화를 자동화 해보겠다.
Packet Generator 1
일단 우리가 생각해보면 Session에서 Packet 클래스를 정의한채 사용하고 있었다. 이를 하나로 묶고 패킷에 대한 정의를 하기 위해 새로운 프로젝트를 만들것이다.
그리고 패킷에 대한 정의를 하기 위해 Json 파일이나 Xml 파일을 사용한다. 필자는 Xml이 더 다루기가 쉽다고 생각해 Xml 파일을 사용한다.
우리가 이전에 새용했던 PlayerInfoReq 패킷은 플레이어id와 플레이어 이름 그리고 각종 스킬에 대해 정보를 담고 있었다. 이를 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에서 이를 파싱해주면된다. c++이라면 제공되는 라이브러리가 딱히 없어 어떤 오픈소스를 끌어다가 사용해야하지만 친절하게도(?) c#같은 경우는 xml 파일을 읽을 수 있는 라이브러리가 존재한다.
using System;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true, // 주석 무시
IgnoreWhitespace = true, // 스페이스바 무시
};
// 파싱
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
{
r.MoveToContent();
while (r.Read())
{
// xml 파일에서 depth가 1인 것
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
}
}
}
public static void ParsePacket(XmlReader r)
{
if (r.NodeType == XmlNodeType.EndElement)
return;
if (r.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = r["name"];
if (string.IsNullOrEmpty(packetName))
{
Console.WriteLine("Packet without name");
return;
}
ParseMembers(r);
}
private static void ParseMembers(XmlReader r)
{
string packetName = r["name"];
int depth = r.Depth + 1;
while (r.Read())
{
if (r.Depth != depth)
break;
string memberName = r["name"];
if (string.IsNullOrEmpty(memberName))
{
Console.WriteLine("Member without name");
return;
}
string memberType = r.Name.ToLower();
switch (memberType)
{
case "bool":
case "byte":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
case "string":
case "list":
default:
break;
}
}
}
}
}
xml 파일에서 depth를 추적하며 우리가 원하는 타입에 따른 case문을 통해 패킷을 처리할 것이다. 이럴때 유용하게 쓰이는 방법이 있다.
포멧을 만드는 것이다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PacketGenerator
{
class PacketFormat
{
// {0} 패킷 이름
// {1} 멤버변수들
// {2} 멤버변수 read
// {3} 멤버변수 write
public static string packetFormat =
@"
class {0}
{{
{1}
public void Read(ArraySegment<byte> segment)
{{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}
public ArraySegment<byte> Write()
{{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.{0});
count += sizeof(ushort);
{3}
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}}
}}
";
// {0} 변수 형식
// {1} 변수 이름
public static string memberFormat =
@"public {0} {1}";
// {0} 변수 이름
// {1} To~ 변수 형식
// {2} 변수 형식
public static string readFormat =
@"this.{0} = BitConverter.{1}(s.Slice(count, s.Length - count));
count += sizeof({2});";
// {0} 변수 이름
public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(s.Slice(count, {0}Len));
count += {0}Len;";
// {0} 변수 이름
// {1} 변수 형식
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.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, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), {0}Len);
count += sizeof(ushort);
count += {0}Len;";
}
}
일정한 포멧을 만들어 {0} 의 값들을 우리가 원하는 값들로 바꿔치기를 해서 자동화를 하는 것이다. 아직은 List를 구현하지 않았다.
PacketGenerator 2
지금 위의 과정을 왜 했는지 이해가 잘 가지 않을 수 있는데 이는 코드를 다 짜고 흐름을 보다보면 이해가 갈 것이다.
형식을 쌓고 xml 파일을 읽어 새로운 클래스를 제작하는 툴을 만들고 있다고 생각하면 된다.
using System;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static string genPacket;
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true, // 주석 무시
IgnoreWhitespace = true, // 스페이스바 무시
};
// 파싱
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
{
r.MoveToContent();
while (r.Read())
{
// xml 파일에서 depth가 1인 것
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
}
File.WriteAllText("GenPackets.cs", genPacket);
}
}
public static void ParsePacket(XmlReader r)
{
if (r.NodeType == XmlNodeType.EndElement)
return;
if (r.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = r["name"];
if (string.IsNullOrEmpty(packetName))
{
Console.WriteLine("Packet without name");
return;
}
Tuple<string, string, string> t = ParseMembers(r);
genPacket += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}
private static Tuple<string, string, string> ParseMembers(XmlReader r)
{
string packetName = r["name"];
string memberCode = "";
string readCode = "";
string writeCode = "";
int depth = r.Depth + 1;
while (r.Read())
{
if (r.Depth != depth)
break;
string memberName = r["name"];
if (string.IsNullOrEmpty(memberName))
{
Console.WriteLine("Member without name");
return null;
}
if (string.IsNullOrEmpty(memberCode) == false)
memberCode += Environment.NewLine;
if (string.IsNullOrEmpty(readCode) == false)
readCode += Environment.NewLine;
if (string.IsNullOrEmpty(writeCode) == false)
writeCode += Environment.NewLine;
string memberType = r.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":
break;
default:
break;
}
}
memberCode = memberCode.Replace("\n", "\n\t");
readCode = readCode.Replace("\n", "\n\t\t");
writeCode = writeCode.Replace("\n", "\n\t\t");
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}
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 "";
}
}
}
}
이제 List를 파싱해보겠다.
using System;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static string genPacket;
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true, // 주석 무시
IgnoreWhitespace = true, // 스페이스바 무시
};
// 파싱
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
{
r.MoveToContent();
while (r.Read())
{
// xml 파일에서 depth가 1인 것
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
}
File.WriteAllText("GenPackets.cs", genPacket);
}
}
public static void ParsePacket(XmlReader r)
{
if (r.NodeType == XmlNodeType.EndElement)
return;
if (r.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = r["name"];
if (string.IsNullOrEmpty(packetName))
{
Console.WriteLine("Packet without name");
return;
}
Tuple<string, string, string> t = ParseMembers(r);
genPacket += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}
private static Tuple<string, string, string> ParseMembers(XmlReader r)
{
string packetName = r["name"];
string memberCode = "";
string readCode = "";
string writeCode = "";
int depth = r.Depth + 1;
while (r.Read())
{
if (r.Depth != depth)
break;
string memberName = r["name"];
if (string.IsNullOrEmpty(memberName))
{
Console.WriteLine("Member without name");
return null;
}
if (string.IsNullOrEmpty(memberCode) == false)
memberCode += Environment.NewLine;
if (string.IsNullOrEmpty(readCode) == false)
readCode += Environment.NewLine;
if (string.IsNullOrEmpty(writeCode) == false)
writeCode += Environment.NewLine;
string memberType = r.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> t = ParseList(r);
memberCode += t.Item1;
readCode += t.Item2;
writeCode += t.Item3;
break;
default:
break;
}
}
memberCode = memberCode.Replace("\n", "\n\t");
readCode = readCode.Replace("\n", "\n\t\t");
writeCode = writeCode.Replace("\n", "\n\t\t");
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}
public static Tuple<string, string, string> ParseList(XmlReader r)
{
string listName = r["name"];
if (string.IsNullOrEmpty(listName))
{
Console.WriteLine("List without name");
return null;
}
Tuple<string,string,string> t = ParseMembers(r);
string memberCode = string.Format(PacketFormat.memberListFormat,FirstCharToUpper(listName),FirstCharToLower(listName)
,t.Item1, t.Item2,t.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 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 "";
}
}
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);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PacketGenerator
{
class PacketFormat
{
// {0} 패킷 이름
// {1} 멤버변수들
// {2} 멤버변수 read
// {3} 멤버변수 write
public static string packetFormat =
@"
class {0}
{{
{1}
public void Read(ArraySegment<byte> segment)
{{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}
public ArraySegment<byte> Write()
{{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.{0});
count += sizeof(ushort);
{3}
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}}
}}
";
// {0} 변수 형식
// {1} 변수 이름
public static string memberFormat =
@"public {0} {1};";
// {0} 리스트 이름 {대문자}
// {1} 리스트 이름 {소문자}
// {2} 멤버변수들
// {3} 멤버변수 read
// {4} 멤버변수 write
public static string memberListFormat =
@"
public struct {0}
{{
{2}
public void Read(ReadOnlySpan<byte> s, ref ushort count)
{{
{3}
}}
public bool Write(Span<byte> s, ref ushort count)
{{
bool success = true;
{4}
return success;
}}
}}
public List<{0}> skills = new List<{0}>();";
// {0} 변수 이름
// {1} To~ 변수 형식
// {2} 변수 형식
public static string readFormat =
@"this.{0} = BitConverter.{1}(s.Slice(count, s.Length - count));
count += sizeof({2});";
// {0} 변수 이름
public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(s.Slice(count, {0}Len));
count += {0}Len;";
// {0} 리스트 이름 [대문자]
// {1} 리스트 이름 [소문자]
public static string readListFormat =
@"this.{1}s.Clear();
ushort {1}len = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
for (int i = 0; i < {1}len; i++)
{{
{0} {1} = new {0}();
{1}.Read(s, ref count);
{1}s.Add({1});
}}";
// {0} 변수 이름
// {1} 변수 형식
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.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, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), {0}Len);
count += sizeof(ushort);
count += {0}Len;";
// {0} 리스트 이름 [대문자]
// {1} 리스트 이름 [소문자]
public static string writeListFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach ({0} {1} in {1}s)
success &= {1}.Write(s, ref count);
";
}
}
List를 하나의 클래스를 파싱한다고 생각하면서 계층구조로 클래스 안에 있는 List 안에 있는 데이터들을 먼저 member화 한후에 그 값을 member화해서 일반화 한다.
PacketGenerator 3
우리가 만들어낸 GenPacket.cs 파일을 클라든 서버든 참조해서 사용해야한다. 지금까지 제작한 것에 문제는 바로 PacketID의 enum값과 using~~ 가 없어서 파일이 제대로 작동하지 않는다. 이를 수정해줘야한다.
// {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}";
패킷 포멧에 이 부분을 맨 처음에 추가해준다. 그리고 이를 파싱하기 위해
string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPacket);
다음과 같은코드를 통해 파일을 집어넣는다.
최종적으로 생성된 GenPacket.cs파일은 다음과 같다.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
public enum PacketID
{
PlayerInfoReq = 1
Player = 2
}
class PlayerInfoReq
{
public byte testByte;
public long playerId;
public string name;
public struct Skill
{
public int id;
public short level;
public float duration;
public void Read(ReadOnlySpan<byte> s, ref ushort count)
{
this.id = BitConverter.ToInt32(s.Slice(count, s.Length - count));
count += sizeof(int);
this.level = BitConverter.ToInt16(s.Slice(count, s.Length - count));
count += sizeof(short);
this.duration = BitConverter.ToSingle(s.Slice(count, s.Length - count));
count += sizeof(float);
}
public bool Write(Span<byte> s, ref ushort count)
{
bool success = true;
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.id);
count += sizeof(int);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.level);
count += sizeof(short);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.duration);
count += sizeof(float);
return success;
}
}
public List<Skill> skills = new List<Skill>();
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.testByte = segment.Array[segment.Offset + count];
count += sizeof(byte);
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
this.skills.Clear();
ushort skilllen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
for (int i = 0; i < skilllen; i++)
{
Skill skill = new Skill();
skill.Read(s, ref count);
skills.Add(skill);
}
}
public ArraySegment<byte> Write()
{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);
count += sizeof(ushort);
segment.Array[segment.Offset + count] = this.testByte;
count += sizeof(byte);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach (Skill skill in skills)
success &= skill.Write(s, ref count);
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
class Player
{
public int playerName;
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerName = BitConverter.ToInt32(s.Slice(count, s.Length - count));
count += sizeof(int);
}
public ArraySegment<byte> Write()
{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.Player);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerName);
count += sizeof(int);
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
'Unity > 온라인 RPG' 카테고리의 다른 글
[게임 서버] JobQueue - 서버 과부화를 줄이기 위한 패킷 처리 방법 (0) | 2024.02.05 |
---|---|
[게임 서버] Job Queue - 채팅 테스트 (0) | 2024.02.01 |
[게임 서버] 패킷 직렬화 - Serialization (1) | 2024.01.24 |
[게임 서버] 네트워크 프로그래밍 - Connector, TCP? UDP?, Buffer와PacketSession (1) | 2024.01.22 |
[게임 서버] 네트워크 프로그래밍 - Listener와 Session (0) | 2024.01.19 |