이번 포스팅에서는 자동으로 패킷을 생성하는 방법에 대해 설명합니다. 먼저, XML 파일을 사용하여 패킷의 포맷을 정의하고, 이를 기반으로 자동으로 패킷을 생성하는 방법에 대해 다룹니다.
그 과정 중의
첫단계로 XML 파일을 만들어 보고,
Main에서 XmlReader를 이용해 읽어들여 출력을 하고,
더 나아가 Xml 파일의 전부가 아닌 packet부분만 출력을 하기 위해 Parse하는 코드와 PacketFormat을 만드는 작업을 해 봅니다.
자동화 할 때 거치게 되는 단계 :
- 하드코딩으로 만들어 본 다음에 (이전 시간까지의 작업)
- 최대한 재사용할 수 있는 것들을 자동화 코드로 빼주면 된다. (이제 할 것)
지난 시간까지 작업한 코드에서 Packet을 사용하지 말지가 고민이야. size랑 packetId라는 건 header에 들어가 있는 정보긴 하지만 실질적으로 사용하지 않으니까 얘를 넣어야 할지 고민이 되는데 자동화를 간략하게 하기 위해 빼는 방식으로 구현 할거야.
class Packet을 삭제하고, PlayerInfoReq에서 상속하는 것도 없애준다.
public PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
생성자는 삭제하고 생성자에서 해주던 것을
PlayerInfoReq 클래스의 Write에서 바로 밀어 넣어줘도 별 차이가 없다.
public override 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);
// 혹시라도 중간에 한번이라도 실패하면 false가 뜬다.
//success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), packet.size);
count += sizeof(ushort);
**success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);**
count += sizeof(ushort);
그리고 override는 빼주고 자동화를 해보자.
솔루션→추가→새프로젝트
콘솔앱 선택하고 프로젝트 이름은 PacketGenerator라고 지어주자.
위치가 중간에 있는게 깔끔해 보이지 않으니까 솔루션에 새 솔루션 폴더를 만들어 주고, 이름을 Tools로 한다. Tool을 만들게 되면 여기에 몰아 배치한다. PacketGenerator를 옮겨준다.
중요도가 조금 떨어지는 애들이니까 뭉쳐서 관리.
이제 PacketGenerator에서 이것 저것 만들어 줘야 한다.
참고를 하기 위해 ServerSession과 동시에 보면서 한다.
이제 이 형식을 그대로 자동화 해서 뭔가를 만들어야 하는데, 어딘가에는 원본에 대한 정의가 필요한데 그렇다는 건 Packet의 정의를 어떤식으로 할지 정해야 한다. 이게 2단계
json으로 해도 되고 xml로 해도 되고, 아니면 기타 우리가 자체 정의한 idl을 이용해도 되는데 선생님은 xml을 선호하는 편이야. json에 비해 하이어라키가 좀 더 편하게 보인다는 장점이 있기 때문에 패킷 같은 경우는 xml로 하는 경우가 많고 하다보면 이해가 된다.
PacketGanerator프로젝트에 새 항목 추가를 해서 데이터의 XML파일을 선택해서 이름을 PDL로 만들어 준다. PacketDefinitionList의 약자다.
XML 파일은 헤더에 <?xml version="1.0" encoding="utf-8" ?> 이런 XML에 대한 정보가 들어가고, 다음에 패킷에 대한 정보를 만들어주면 되는데 일단 간단하게 인터페이스를 만들어 준다.
XML형식은 굳이 이해할 필요 없고, 쓰다 보면 자연스럽게 알게 되니까 이런 식으로 <PDL>쌍으로 이루어져 있다는 게 특징이고, 예를 들어 ServerSession의 PlayerInfoReq라는 패킷을 만들어 줬었는데 얘를 XML에 샘플로 하나 정의를 해보도록 할거야.
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
</packet>
</PDL>
이렇게 해주면 </packet> 까지 패킷 영역이되어서 하나의 패킷을 정의를 한 거다.
패킷인데 이름은 PlayerInfoReq가 된 거.
그리고 playerId를 추가하는데
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
<long name="palyerId"></long>"
</packet>
</PDL>
이렇게 </long>"로 닫아줘도 되지만,
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
<long name="palyerId"/>
</packet>
</PDL>
/로 닫아줘도 똑같은 말이다.
한줄짜리인 경우는 이렇게 하는 거고, 안에 뭐가 들어간다면 </packet>로 닫아주면 된다.
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
<long name="palyerId"/>
<string name="name"/>
</packet>
</PDL>
참고로 packet, long, string 이런 건 정해진 이름이 아니라 우리가 나중에 파싱을 해서 사용하기 위해서 맞춰주는 이름이라고 생각하면 된다.
다음은 List인데 SkillInfo라는 타입으로 되어 있어. 이런 부분에서 json보다 XML이 더 편하게 사용할 수 있는 부분인데 대칭적인 구조로 쓰는게 편하다는게 어떤건지 보여줄거야.
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
<long name="palyerId"/>
<string name="name"/>
<list name ="skill">
<int name="id"/>
<short name="level"/>
<float name ="duration"/>
</list>
</packet>
</PDL>
얘가 정의 하는게 클래스로 ServerSession에서 만들어 주고 있었던게 되는 거니까, 여기있는 데이터만 긁어서 ServerSession의 구조를 알 수 있다는 게 된다.
그래서 얘를 파싱하면 오른쪽의 코드를 생성하는 코드를 만들어 주면 된다.
그럼 다시 PacketGenerator의 Program으로 가서 XML을 파싱하는 부분부터 들어가기 시작해야 한다.
C++의 경우는 XML을 파싱하는 게 공식 라이브러리 상에 없기 때문에 open souce 중에서 찾아서 해야 하는데 C#의 경우에는 마련되어 있다.
using System;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static void Main(string[] args)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true, // 주석 무시한다
IgnoreWhitespace = true // 스페이스바 무시한다
};
// using을 쓰면 이 범위를 벋어날 때 알아서 Dispose를 호출해 주니까 조금 더 깔끔하게 된다.
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
{
r.MoveToContent(); // xml의 헤더 같은 건 다 건너 뛰고 <packet name = "PlayerInfoReq"> 부분부터 들어 간다는 말
while(r.Read()) // 스트림 방식으로 읽어 들일거야. 한줄 한줄 읽어들임. packet 정보라면 이어서 계속 읽으면 된다.
{
// 일단은 처음 사용하는 거니까 어떤 정보 갖고 있는지 궁금하니까
Console.WriteLine(r.Name + " " + r["name"]); // 일단 name이란 프로퍼티를 읽어 볼거야.
}
}
}
}
}
실행해 보기 앞서서 기존에는 DummyClient와 Server를 켜고 있었으니까, PacketGenerator를 우클릭해서 시작프로젝트로 설정을 누른다.
이 다음에 바로 실행을 하면 에러가 난다.
문제가 일어나는 이유는 애당초 Program에서 읽는 경로가 좀 달라서 그래.
PacketGenerator 우클릭을 하고 파일탐색기에서 폴더 열기를 하면
PDL이란 파일이 있다.
근데 실제로 얘가 읽어 들이는 경로는 실행파일이 있는 경로를 기준으로 읽어들이기 때문에
이 exe 파일이 있는 기준으로 만들어 질거야.
나중에는 얘를 설정으로 뺴내 줄거지만 지금은 그냥 간단하게 하는 거니까 PDL파일을 여기다가 복사해 주도록 할거야.
이상태에서 ctrl + f5로 다시 실행을 시켜준 다음에
어떤 결과물이 나오는지 살펴 보도록 하자.
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name = "PlayerInfoReq">
<long name="palyerId"/>
<string name="name"/>
<list name ="skill">
<int name="id"/>
<short name="level"/>
<float name ="duration"/>
</list>
</packet>
</PDL>
<packet name = "PlayerInfoReq"> 부터 읽어주는 걸 알 수 있다.
Console.WriteLine(r.Name + " " + r["name"]);
이 코드에서
r.Name으로 뽑아 온게 <packet여기 있는 이름이라고 생각할 수 있고,
두번쨰로 r["name"]이렇게 뽑아 왔는데 얘가 바로 name = "PlayerInfoReq"이 atrribute를 뽑아 온 거라 볼 수 있다.
사실 자동화 툴을 만들 때 필요한 내용은 이게 다다.
또 하나 중요한 건 한줄 한줄을 읽으면서 가고 있는데
</packet>
</PDL>
여기 있는 부분까지 지금 다 한줄 한줄씩 긁어서 파싱을 하고 있는 거 까지 알 수 있다.
다시 돌아간 다음에 일단은 Main에서 하고 싶은 건 packet만을 대상으로 뭔가를 실행하고 싶다고 해보자. 어떻게 구별할 수있을지 생각을 해보면,
일단은 r.Name을 이용해서 packet인지 아닌지 구분하는 방법이 있을 거고,
그거보다 세련된 방법은 r.Depth를 이용하는 거다.
Depth란 말 그대로 0부터 시작해서 몇개를 파고 드는지 나타내는데 <packet의 Depth는 1이 될테니까
if (r.Depth == 1)
ParsePacket(r);
이렇게 넘겨주도록 하자. Console.WriteLine부분은 주석처리를 한다.
public static void ParsePacket(XmlReader r)
{
}
이렇게 XmlReader를 받는 함수를 만들어 준다.
그리고 </packet> 이렇게 닫는 packet도 출력이 됐었는데 얘가 만약 닫는 목적이면 굳이 얘를 출력하면 안되는 거니까, 이거를 구분할 수 있는 방법이 있다.
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
Element라는게 시작하는 거고, 끝나는 위치는 EndElement다.
이렇게 시작하는 부분이라면 ParsePacket에 넘겨주자.
public static void ParsePacket(XmlReader r)
{
if (r.NodeType == XmlNodeType.EndElement) // 혹시 NodeType이 잘 못 왔다면
return;
if (r.Name.ToLower() != "packet") // 소문자로 변환해서 packet이 아니면
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = r["name"]; // "PlayerInfoReq" 이 부분 긁어 온 거
if (string.IsNullOrEmpty(packetName))
{
Console.WriteLine("Packet without name");
return;
}
ParseMembers(r);
}
여기까지 왔으면 packet이라는 건 틀림 없지만 이제 안에 있는 부분을 하나하나 긁어와야 된다는 얘기가 되는 거.
ParseMembers라는 애로 넘겨줘야 한다.
세부적인 기능은 나중에 만들거고 일단은 간단한 인터페이스 부터 뚫어주고 큰틀을 잡는 작업을 하고 있는 거다.
public static void ParseMembers(XmlReader r) // 여기까지 왔으면 하나하나의 정보를 긁어주는 역할을 하게 될거고,
{
string packetName = r["name"];
while(r.Read()) // **packet안에 있는 정보들을 다 긁어 올 때 까지 계속 실행**을 할거야.
{
}
}
그렇다는 건 Read를 하다가 벗어나는 걸 인지할 수있어야 한다는 건데 packet은 depth를 1로 판별하고 있었으니 마찬가지로 depth로 판별을 하면 될 거 같아.
<long name="palyerId"/>
<string name="name"/>
<list name ="skill">
<int name="id"/>
<short name="level"/>
<float name ="duration"/>
</list>
그럼 최초로 ParseMember로 사용됐던 <long name을 기준으로 얘네들만 실행이 되어야 하니까 여기를 depth라고 해줄거야.
public static void ParseMembers(XmlReader r) // 여기까지 왔으면 하나하나의 정보를 긁어주는 역할을 하게 될거고,
{
string packetName = r["name"];
int depth = r.Depth + 1;
// r.Depth가 packet의 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":
break;
default:
break;
}
}
}
이런 각 case 별로 다를지 안 다를지 따라서 코드를 자동화 하는 부분을 이제 여기다가 넣어주면 된다.
이렇게만 보면 어떤 식으로 자동화를 해야 할지 막막한데 이런 유형을 처리할 때 굉장히 유용한 방법이 있다.
일단 패킷의 포멧을 정의 한 다음에 나뉘는 부분을 갈아 끼우는 방식으로 작업을 하는 건데
일단 코드로 작업을 해보자.
PacketGenerator프로젝트에서 새항목 추가로 C#항목에서 클래스를 추가한다. PacketFormat이란 이름으로 한다.
그리고 ServerSession으로 가서 기존에 작업했던 내용을 컨닝 해보자. 여기 안의 무엇이 자동화 되면 좋을지 보자.
using System;
using System.Collections.Generic;
using System.Text;
namespace PacketGenerator
{
class PacketFormat
{
// 여러줄에 걸쳐서 문자열을 정의해 주고 싶을 땐 @를 붙이면 된다.
public static string packetFormat =
@"
";
}
}
여기로 일단 ServerSession에서 기존에 만들어 놨던 PlayerInfoReq 클래스 코드를 싸그리 긁어 와보자.
namespace PacketGenerator
{
class PacketFormat
{
// 여러줄에 걸쳐서 문자열을 정의해 주고 싶을 땐 @를 붙이면 된다.
public static string packetFormat =
@"
class PlayerInfoReq
{
public long playerId;
public string name;
public struct SkillInfo
{
public int id;
public short level;
public float duration;
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 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 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); // 나중에 이어서 파싱을 해야 할 경우를 대비해서 맞춰준다.
// string
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
// skill list
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);
}
}
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;
// 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);
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}
}
";
}
}
여기서 고정된 부분은 냅두고, 불필요하게 반복되서 자동화 해야 하는 부분은 걸러 내줄거야.
shift+tab으로 왼쪽으로 이동시켜 준다.
고정적인 부분이 있을거고, 바꿔치기해줄 부분으로 갈릴건데
public long playerId;
public string name;
이런건 고정적으로 들어가는게 아니라 파싱한 XML파일에서 내용이 들어가야 하는 거니까,
이런 부분을
{0}
이런식으로 0번이든 1번이든 해서 바꿔치기를 해주면 된다.
이제 일반적인 소괄호 같은 경우에는 {{ 이런 식으로 하나를 더붙여야지만 소괄호로 인식 된다. 그래서 하나씩 다 소괄호를 붙여준다.
class PlayerInfoReq 같은 경우는 패킷 이름이야. 항상 이 이름이 아닐테고 PDL에 정의된 이름이 들어가야 할 테니까, 얘가 맨 처음으로 바꿔치기 해야할 부분이 된다.
계속 숫자로 하면 헷갈리니까 주석을 달면서 진행을 해보자.
public struct SkillInfo
{{
public int id;
public short level;
public float duration;
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 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 List<SkillInfo> skills = new List<SkillInfo>();
여기있는 부분은 List인데 List는 좀 있다가 하도록 한다. 복잡하니까 일단 다 빼고,
일반적인 애들을 보면 Read랑 Write가 들어가는데, 모든 애들이 공통적으로 갖고 있는 인터페이스였어.
일단 Read에서 어디부터 바뀔지 곰곰히 생각해보면
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); // 나중에 이어서 파싱을 해야 할 경우를 대비해서 맞춰준다.
// string
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
// skill list
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}로 찝어 준다.
class PacketFormat
{
// {0} 패킷 이름
// {1} 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
// 여러줄에 걸쳐서 문자열을 정의해 주고 싶을 땐 @를 붙이면 된다.
public static string packetFormat =
@"
class {0} // 클라에서 서버로: 나는 플레이어 정보를 알고 싶어. 그 정보는 playerId라는 플레이어야.
{{
{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}
}}
Write로 들어가서 뭔가를 넣어줘야 한다.
@"
class {0} // 클라에서 서버로: 나는 플레이어 정보를 알고 싶어. 그 정보는 playerId라는 플레이어야.
{{
{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);
**// 여기에 packetId가 들어가는 건데 packetId는 클래스 이름 {0}랑 똑같이 맞춰주면 되니까** (ushort)PacketID.**{0}** 이렇게 맞춰주면 된다.
**** success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.**PlayerInfoReq**);
count += sizeof(ushort);
**// 여기 부터가 실질적으로 데이터가 들어가는 거
// {3}
// 이것저것 정리한 XML 파일을 읽어가지고,
//** {3} 멤버 변수 Write**를 여기다가 바꿔치기 해서 넣어주면 된다는 얘기가 된다.
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
// string
ushort nameLen = (ushort)Encoding.Unicode.GetByteCount(this.name);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
Array.Copy(Encoding.Unicode.GetBytes(this.name), 0, segment.Array, count, nameLen);
count += nameLen;
// 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);
// 최종 사이즈를 넣는 부분**
success &= BitConverter.TryWriteBytes(s, count);
if (success == false)
return null;
return SendBufferHelper.Close(count);
}}
수정을 하면
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} // 클라에서 서버로: 나는 플레이어 정보를 알고 싶어. 그 정보는 playerId라는 플레이어야.
{{
{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);
}}
}}
";
}
}
이게 뭘 하는지는 나중에 완성을 하면 좀더 윤곽이 보일 거야.
어쨌든 이렇게 packetFormat 에 관한 걸 하나 만들어 줬고,
이제 내려가서 이 작업을 이어서 하면 된다.
다음에 만들 건
public static string memberFormat =
@"";
얘 같은 경우엔 뭐가 들어갈 거냐면
ServerSession의 PlayerInfoReq클래스의 struct SkillInfo안의
public int id;
public short level;
public float duration;
이 부분이 들어가게 될 거다.
// {0} 변수 형식
// {1} 변수 이름
public static string memberFormat =
@"public {0} {1}";
List는 조금 이따가 하기로 했고, 그 다음에 해야 할 건 struct SkillInfo의 Read와 Write에 관한 부분인데
Read에선
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);
}
여기 있는 부분이 자동화가 들어가야 하니까,
// {0} 변수 이름
// {1} To~ 변수 형식
// {2} 변수 형식
public static string readFormat =
@"this.{0} = BitConverter.{1}(s.Slice(count, s.Length - count));
count += sizeof({2});";
일반적으론 이렇긴 한데
string 같은 경우는 양식이 달랐어.
class PlayerInfoReq의 Read에서 보면
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);
**// string
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;**
// skill list
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);
}
}
string도 만들어 주자.
public static string readStringFormat =
@"ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;";
일단 이렇게 복사를 하고 시작한다.
그러면 여기있는 name이란 부분도 바꿔치기를 해줘야 하는데
// {0} 변수 이름
string은 이것만 해주면 사실 다 처리할 수 있다. ToUInt16같은 경우는 고정이었어. size를 ushort로 밀어 넣어주고 있었으니까, 그 부분이 여기 들어가 있었던 거였고. sizeof도 똑같고, GetString도 똑같고, name부분만 {0으로 바꿔치기 해주면 된다.
// {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;";
이제 Write로 넘어가면 된다. Write도 무엇이 필요할지 살펴보자.
public static string writeFormat =
@"";
writeFormat은 어디서 사용하고 있었는지 보면
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);**
playerId를 어떤 식으로 Write하고 있었냐면 success로 뭔가를 하고 있었으니까 이부분을 대표라고 생각을 해서 일단을 복사를 하고 시작을 해보자.
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);";
이렇게 하고 이제 뭐를 바꿔치기 해줘야 하는지 살펴 보면
// {0} 변수 이름
// {1} 변수 형식
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.{0});
count += sizeof({1});";
이렇게 해주면 된다.
그리고 Write에서 string의 경우
// string
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;
이걸 일단 복붙한 다음에
public static string writeStringFormat =
@"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;
";
얘도 뭘 바꿔치기 해줘야 하는지 보는데 생각보다 많지 않다는 걸 알 수 있다.
// {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;
";
일단은 List를 제외한 일반적인 형태, 그리고 string에 대한 부분을 어떤 식으로 자동화 하면 될지 템플릿을 만들어 준 거.
이제 얘를 이용해서 뭔가를 작업을 해주면 된다.
작업한 코드
PDL.mxl
<?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의 Program.cs
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())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
}
}
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);
}
public 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":
break;
default:
break;
}
}
}
}
}
PacketGnerator의 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 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);
{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;";
}
}
확인 테스트:
Q1. XML 파일을 만들고 내용을 차례로 출력하는 코드를 작성하는 과정을 말해보세요.
→
PacketGenerator프로젝트를 생성하고, PDL.xml파일을 추가하고, PlayerInfoReq 클래스의 구조를 나타내는 XML 코드를 작성한다.
PacketGenerator의 Program에서 XmlReaderSetting을 생성하고, using안에서 xmlReader를 PDL.xml파일과 settings를 매개변수로 넣어 Creaete 한 뒤, Content부분부터 While(r.Read())를 하며 r.Name + " " + r["name"]의 형식으로 xml파일의 내용을 출력하는 코드를 작성해 보자.
PacketGenerator를 시작 프로젝트로 설정한다.
PDL.xml파일을 PacketGenerator.exe 파일이 있는 폴더로 복사해준다. 실행을 하면 PDL.xml 파일에 입력해준 내용이 출력된다.
Q2. Depth를 이용해 packet 안의 내용만 출력하기 위해 Program.cs 안에 해준 내용을 말해 보세요
→
-> Main의 Xml파일의 내용을 출력하는 부분에서 Depth가 1이고 XmlNodeType.Element이면 ParsePacket에 XmlReader r을 넣어 호출한다.
-> ParsePacket 함수에서 XmlNodeType.EndElement인지, 이름이 packet이 맞는지 체크한 후 r["name"]을 출력하고, 비었으면 오류 메시지를 출력해 return 한다. 정상이면 ParseMemebers함수에 다시 r을 넣어 호출한다.
-> string packetName에 r["name"]을 대입한다. while문으로 r.Read()를 호출하며 그 안에서 r.Depth가 depth+1과 맞지 않으면 break 하고, 맞다면 memberName에 r[”name”]을 대입해 주고, memberName이 비었는지 체크해 주고, memberType에 r.Name을 대입해 그 값에 따라 분기가 갈리는 case문을 작성한다.
Q3. PacketFormat.cs에서 포멧을 정의해 주기 위해 어떤 작업을 했는지 설명해 보세요.
-> PacketGenerator 프로젝트에 PacketFormat 클래스 파일을 추가한다. public static string packetFormat = @" " 여기 안에 class PlayerInfoReq를 넣고, struct SkillInfo는 나중에 하기 위해 삭제하고 고정된 부분은 냅두고 값이 바뀌는 부분은 {0} 이런식으로 바꿔주고 무엇을 의미하는지 주석을 달아준다.
-> public static string memberFormat = 을 선언하고 struct SkillInfo 안의 변수 3개에 대해 써준다.
->public static string readFormat =를 선언하고 Read에 관해 넣어준다.
->public static string readStringFormat =를 선언하고 Read의 string에 관해 넣어준다.
->public static string writeFormat =를 선언하고 Write에 관해 넣어준다.
->public static string writeStringFormat =를 선언하고 Write의 string에 관해 넣어준다.
'Server programming' 카테고리의 다른 글
03_08_패킷 직렬화_PacketGenerator#3_전체 코드 생성 (0) | 2023.04.18 |
---|---|
03_07_패킷 직렬화_PacketGenerator #2_코드 자동 생성, List 추가 (0) | 2023.04.18 |
03_05_패킷 직렬화_Serialization #4_List< > (0) | 2023.04.13 |
03_04_패킷 직렬화_Serialization #3_string (0) | 2023.04.12 |
03_03_패킷 직렬화_UTF-8 vs UTF-16 (0) | 2023.04.11 |
댓글