03_07_패킷 직렬화_PacketGenerator #2_코드 자동 생성, List 추가
지난 시간에 작업한 코드를 이용해서 주어진 입력을 기반으로 패킷을 생성하고, 생성된 패킷을 GenPackets.cs 파일에 저장해 그 코드를 이용해 실제로 작동하는시 실험 해 봅니다.
지난 시간에 안해준 List의 코드를 만드는 부분도 작성해 봅니다.
지난 시간에 이걸 왜 했는지 아직 와닿지 않을 수 있는데 코드를 만들어 보면 쉽게 이해가 갈 것이다.
다시 프로그램으로 돌아와서 결국에는 우리가 하고 싶은 건 무엇이냐
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())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
File.WriteAllText("GenPackets.cs", "");
}**
}
여기 파일을 파싱을 해줘가지고, 여기서 하나하나씩 공간을 만들어 준 다음에 걔를 파일에다가
File.WriteAllText(); 라는 기능을 이용 해가지고, 여기다가 파일로 하나로 만들어 주는거. 파일 이름을 GenPackets.cs로 만들어 줄거고. 그 옆 “”여기다가 자동완성한 거를 넣어주면 된다는 얘기가 되는데
이거를 코드 위쪽에다가 계속 저장하고 있을 거야.
static string genPackets;
실시간으로 계속 만들어지는 packet string을 여기다가 밀어넣어 주는 거. 일단 정확히 완성이 되면 어떤 내용이 될지 모르지만, 테스트하기 위해서
**File.WriteAllText("GenPackets.cs",** genPackets**);**
정확히 완성이 되면 어떤 내용이 될지 모르겠지만 일단 테스트 하기 위해서 이렇게 넣어 주도록 할거야.
namespace PacketGenerator
{
class Program
{
**static string genPackets;**
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())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
**File.WriteAllText("GenPackets.cs", genPackets);**
}
}
안에서 ParsePacket을 하는 동안에 genPackets에다가 내용을 덕지덕지 추가를 하면 된다는 얘기가 된다.
그리고 ParsePacket에 가보면
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);
genPackets += string.Format(PacketFormat.packetFormat,
packetName, "", "", "");
}
ParseMembers에다 자신의 멤버를 하나하나 파싱하고 있었어.
만약에 ParseMembers가 끝났다고 가정을 하면, genPackets에다가 이어서 만들어 줘야 하는데 string.Format에 가보면, Format을 먼저 입력을 한 다음에 그 다음에 하나하나씩 입력을 하는 부분이 있는데
일단 첫번째 인자인 Packet의 경우 PacketFormat.packetFormat 이 아이로 준비하고 있었어.
PacketGenerator의
public static string packetFormat =
이 아이로 정의를 하고 있었어.
그리고 Format에 값을 4개를 더 넣어 줘야 하는데 각각의 의미가
// {0} 패킷 이름
// {1} 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
이거였어. 이걸 만들어서 넣어 줘야 한다는 얘기가 된다.
packetName은 바로 위에 있으니 넣어줄 수 있는데 나머지 애들은 쉽지 않다.
어떻게든 ParseMembers를 통해서 나머지 1,2,3 애들을 완성을 한 다음에 반환을 해줘야 하는데
그 반환 하는 걸 ParseMemebers에서 받아 오도록 할거야.
public static void ParseMembers(XmlReader r)
에서
public static Tuple<string, string, string> ParseMembers(XmlReader r)
이런식으로 1,2,3번을 얘가 만들어줘서 뱉어주는 방식으로 작업 해주자.
return 타입이 생겼으므로 null체크에서 null이면 그냥 return;만 했는데 **return null;**으로 하게 수정하고,
Tuple<string, string, string> t = ParseMembers(r);
genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
이어가지고 여기 ParseMembers 함수를 계속ㅠ 만들어 보자.
헷갈리지 않도록 1,2,3주석도 갖고 오자.
// {1} 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
Tuple<string, string, string> 1번, 2번, 3번이 각각 무엇을 반환 하는지를 얘를 보고 맞춰 주도록 하자.
여기도 마찬가지로 return 할 것을 내부에서 만들어 주도록 할거야.
string memberCode = "";
string readCode = "";
string writeCode = "";
이 코드들을 내부에서 만들어 주다가
while(r.Read())
{
최종적으로 whlie문에서 벗어나 끝났다 하면
while문 아래다가
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
Tuple은 별거는 아니고, 세개를 한방에 묶어서 보낼 수 있는 클래스라고 생각하면 된다.
// {1} 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
public 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 "byte":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
case "string":
case "list":
break;
default:
break;
}
}
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}
이렇게 일단 인터페이스를 맞춰 줬으니까, 이어서 해야 하는 것은
switch문에서 값에 따라서 채워주면 된다.
그리고 ParseMembers의
while(r.Read())
여기 들어 올 때 마다 사실은 하는 작업이 PDL의 한줄 한줄씩 긁어오는 작업이니까, 한줄씩 띄는 코드를 넣어주도록 할거야. 이게 없으면 코드가 생성 될 때 줄 바꾸는 거 없이 다 한줄로 나오게 될거다.
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 "byte":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
break;
case "string":
break;
case "list":
break;
default:
break;
}
이 부분도 double에서 자르도록 할거야.
string을 제외한 bool~double 같은 고정 사이즈 애들은 PacketFormat에서 처음으로 맞춰준
memberFormat, readFormat, writeFormat으로 만들어 줄 수 있었으니까 다음과 같이 써준다.
case "double":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readFormat, memberName, "", memberType);
break;
readFormat의 세번째 인자 To~ 변수 형식 부분은 아직 어떻게 할지 만들어 줘야 한다.
readCode의 경우 PacketFormat에서 보면
// {0} 변수 이름
// {1} To~ 변수 형식
// {2} 변수 형식
public static string readFormat =
@"this.{0} = BitConverter.{1}(s.Slice(count, s.Length - count));
count += sizeof({2});";
To~ 변수 형식 이 부분을 따로 함수로 빼주도록 한다.
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 "";
}
}
이렇게 해주고,
case "double":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
break;
이렇게 readCode를 채워준다.
이어서 writeCode는 PacketFormat에서 보면
// {0} 변수 이름
// {1} 변수 형식
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.{0});
count += sizeof({1});";
변수 이름과 변수 형식으로 이루어져 있으니까,
writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
이렇게 넣어주면 된다.
string memberType = r.Name.ToLower();
switch(memberType)
{
case "bool":
case "byte":
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":
break;
case "list":
break;
default:
break;
}
이렇게 완성이 됐고, 우리고 list는 아직 안만들어 줬지만, string 까지는 만들어 줬어. 이 부분을 string에 복사를 한 다음
case "string":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readStringFormat, memberName);
writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
break;
결국 우리가 해주고 있는 작업은 계속 한줄 한줄
<?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>
얘네들을 파싱을 하면서
playerId, name을 긁은 다음 memberCode, readCode, writeCode에다가 신나게 덧붙인 다음에
완성이 된 다음에
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
이렇게 전달을 해주는 부분이 된다.
일단 여기까지 실행을 해봐서 어떤 식으로 출력이 되는지 보자.
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);
genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}
genPackets를 여기서 추출한 다음에,
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())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
File.WriteAllText("GenPackets.cs", genPackets);
}
}
추출한 내용을 GenPackets.cs로 만들어 주고 있었으니까 실행을 한 다음에
파일 탐색기에서 폴더 열기를 하면
실행파일이 있는 경로로 가서 보면
GenPackets.cs 가 완성이 되어 있는데 열어 보면
class PlayerInfoReq
{
public long playerId
public string name
public List<SkillInfo> skills = new List<SkillInfo>();
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
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;
}
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);
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, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
얼추 내용은 나오는데 몇 가지 빠진 부분이 있어.
일단은 멤버 변수에 ;이 빠져 있는 것을 볼 수 있다. 다시 PacketFormat.cs로 가서,
// {0} 변수 형식
// {1} 변수 이름
public static string memberFormat =
@"public {0} {1};";
이렇게 ;를 추가해준다.
다시 실행을 해보면
class PlayerInfoReq
{
public long playerId;
public string name;
이제는 정상적으로 되어 있는 것을 볼 수 있다.
나머지 부분은 일단은 정상적으로 출력이 되고 있기는 한데, 정렬이 예쁘게 안되고 있는 것을 볼 수 있다.
정렬을 해주기 위해서 Program.cs의 ParseMembers에서 끝내기 전에 tab을 맞춰 주도록 하자.
memberCode = memberCode.Replace("\\n", "\\n\\t");
이걸로 변수를 강제로 칸을 띄어주게 할 거고,
readCode = readCode.Replace("\\n", "\\n\\t\\t");
writeCode = writeCode.Replace("\\n", "\\n\\t\\t");
read코드, write코드의 경우도 이렇게 탭을 두번 해 줄 것이다.
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);
}
이 상태에서 다시 한번 실행을 해보면,
class PlayerInfoReq
{
public long playerId;
public string name;
public List<SkillInfo> skills = new List<SkillInfo>();
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
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;
}
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);
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, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
깔끔하게 잘 나오는 것을 볼 수 있다.
이어서 우리가 아직 안해줬던 List 부분도 똑같이 넣어주면 된다.
List는 string과 다르게 멤버 포멧 부터 다르다.
serverSession에서 보면 일반적인 형식이나 string은 그냥 간단하게 정의가 되었는데
public long playerId;
public string name;
List 같은 경우에는 일단 struct를 만들어 주고, 이어서
public List<SkillInfo> skills = new List<SkillInfo>();
이렇게 List를 만들어 줘야 똑같이 출력이 될거야.
PacketFormat.cs로 가서 만들어 주자.
public static string memberListFormat =
@"
";
이 다음에 ServerSession의 struct SkillInfo와 List<SkillInfo> skills를 생성하는 부분을 통으로 복붙 해주자.
그리고 소괄호는 두개 짜리로 바꿔줘야 한다.
그 다음에 여기서 무엇이 필요할지를 곰곰히 생각을 해봐야 한다.
// {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}> {1}s = new List<{0}>();";
Read랑 Write의 경우도 List는 다르게 처리해줘야 한다.
PlayerInfoReq클래스의 Read함수의 skill list를 복붙을 한 뒤 시작한다.
public static string readListFormat =
@"skills.Clear();
ushort skillLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
for (int i = 0; i < skillLen; i++)
{
SkillInfo skill = new SkillInfo();
skill.Read(s, ref count);
skills.Add(skill);
}";
여기서 날릴 부분은 날려야 한다. 소괄호를 2개로 바꿔주고 시작하자.
// {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});
}}";
Write 하는 부분도
ServerSession의 list의 Write하는 부분은
// skill list
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach (SkillInfo skill in skills)
success &= skill.Write(s, ref count);
이런 식으로 조금 달랐으니까,
얘도 마찬가지로 PacketFormat에
// {0} 리스트 이름 [대문자]
// {1} 리스트 이름 [소문자]
public static string writeListFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)this.{1}s.Count);
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
success &= {1}.Write(s, ref count);";
이런식으로 넣어주면 모든 준비가 완료된다.
다시 Program으로 가서 case “list”: 부분을 채워주기 시작해야 한다.
방식이 다르니까
case "list":
ParseList(r);
break;
이렇게 빼주도록 한다.
public static Tuple<string, string, string> ParseList(XmlReader r)
{
string listName = r["name"];
if(string.IsNullOrEmpty(listName))
{
Console.WriteLine("List without name");
return null;
}
}
// {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}> {1}s = new List<{0}>();";
{2}{3}{4}같은 경우는 똑같았기 때문에 ParseMember를 재사용할 수 있어.
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,
)
}
근데 memberListFormat은
// {0} 리스트 이름 [대문자]
// {1} 리스트 이름 [소문자]
// {2} 멤버 변수
// {3} 멤버 변수 Read
// {4} 멤버 변수 Write
이렇게 5개를 갖고 있었어.
대문자랑 소문자 이렇게 각각 구분해야 하는데 아직 그런 기능이 없으니까 아래다가 하나 파주도록 하자.
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);
}
이렇게 하면 말 그대로 모든 애들이 소문자가 되는 거. 첫 문자만 소문자로 바꿔준 거.
이렇게 두 개를 마련해 줬으니까 다시한번 올려서 ParseList에서 넣어 줄 수 있게 되는데
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);
}
이렇게 해주면
// {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}> {1}s = new List<{0}>();";
여깄는 멤버 코드는 완성이 될 거고,
그 다음에 read코드도 만들어 줘야 하는데
얘도 마찬가지로 대문자, 소문자를 받고 있었으니까 그대로 복붙을 해주면 된다.
string readCode = string.Format(PacketFormat.readListFormat,
FirstCharToUpper(listName),
FirstCharToLower(listName));
이렇게 됐고, 얘도 그대로 통으로 복사를 한 다음에
string writeCode = string.Format(PacketFormat.writeListFormat,
FirstCharToUpper(listName),
FirstCharToLower(listName));
이렇게 writeCode가 완성이 된다.
이렇게 각각을 다 만들어 줬으니까 이제 얘를 최종적으로 Tuple을 이용해서
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);
}
case "list":
ParseList(r);
break;
그럼 ParseList를 했으면, List에 대한 코드도 어느정도 완성이 된 채로 여기서 뱉어주게 될텐데
case "list":
Tuple<string, string, string> t = ParseList(r);
break;
이렇게 받아준 다음에
case "list":
Tuple<string, string, string> t = ParseList(r);
memberCode += t.Item1;
readCode += t.Item2;
writeCode += t.Item3;
break;
이렇게 추가를 해주면 된다
여기까지 잘 되는지 확인을 해보자.
<?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>
PDL의 list 부분이 잘 만들어졌나 보면
GenPackets.cs를 보면
class PlayerInfoReq
{
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 List<SkillInfo> skills = new List<SkillInfo>();
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
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);
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)this.skill.Count);
count += sizeof(ushort);
foreach (Skill skill in this.skills)
success &= skill.Write(s, ref count);
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
어느정도 잘 만들어 진 것을 알 수 있다.
딱히 문제가 없어 보이는데 이게 정말로 잘 실행이 되는지 테스트 해보기 위해 GenPacket의 코드를 다 복사 해서 ClientSession과 ServerSession에서 사용하고 있었으니까 여기에 강제로 붙여넣기를 해보자. ServerSession에는 PacketID 전 class PlayerInfoReq에 붙여넣기를 한다. ClientSession에는 PacketID 이전까지 선택해서 덮어 붙여넣기 한다.
여기까지 했으면 테스트를 하기 위해서 다시 솔루션의 속성에 가서, 여러 개의 시작 프로젝트로 설정을 변경하고, DummyClient랑 Server로 맞춰준 다음에 다시 빌드를 해보면,
빌드가 실패를 했는데, 에러 메시지를 보면 SkillInfo가 PlayerInfoReq형식에 없다고 나온다. ServerSession, ClientSession의 SkillInfo를 Skill로 바꿔준다.
실행을 하면 기존과 마찬가지로
모든 정보들이 정상적으로 보내지는 걸 볼 수 있다.
일단은 좀 더 실험을 해 봐야겠지만 PacketGenerator가 어느정도 제대로 실행되고 있다는 걸 알 수 있다.
XML 타입으로 패킷을 간단하게 정리 한 다음에
PacketGenerator를 돌려가지고 이런 코드를 자동화 해서 사용을 하면 된다는 얘기
작업한 코드
PacketFormat.cs
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> segment)
{{
ushort count = 0;
ReadOnlySpan<byte> s = new Span<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}> {1}s = 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)this.{1}s.Count);
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
success &= {1}.Write(s, ref count);";
}
}
PacketGenerator의 Program.cs
using System;
using System.IO;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static string genPackets;
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())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
File.WriteAllText("GenPackets.cs", genPackets);
}
}
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);
genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}
// {1} 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
public 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 "byte":
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);
}
}
}
Here's a high-level overview of the program:
- The program starts with the Main method, where it sets up an XmlReader with some settings and opens the "PDL.xml" file for reading.
- It then iterates through the XML nodes to find the packet elements. For each packet element, the ParsePacket method is called.
- The ParsePacket method checks the current XML node's name and extracts the packet name. If the packet name is not empty, it proceeds to parse the members of the packet by calling the ParseMembers method.
- The ParseMembers method iterates through the XML nodes of the current packet and processes different member types. It generates code for each member's declaration, read method, and write method. If a member is a list, it calls the ParseList method.
- The ParseList method parses the list members and generates code for the list member's declaration, read method, and write method.
- Helper methods like ToMemberType, FirstCharToUpper, and FirstCharToLower are used to generate the correct code syntax for different member types and to manipulate strings.
- The generated code for all packets is combined and written to the "GenPackets.cs" file.
확인 테스트
Q1. 이번 시간에 작업한 내용을 나열해 보세요.
->PacketGenerator의 Program.cs 파일에 static string genPackets;를 선언하여 자동 생성된 코드를 저장하게 한다. 이곳에 자동 완성된 코드를 넣을 것이고, File.WriteAlltext를 이용해 GenPackets.cs에 그 코드들을 넣어줄 것이다.
-> Main에서 호출되는 ParsePacket 메서드에서 ParseMembers(r);를 한 다음에 genPackets에 string.Format로 생성한 코드를 더해준다. string.Format의 첫번째 인자로는 PacketFormat.packetFormat을 두번째 인자로는 packetName을 넣는다. 나머지 3개는 멤버 변수, 멤버 변수 Read, 멤버 변수 write인데 아직 넣을 수 없다.
->ParseMembers의 반환 값을 통해 나머지 3개를 받기 위해 ParseMembers의 반환값을 void에서 Tuple<string, string, string>으로 수정한다.
->ParseMembers 메서드에 string memberCode, readCode, writeCode를 선언하고 ""로 초기화 한다 while(r.Read())안에서 각 코드의 내용이 있다고 햐면 Environmemt.NewLine을 통해 줄을 바꿔준다. memberType을 r.Name.ToLower()로 뽑아내 switch문으로 각 타입을 case별로 나눠준다. while문을 벗어나면 Tuple(string, string, string>(memberCode, readCode, writeCode)를 return 한다.
-> ParseMembers의 switch문에서 bool~double, string, list에서 각각 break를 해준다. case bool~double의 경우 readCode를 추가할 때 To~변수 형식이 필요한데 이를 위해 ToMemberType 함수를 정의한다. memberType을 string으로 받아 switch문으로 해당 case에 해당하는 string을 리턴한다. 이렇게 case "bool"~"double":에 memberCode, readCode, writeCode에 string.Format을 이용해 코드를 생성해 +=로 넣게 한다. case "string":도 string.Format으로 생성 코드를 생성해 memberCode, readCode, writeCode에 더한다.
->ParsePacket에서 ParseMembers를 호출해 return된 값을 받아서 string.Format(PacketFormat.packetFormat,...)의 나머지 3개의 값을 채워 코드를 생성하고 genPakcets에 더해 준다.
->실행해 보면 실행파일이 있는 경로에 GenPackets.cs파일이 있다.
-> 멤버 변수에 ;가 빠져 있어서 PacketFormat.cs에서 수정해준다. ParseMembers에서 정렬을 위해 memberCode, readCode, writeCode에 Replace를 이용해 \t를 추가해준다.
-> List도 똑같이 해준다.
-> PacketFormat.cs로 가서 public static string memberListFormat =@"";안에 ServerSession의 struct SkillInfo와 List<SkillInfo> skills를 생성하는 부분을 복붙한다. 리스트 이름(대문자), 소문자, 멤버 변수, Read, Write부분을 {0}~{4}로 바꿔준다. List의 read의 경우 public static string readListFormat =@""의 안에 Read함수의 skill list 부분을 복붙해 작업한다. List의 Write의 경우 public static string writeListFormat =@""의 안에 Write함수 부분을 복붙해 작업한다.
-> Program에 가서 case "list": 부분에 필요한 ParseList라는 함수를 정의해준다.일단 listName을 null 체크 하고, 멤버 변수, 멤버Read, 멤버Write는 똑같기 때문에 ParseMembers(r)를 ParseList에서 호출한다. 첫문자를 대문자로 바꾸는 함수 FirstCharToUpper와 반대인 FirstCharToLower를 만든다. ParseList에서 string.Format을 이용하여 memberListFormat의 코드를 생성하게 한다. 이어서 readCode와 WriteCode도 생성하는 코드를 만들어어 준다. Tuple로 memberCode, readCode,writeCode를 생성해 return한다.
->ParseMembers에서 case "list":에서 parseList(r)를 호출하고 return 값을 Tuple변수 r로 받습니다.그리고 memberCode, readCode, wirteCode에 각 t.Item1, 2, 3를 더해줍니다.
->실행해서 생성된 GenPacket의 코드를 복사해 ClientSession과 ServerSession에 복붙합니다. DummyClient와 ClientSession이 실행되게 설정하고 실행하면 에러가 발생하는데 ServerSession과 ClientSession에서 SkillInfo를 Skill로 바꾼다. 기존과 마찬가지로 정보들이 정상적으로 보내진다.