지난 시간에 생성된 코드를 ServerSession, ClientSession에 각각 수동으로 붙여 넣는게 문제였습니다.
패킷은 컨텐츠 만들면서 계속 변화할텐데 만들 때 마다 수동으로 복사하는 건 힘드니까 오늘은 그 부분을 자동으로 처리하는 방법을 알아볼 것 입니다.
이를 위해 다음과 같은 과정을 거칩니다.
- GenPackets.cs이 생성될 장소 만들기
- PDL의 경로를 받을 string 변수 선언
- 실행 파일 및 빌드된 파일이 생성되는 위치 변경
- PDL의 기본 경로 설정
- bat파일을 이용해 GenPacket.cs 생성
- GenPacket.cs 생성되는 위치를 bat파일에서 설정
1. GenPackets.cs이 생성될 장소 만들기
DummyClient→추가→ 새항목
GenPackets라는 클래스를 만들어 준다.
그리고 DummyClient에 Packet이라는 폴더를 만들어서 그 산하에 넣어준다.
Server에도 Packet이란 폴더를 만들고, 산하에 GenPackets라는 클래스를 만든다.
결국 ServerSession 안에서 PacketID부터 PlayerInfoReq 클래스 부분을 잘라서 GenPackets에다가 통째로 들어간다는 말이 된다.
namespace는 당장엔 없어도 될 거 같아 삭제한다.
ClientSession에서도 같은 작업을 해준다.
어떤 방식으로든 이걸 직접 복붙하지 않더라도 자동으로 갱신이 되게끔 만들어 줄 거야.
2. PDL의 경로 받을 string 변수 선언
준비 작업이 필요한데
PackeGenerator의 Program.cs에 가보면, Main에서 PDL의 경로를
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
이렇게 PDL.xml로 받고 있었어.
경로를 보면 PacketGenerator/bin/Debug/necoreapp3.1의 경로에 있는 PDL이 실행이되는데
이거를 인자로 받아서 처리할 수 있게 바꿔 보도록 할거야. 일단 이 경로에 있는 PDL은 삭제한다.
일단 Program.cs 제외 다 닫아보자.
Main에다가
string dplPath = "PDL.xml";
이렇게 써준다. 일단 실행파일이 있는 곳이 기본 상태이지만
여기 Main에서 처음 프로그램을 실행할 때 인자를 받아서 경로를 바꿔줄 수 있게 옵션으로 넣어주도록 할거야.
if (args.Length >= 1)
pdlPath = args[0];
using (XmlReader r = XmlReader.Create(pdlPath, settings))
{
뭔가 프로그램이 시작할 때 인자로 넘겨 줬다고 하면,
걔를 파싱을 해가지고, pdlPath에 경로를 넣어 줘가지고, 기존에 했던 Create를 이어간다.
3. 실행 파일이 생성되는 위치 변경
지금까지는 경로를 보면 실제로 빌드를 하면 실행되는 경로는
\Server\PacketGenerator\bin\Debug\netcoreapp3.1
여기까지 왔었어. 근데 여기있는 파일들이 여기까지 와서 만들어 지면 너무 복잡하니까,
bin 산하에 생성되도록 설정을 바꿔 준다.
PacketGenerator에서 우클릭을 해서 속성에 가보면
이렇게 Output path:를 Browse를 눌러 선택해 bin으로 바꿔준다.
이 상태에서 테스트로 빌드를 한번 해본다.
bin의 netcoreapp3.1이 있는 걸 볼 수 있다.
이 폴더도 없애자면 없앨 수 있다. PacketGenerator에서 파일 탐색기에서 열기를 누른 다음에
일단 비주얼 스튜디오는 잠시 닫아 준다.
폴더를 보면 PacketGenerator.csprj라는 파일이 있을 건데 얘를 열어준다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>bin\\</OutputPath>
</PropertyGroup>
</Project>
여기다가 옵션을 하나 추가를 해줄건데
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>bin\\</OutputPath>
</PropertyGroup>
</Project>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
해석 하자면
Target Framework를 Output Path에다가 추가할 것이냐? 아니
이러면 아까 같은 파일이 뜨는 걸 막을 수 있어.
이 상태에서 다시 비주얼 스튜디오를 켜준다.
다시PacketGenerator를 빌드해본다.
이제 보면 netcoreapp3.1폴더가 아닌 그냥 bin에 파일들이 만들어져 있는 것을 볼 수 있다.
Debug랑 netcoreapp3.1폴더는 사용 안하니 삭제해준다.
이제 원본 PDL.xml파일을 어디서 관리할지 고민이 되는데 별다른 문제가 없으면 PacketGenerator산하에 있던 여기의 PDL을 게임에서 사용할 버전으로 인식을 해서 얘를 원본으로 사용해도 무방하다. C++, C#이랑 다른 게 C++은 파일을 만든 다음에 파일마다 공유하는게 간단한 반면, C#은 그게 까다롭다. 일단 지금의 경우에는 여기에 놓고 진행을 해본다.
4. PDL의 기본 경로 설정
이상태에서 PacketGenerator를 시작프로젝트로 설정하고 실행을 하면 크래시가 일어난다.
그 이유는 bin을 디폴트 경로로 실행하려는데 여기에 PDL이 없기 때문이었어.
그래서 현재 exe파일이 있는 경로가 아니라 하나 뒷칸으로 가서 찾도록
static void Main(string[] args)
{
string pdlPath = "../PDL.xml";
이렇게 ../를 추가해준다. 그럼 bin의 이전 폴더에 있는 PDL.xlm을 잘 참조한다.
5. bat파일을 이용해 GenPacket.cs 생성
DummyClient랑 Server를 연동해서 계속 만들어야 하는데 PDL을 수정한 다음에 어떤 식으로든 PacketGenerator를 실행하면 GenPackets 얘들까지 자동으로 뭔가가 갱신되게 만들어 줘야 한다. 이제 그 부분을 만들어 줄거야 .
방법은 다양한데 기본적으로 간단하게 사용할 수 있는 건. 배치 파일을 만들어서 걔를 눌러주는게 가장 간략하고 많이 사용하는 방법이다.
다시 solution 의 경로를 열어주고 한칸 위로 올라가 솔루션 파일이 있는 폴더를 본다.
여기에 소스가 아니라 잡동사니 느낌으로 관리할 폴더를 하나 만들어준다. 폴더를 만들고 이름을 Common으로 지어준다. 그리고 그 안에 Packet이란 폴더를 만든다.
여기에 배치파일을 만들어 줄건데 텍스트 문서를 만들어 준다. 파일명을 GenPackets.bat으로 지어준다.
bat파일은 말 그대로 윈도우에서 제공해주는 이런 저런 명령어들을 작성해서 개네들을 한번에 실행하게끔 할 수 있는건데 외울 필요 없고 그 때 그 때 필요한 기능을 구글링 해서 찾아주면 된다.
일단 하고 싶은 건, PacketGenerator/bin에 exe파일이 만들어졌는데 얘를 클릭하는 작업을 대신하고 싶은 거.
START PacketGenerator.exe
이렇게 해주는데 경로를 맞춰 줘야 한다.
bat파일 경로로부터 두칸 뒤로 간 다음에 PacktGenerator로 이동을 해서 다시 bin으로 이동을 해야 한다.
START ../../PacketGenerator/bin/PacketGenerator.exe
이렇게 하고 인자를 넣어줄건데 PDL의 경로를 넣어줘야 한다.
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
여기까지 작성을 하면 무슨일이 일어날지 보자.
탐색기에서 GenPackes.bat을 더블클릭 해보자.
열어주면 자동 생성된 파일 GenPacket.cs이 나온다.
걔가 바로 Program.cs에서
string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);
File.WriteAllText("GenPackets.cs", fileText);
여기서 만들어준 아이였어.
굳이 이름을 바꿀 필요가 없으니까 하드코딩 상태로 둘게. 굳이 옵션으로 빼고 싶다면 pdlPath와 마찬가지로 외부 인자로 빼면 된다.
방금 만들어준 배치파일에서 인자로 넣어준 ../../PacketGenerator/PDL.xml 이게
프로그램이 실행될 때 Main의 string[] args로 들어가게 된다. 하나만 넣어 줬으니까 args[0]에 들어가게 될 거고, 걔를
if (args.Length >= 1)
pdlPath = args[0];
이런 식으로 파싱해서
using (XmlReader r = XmlReader.Create(pdlPath, settngs))
얘를 찾아 준거다.
6. GenPacket.cs 생성되는 위치를 bat파일에서 설정
배치 파일을 만들어서 생성하는 것 까지 했는데 다음에 해야 할 건 생성된 GenPackets.cs라는 파일을 DummyClient산하의 Packet/GenPacket이라는 애한테 옮기고, Server의 GenPackets라는 애한테도 옮겨야 한다.
GenPackes.bat 파일을 열고
구글링을 해보면 여러가지 버전이 있는데
가장 마음에 드는 버전에 XCOPY
XCOPY만 해주면 만약 경로에 똑같은 파일이 있으면 에러가 나니까, 옵션으로 /Y를 넣어주면 만약 같은 이름의 파일 있으면 덮어쓴다는 의미다.
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
이렇게 3가지 명령어를 동시에 실행하는 것을 만들었으니까 잘 실행이 되는지 보자.
양쪽의 GenPackets.cs의 내용을 삭제하고
GenPackets.bat파일을 누르면 양쪽의 GenPackets.cs의 내용이 다시 생기는 것을 알 수 있다.
파일을 지운다음에 하면 다시 파일이 생성된다.
이런 식으로 bat 파일을 만들어서 수동으로 한번씩 눌러주는 방법도 되고, 아니면 빌드하는 순간에 bat파일을 자동으로 실행을 해가지고, 자동으로 GenPackets를 만들어 주게끔도 할 수 있다.
이제 남은 건 PDL.xml에 실컷 작업을 한 다음에 bat 파일을 한번씩만 눌러주면 GenPackets.cs 파일이 자동으로 완성이 된다. 그럼 ServerSession과 ClientSession에서 패킷에 해당하는 부분을 잘 활용하면 된다.
작업한 코드
Common/Packets의 GenPackets.bat
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
PacketGenerator프로젝트의 Program.cs
using System;
using System.IO;
using System.Xml;
namespace PacketGenerator
{
class Program
{
static string genPackets;
static ushort packetId;
static string packetEnums;
static void Main(string[] args)
{
string pdlPath = "../PDL.xml";
XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true,
IgnoreWhitespace = true
};
if (args.Length >= 1)
pdlPath = args[0];
using (XmlReader r = XmlReader.Create(pdlPath, settings))
{
r.MoveToContent();
while(r.Read())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
//Console.WriteLine(r.Name + " " + r["name"]);
}
string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);
File.WriteAllText("GenPackets.cs", fileText);
}
}
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);
packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine + "\\t";
}
// {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 "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> 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);
}
}
}
ServerSession
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;
namespace DummyClient
{
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
var skill = new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f};
skill.attributes.Add(new PlayerInfoReq.Skill.Attribute() { att = 77 });
packet.skills.Add(skill);
packet.skills.Add(new PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
packet.skills.Add(new PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
packet.skills.Add(new PlayerInfoReq.Skill() { id = 401, level = 4, duration = 6.0f });
// 보낸다
//for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = packet.Write();
if (s != null)
Send(s);
}
}
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}");
}
}
}
ClientSession
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 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, openSegment.Offset + buffer.Length, buffer2.Length);
//ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
//Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
//long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
//count += 8; // 나중에 이어서 파싱을 해야 할 경우를 대비해서 맞춰준다.
PlayerInfoReq p = new PlayerInfoReq();
p.Read(buffer);
Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");
foreach (PlayerInfoReq.Skill skill in p.skills)
{
Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
}
}
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}");
}
}
}
확인 테스트
Q1. 패킷 파일 자동화를 위해 작업한 과정을 설명해 보세요.
->
1. GenPackets.cs이 생성될 장소 만들기
-> DummyClient와 Server에 Packet이란 폴더를 각각 만들고 GenPackets라는 클래스를 각각 만들어 넣어준다.
ServerSession과 ClientSession의 PacketID부터 PlayerInfoReq클래스 부분을 각각의 GenPackets에 잘라서 붙여넣기 한다.
2. PDL의 경로를 받을 string 변수 선언
-> PacketGenerato의 Program.cs의 Main에서 string dplPath = "PDL.xml";를 선언한다.
if (args.Length >= 1)
pdlPath = args[0];
using (XmlReader r = XmlReader.Create(pdlPath, settings))
{
이렇게 코드를 추가해서 Main함수가 인자를 받았다면 pdlPath에 그 인자의 첫번째 값을 넣어주게 해 pdlPath의 값을 수정할 수 있게 한다.
3. 실행 파일 및 빌드된 파일이 생성되는 위치 변경
-> PacketGenerator를 실행하면 생성되는 파일의 위치를 bin으로 바꿔주기 위해 PacketGenerator 프로젝트에서 속성에 들어가 Ouput path를 설정해 준다.
bin의 netcoreapp3.1 폴더 안에 생성되게 되는데 폴더를 없애기 위해 PacketGenerator에서 파일탐색기에서 열기를 누르고 비쥬얼 스튜디오를 닫고 PacketGenerator.csprj 파일을 열어준다.
<TargetFramework>netcoreapp3.1</TargetFramework> 아래줄에
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>를 추가한다.
4. PDL의 기본 경로 설정
-> PacketGenerator를 시작프로젝트로 설정하고 실행을 하면 크래시 발생한다. bin을 디폴트 경로로 설정했지만 여기에 PDL이 없기 떄문이다.
static void Main(string[] args)
{
string pdlPath = "../PDL.xml";
이렇게 output되는 bin의 상위 폴더로 디폴트 경로를 바꿔준다.
5. bat파일을 이용해 GenPacket.cs 생성
-> solution 파일이 있는 폴더의 상위 폴더로 가서 Common 폴더를 만들고, 안에 Packet이란 폴더를 만든다.
이곳에 텍스트 파일을 만들고 GenPacket.bat으로 이름을 짓는다.
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
이렇게 입력한다.
GenPacket.bat를 실행하면 GenPacket.cs이 생성된다.
6. GenPacket.cs 생성되는 위치를 bat파일에서 설정
-> 생성된 GenPackets.cs 파일을 옮기기 위해 GenPackes.bat파일을 열고
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
이 코드를 추가해 준다.
GenPackets.cs를 지운 다음에 GenPackes.bat을 실행하면 GenPackets.cs이 생성된다.
Q2. string pdlPath = "../PDL.xml"; 코드에서는 ../를 1번만 해줬는데 ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml에서는 2번 해준 이유를 설명해 보세요.
-> 전자의 경우는 output 폴더인 bin 폴더가 기준이라 ../를 1회만 해 주었고, 후자의 경우는 bat파일의 위치인 Common/Packet이 기준이라 ../../이렇게 2회 해줬다.
'Server programming' 카테고리의 다른 글
03_11_패킷 직렬화_PacketGenerator #6_PacketManager 코드 생성을 자동화, 패킷 분리 (0) | 2023.04.20 |
---|---|
03_10_패킷 직렬화_PacketGenerator #5_ switch문의 비효율성 개선, case 파싱 자동화 (0) | 2023.04.20 |
03_08_패킷 직렬화_PacketGenerator#3_전체 코드 생성 (0) | 2023.04.18 |
03_07_패킷 직렬화_PacketGenerator #2_코드 자동 생성, List 추가 (0) | 2023.04.18 |
03_06_패킷 직렬화_PacketGenerator #1_XML파일 사용해 자동으로 패킷 생성하기 위한 첫 작업 (0) | 2023.04.14 |
댓글