확인 테스트
Q1. List<>를 직렬화 하기 위해 한 작업들을 나열해 보세요.
→
-> PlayerInfoReq 클래스에 SkillInfo구조체를 선언하고, 그 구조체를 들고 있는 List인 skills를 생성한다. public struct SkillInfo { public int id; public short level; public float duration; } public List<SkillInfo> skills = new List<SkillInfo>();
->ServerSession의 Write에 List가 가지고 있는 갯수를 buffer에 밀어 넣고, foreach로 List를 순회한다. // skill list success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count); count += sizeof(ushort); foreach(SkillInfo skill in skills) { // TODO }
-> SkillInfo 구조체로 가서 Write 함수를 만들어 준다. 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; }
-> PlayerInfoReq클래스의 Write로 가서 위에서 정의해준 SkillInfo의 Write를 사용해 데이터를 buffer에 넣는다. // 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);
-> SkillInfo의 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); }
-> PlayerInfoReq의 Read에서 skills를 SkillInfo의 Read를 이용해 파싱하게 해주자. // 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); }
-> Packet 코드를 복사해서 반대편(ClientSession)에 붙여 넣는다
-> ClientSession의 OnRecvPacket에 skill의 내용을 출력하는 코드를 추가한다 PlayerInfoReq p = new PlayerInfoReq(); p.Read(buffer); Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");
foreach (PlayerInfoReq.SkillInfo skill in p.skills) { Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration}"); }
-> ServerSession의 OnConnected에서 스킬 정보를 가라로 만들어 준다 PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" }; packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 101, level = 1, duration = 3.0f }); packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 201, level = 2, duration = 4.0f }); packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 301, level = 3, duration = 5.0f }); packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 401, level = 4, duration = 6.0f });
-> 테스트를 한다
지난 시간에 이어서 List<> 작업을 해보자.
만약
class PlayerInfoReq : Packet
{
public long playerId;
public string name;
public List<int> skills = new List<int>();
만약 List<> 안에 기본 자료형인 int 같은게 들어있다고 하면, 사실 string이나 byte 배열을 넘긴 것과 마찬가지로 List<>가 몇개가 있는지 넘긴 다음에 그 다음에 data 밀어 넣은 작업을 똑같이 적용해 주면 된다.
경우에 따라 List<>안에도 구조체 형식이 들어있는 경우가 있다
만약 skill 의 정보를 보낸다고 했는데 skill의 id만 보내는 경우라면 이렇게 하겠지만 상황을 바꿔 볼거야.
단순한 int가 아니라 구조체라고 가정을 해보자.
PlayerInfoReq 클래스에 SkillInfo 구조체를 선언하고, List<SkillInfo>를 생성한다.
public struct SkillInfo
{
public int id;
public short level;
public float duration;
}
public List<SkillInfo> skills = new List<SkillInfo>();
이런식으로 int가 아니라 skillInfo라는 구조체를 통해 저장되어 있다고 하면 이제 skills를 패킷에 밀어넣을 때 어떻게 해야 할까?
어자피 List 형식도 생각나는 방법이 한가지 밖에 없다.
List가 몇개를 가지고 있는지 short로 밀어 넣어 준 다음에, 그 다음에 데이터를 밀어 넣는 건 비슷할텐데,
그러면 일단 인터페이스를 맞춰볼거야.
ServerSession의 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;
일단 nameLen 이라는 크기를 밀어 줬어.
마찬가지로 비슷하게 맞춰 볼거야.
nameLen의 위치에 skills.Count를 넣어주면 될 거 같아.
skills.Count는 4바이트니 캐스팅을 해줘야 한다. (ushort)skills.Count
count를 sizeof(ushort)만큼 증가 시킨다.
skills의 SkillInfo를 하나씩 돌면서 작업을 해줘야 한다.
skill 하나마다 데이터를 밀어 넣어 줘야 한다는 얘기. 일단 TODO 상태로 두고 SkillInfo로 가서 뭔가를 만들어 주자.
// skill list
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)skills.Count);
count += sizeof(ushort);
foreach(SkillInfo skill in skills)
{
// TODO
}
SkillInfo로 가서
Write를 하고 있었으니까 Write를 만들어 주고, 성공여부 bool로 return 하고, Sapn<byte> 타입으로 데이터를 받고 있었어.
다시 string 쪽을 보면 count를 늘려주고 있었는데 Write안에서도 이어서 같은 count를 참조하고 조작할 수 있도록 Write의 인자로 ref ushort count로 넘겨준다. 유일한 방법은 아니니 편한대로 작동되게 맞춰주면 된다.
bool success는 true로 시작하고 마지막에 return 한다.
success &= BitConverter.TryWriteBytes();이런 식으로 했던 작업을 똑같이 이어서 하면 된다.
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();
return success;
}
}
id, level, duration 이 3개를 넣어줘야 하니까,
Write의
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.packetId);
count += sizeof(ushort);
이 부분을 컷닝한다.
public bool Write(Span<byte> s, ref ushort count)
{
bool success = true;
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.packetId);
count += sizeof(ushort);
return success;
}
Slice해서 count를 넣는 부분까지는 같을 거야.
애당초 s에 넣어줄 Span은 전체 byte배열을 넣어준거고, 실시간으로 count에 몇번째 count를 작업 하는지 넣어줄 거니까, 이런 식으로 Slice해서 유효 범위를 집어서 TryWriteBytes에 넣어주면 된다.
그 다음에 3개로 복제한 다음에
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;
}
}
각각 value에 id, level, duration을 넣고, sizeof에도 int, short, float을 넣어주면 된다.
다시 PlayerInfoReq의 Write로 가서 TODO를 채우면 된다.
// 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);
이렇게 Span 값을 넘겨주고, reference로 count를 넘겨주면 된다.
이런 식으로 작업을 하면, 내가 들고 있는 스킬마다 계속 loop를 돌면서 하나씩 Span으로 집어준 영역에다가 넣어주게 될거야.
나중에는 이런 부분을 잘 자동화 해서 만들어주면 될 거 같다.
여기서 count를 증가시키는 작업을 하지 않는 이유는 skillInfo의 Write에서 count를 늘려주는 작업을 하고 있었어. 그래서 밖에서는 Count를 신경 안쓰고 그냥 호출하는 그런 부분이 된다.
Read도 똑같이 맞춰 줄건데
PlayerInfoReq의 Read를 컨닝 해보면
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
이런식으로 사용하고 있었으니까 복사해서 인터페이스를 맞춰주고 시작을 하자.
public void Read(ReadOnlySpan<byte>s, ref ushort count)
{
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
}
playerId에 id가 들어갈테고, ToInt64가 아니라 ToInt32가 될거야. Slice 부분은 똑같이 들어갈거고, sizeof(int)만큼을 증가 시켜주면 된다. 같은 방식으로 2개를 더 파싱한다.
ToSingle은 float 타입을 의미하고, double은 그냥 ToDouble을 쓰면 된다.
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);
}
이렇게 어느정도 완성을 할 수 있다.
나중에 정의를 할 때 List<>안에 뭐가 들어올지를 어떤 구조체로 선언을 해줄건데, 걔를 다시 한번 코드를 SkillInfo 구조체 처럼 만들어 줘가지고, Write랑 Read를 편하게 할 수 있게 열어 준다음에, 그거를 PlayerInfoReq에서 실제로 코드를 뭔가 만들어줄 때 걔를 이용해서 자동화를 하면 된다.
PlayerInfoReq의 Read에서 string에 이어서 Skill list를 파싱하는 부분을 만들어 주자.
//string에서 마지막에 count 증가 하는거 까먹었으니 증가 시키고
count += nameLen;
이어서 만들어 주자.
일단은 sillLen을 추출할 수 있었어. Skill이 몇개 들어 있는지 추출할 수 있을거고, 추출한 다음에는 sizeof(ushort)만큼 커서를 뒤로 이동시켜 줘야한다.
그 다음에 sillLen 만큼 뺑뻉이를 돌면서 skill을 추출해 주면 된다.
// 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);
}
그리고 혹시라도 skills가 기존에 다른 정보를 들고 있었을 수도 있으니까 Clear를 한번 해주자.
이러면 확실하게 우리가 원하는 skillLen 만큼 들어가 있을 거고,
이렇게 작업을 하면 처음에 예상했던
public List<SkillInfo> skills = new List<SkillInfo>();
skills에 밀어넣는 작업을 구현할 수 있다.
다시 테스트 하기 위해서 Packet 코드를 복사 한 다음에 반대 편에 복사를 해주도록 하자.
테스트 하기 위해서 ClientSession의 OnRecvPacket에 뭔가를 더 추가해보도록 하자.
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.SkillInfo skill in p.skills)
{
Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration}");
}
}
break;
}
Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}
이렇게 해서 정상적으로 오고 있는지를 테스트 해볼거야.
근데 테스트 하려면 아까 만들어 줬던, ServerSession의 OnConnected에서 스킬 정보를 가라로 만들어 줘야 한다.
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 101, level = 1, duration = 3.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 201, level = 2, duration = 4.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 301, level = 3, duration = 5.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 401, level = 4, duration = 6.0f });
// 보낸다
//for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = packet.Write();
if (s != null)
Send(s);
}
}
이걸 4개 만들어 주자.
그럼 여기까지 만들어준 패킷이 정상적으로 보내지는지 테스트를 하자.
실행을 하면
잘 뜨고 있다.
선생님은 Read에서 count를 늘려주는 코드에서 sizeof()안에 type을 바꿔주지 않아서 error가 났었어. 이렇게 코드를 손수 한땀 한땀 작업 하는건 안좋은 방법이야.
크래시가 안나고 잘못된 값으로 읽혀 진행되면 최악의 경우다.
만약에 출시 목적이 아니라 간단하게 포트폴리오로 만드는 경우는
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);
}
}
이런 식으로 Write와 Read를 한땀 한땀 만드는 것도 크게 잘못된 방법은 아니다.
왜냐하면 자동화 툴을 만드는 것도 어느정도 시간을 소모하기 때문에 작은 규모의 경우 패킷이 많지 않기 때문
그래서 이런 패킷들의 작업을 자동화 할 수 있는 툴을 만들어 볼거야.
이게 Serialization의 개념이라는 건 이해했으면 좋겠어. 데이터가 여러개 있을 때 어떤 식으로 밀어넣어 줄 것인가가 직렬화 그리고 역 직렬화의 개념이었고, 물론 여기서 만든 방법은 가장 간단한 방법중 하나다. size나 배열의 length를 밀어 넣어 준 다음에 그 만큼을 하나하나 파싱을 해주고있었는데 우리가 만든 것의 장점은 가장 직관적이라 생각하기 쉽다는게 장점이고, 단점은 PlayerInfoReq라는 패킷을 만들어줘야 한다는게 단점이야.
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 101, level = 1, duration = 3.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 201, level = 2, duration = 4.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 301, level = 3, duration = 5.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 401, level = 4, duration = 6.0f });
// 보낸다
//for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = packet.Write();
if (s != null)
Send(s);
}
}
패킷을 보내기 위해 일단은 패킷을 만들어 준 다음에 하나하나씩 다 밀어 넣어줘서 인스턴스를 만들어 준 다음에 Write를 해서 byte배열을 만들어서 보내주고 있었어.
이것도 최적화 하는 경우가 있다. 이런식으로 중간 단계를 거쳐서 패킷을 만들어서 보내는게 아니라 실제로 이렇게 밀어 넣는 작업을 바로 byte 배열에 밀어 넣어줄 수 있게 만들어 줄 수 있다.
이런 시뮬레이션 할 때 자주 사용하는 것중 하나가 구글에서 만든 프로토 버퍼나 플랫 버퍼 둘 중 에서 하나를 사용하게 되는데 프로토 버퍼 경우는 우리가 하는 방식과 비슷하게 중간에 인스턴스 만들어서 채운 다음에 걔를 다시 변환하는 작업을 거치게 되고 그게 아니라 플랫 버퍼를 이용하면 막바로
ArraySegment<byte> s = packet.Write(); 이런식으로 바이트 배열에 집어 넣는 방식으로 구현이 되어 있는데 이런 식으로 중간 단계를 거치면 성능상으로는 손해가 있겠지만 직관적이라 코딩은 편해진다. 그래서 선생님은 성능 좀 손해 보더라도 중간에 인스턴스 만들어서 작업하는게 좋다고 생각한다. 자
작업한 코드
ServerSession
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ServerCore;
namespace DummyClient
{
public abstract class Packet
{
public ushort size;
public ushort packetId;
public abstract ArraySegment<byte> Write(); // 밀어 넣는 부분 Serialize
public abstract void Read(ArraySegment<byte> s); // 빼내 주는 부분 DeSerialize
}
class PlayerInfoReq : Packet
{
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 PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
public override 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 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), this.packetId);
count += sizeof(ushort);
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;
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);
}
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
class ServerSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 101, level = 1, duration = 3.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 201, level = 2, duration = 4.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { id = 301, level = 3, duration = 5.0f });
packet.skills.Add(new PlayerInfoReq.SkillInfo() { 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
{
public abstract class Packet
{
public ushort size;
public ushort packetId;
public abstract ArraySegment<byte> Write(); // 밀어 넣는 부분 Serialize
public abstract void Read(ArraySegment<byte> s); // 빼내 주는 부분 DeSerialize
}
class PlayerInfoReq : Packet
{
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 PlayerInfoReq()
{
this.packetId = (ushort)PacketID.PlayerInfoReq;
}
public override 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 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), this.packetId);
count += sizeof(ushort);
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;
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);
}
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
//class Packet
//{
// public ushort size;
// public ushort packetId;
//}
//class PlayerInfoReq : Packet // 클라에서 서버로: 나는 플레이어 정보를 알고 싶어. 그 정보는 playerId라는 플레이어야.
//{
// public long playerId;
//}
//class PlayerInfoOk : Packet // 서버에서 클라로: 답변을 주는 거.
//{
// public int hp;
// public int attack; // hp와 attack을 반환한다고 가정을 해보자. 간단하게 상황을 만들어 준 거.
//}
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.SkillInfo 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}");
}
}
}
end
'Server programming' 카테고리의 다른 글
03_07_패킷 직렬화_PacketGenerator #2_코드 자동 생성, List 추가 (0) | 2023.04.18 |
---|---|
03_06_패킷 직렬화_PacketGenerator #1_XML파일 사용해 자동으로 패킷 생성하기 위한 첫 작업 (0) | 2023.04.14 |
03_04_패킷 직렬화_Serialization #3_string (0) | 2023.04.12 |
03_03_패킷 직렬화_UTF-8 vs UTF-16 (0) | 2023.04.11 |
03_02_패킷 직렬화_Serialization #2_자동화 (0) | 2023.04.11 |
댓글