<PacketSession>
패킷 단위로 보내고 받는다면 Session이 어떻게 달라져야하는가?
서버에서 클라이언트의 데이터를 받을때 패킷 단위로 받고 클라이언트에서 패킷 단위로 보내자!
1. TCP 통신에서는 패킷이 전체가 왔는지 잘려서 왔는지 구분할 필요가 있다. -> 혼잡 제어로 인해 전체 패킷을 받는다는 보장이 x
1) packetId: id를 통해 패킷을 구분(1번: 이동 패킷, 2번: 공격 패킷, 3번: 이동패킷)
-> 문제: 경우에 따라 유동적으로 packet의 사이즈가 줄어들 수 있음
ex) Packet을 상속받는 LoginOKPacket이 캐릭터가 갖는 캐릭턱 정보의 리스트를 보낸다고 할때 쉽게 Packet의 사이즈를 예측 할 수 없다.
=> Packet의 맨 처음에는 Packet의 사이즈를 나타낸다. 일반적으로 형식을 ushort로 2바이트로 나타낸다.
( 최대한 압축하여 적은 바이트로 보내는 것이 효과적임, 만명의 유저에게 4바이트 씩 아끼면 4만 바이트를 아끼는 나비효과가 있다. )
class Packet
{
public int hp;
public int packetId;
}
class Packet
{
public ushort size;
public ushort packetId;
}
2. PacketSession 클래스
1) Session을 상속받아서 추상 클래스 PacketSession을 만든다. *PacketSession 클래스도 추상 클래스이기 때문에 인터페이스를 구현하라는 에러 메세지가 뜨지 않는다.
a) OnRecv 함수를 override 하되 sealed을 통해 다른 클래스가 PacketSession 클래스를 상속받는다고 해도 OnRecv 함수를 override 하지 못하도록한다.
- sealed: 봉인하는 한정자.
b) PacketSession을 상속받는 다른 클래스는 대신 OnRecvPacket을 상속받도록 한다.
public abstract class PacketSession: Session
{
// sealed: 다른 클래스가 PacketSession을 상속받아서 OnRecv 함수를 override할려고 하면 error -> 봉인하겠다.
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
return 0;
}
// PacketSession을 상속받는 클래스는 OnRecvPacket 인터페이스를 받도록
public abstract void OnRecvPacket();
}
2) OnRecv 구현
a) 패킷을 받으면 [size(2)][pakcetId[2][...옵션 내용] 해당 형태의 패킷을 받을 수 있다. (경우에 따라 해당 패턴이 반복된 패킷을 받을 수 도 있음)
b) 패킷의 첫번째 인자인 패킷의 size를 받은 후, 해당 사이즈만큼 데이터가 오기를 기다린다. size 만큼 데이터가 쌓이면 해당 패키을 처리한다.
c) while문으로 buffer를 확인한다.
- processLen: 현재 처리한 바이트의 길이
- 만약 buffer.Count가 HeaderSize보다 적으면 아직 헤더(PacketSize)도 다 안 온것이기때문에 break;(기다린다.
- 그 경우가 아니면, packet의 size를 추출해서 하나의 패킷의 사이즈를 알아야한다. -> BitConverter.ToUInt16: 16비트 -> 2바이트를 추출한다.
- 만일 buffer가 dataSize(하나의 온전한 패킷 사이즈)보다 작으면 아직 패킷 전체가 전달된게 아니기 때문에 break
- 위의 경우가 아니면 패킷 조립이 가능하다 -> OnRecvPacket 함수를 호출한다: dataSize의 패킷 ArraySegment를 인자로
- 처리한 바이트 길이가 증가됐으므로 dataSize 만큼 증가
- buffer에서 다음 패킷을 읽어드려야하기 때문에 커서를 옮겨야한다: 여기서는 buffer 자체를 앞에서 처리한 패킷을 제외한 부분으로 수정하여 새롭게 할당
* buffer.Slice 함수도 있지만 가독성이 좋다고 생각하는 아래의 코드를 사용
public static readonly int HeaderSize = 2;
// sealed: 다른 클래스가 PacketSession을 상속받아서 OnRecv 함수를 override할려고 하면 error -> 봉인하겠다.
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
// 처리한 바이트의 길이
int processLen = 0;
while (true)
{
// 최소한 헤더는 파싱할 수 있는지 확인
if (buffer.Count <= HeaderSize)
break;
// 패킷이 완전히 도착했는지 확인
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize)
break;
// 여기까지 왔으면 패킷 조립 가능
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
processLen += dataSize;
// buffer에서 다음 패킷을 읽어드려야하기 때문에 커서를 옮겨야함
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return 0;
}
3. PacketSession 클래스 전체
1) PacektSession을 상속받으면 패킷을 파싱하는 작업은 내부의 OnRecv 함수를 통해 하게되고 실제로 유효 범위만 OnRecvPacket으로 통해 컨텐츠 단으로 넘어오게 된다.
public abstract class PacketSession: Session
{
// 헤더 사이즈
public static readonly int HeaderSize = 2;
// sealed: 다른 클래스가 PacketSession을 상속받아서 OnRecv 함수를 override할려고 하면 error -> 봉인하겠다.
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
// 처리한 바이트의 길이
int processLen = 0;
while (true)
{
// 최소한 헤더는 파싱할 수 있는지 확인
if (buffer.Count <= HeaderSize)
break;
// 패킷이 완전히 도착했는지 확인
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize)
break;
// 여기까지 왔으면 패킷 조립 가능
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
processLen += dataSize;
// buffer에서 다음 패킷을 읽어드려야하기 때문에 커서를 옮겨야함
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return processLen;
}
// PacketSession을 상속받는 클래스는 OnRecvPacket 인터페이스를 받도록
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
2) Server의 Program.cs
a) GameSession 클래스가 Session이 아닌 PacketSession을 상속받도록 한다. 이때, OnRecv 함수는 override가 불가능하고 OnRecvPacket 함수를 구현해야한다. 아래와 같이 패킷의 id와 size 출력하도록한다.
b) 클라이언트로부터 완전히 Receive하면 OnRecvCompleted 함수가 호출되고 함수 내부의 OnRecv 함수가 호출 -> OnRecvPacket이 호출
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
Console.WriteLine($"RecvPacketId: {id}, size: {size}");
}
c) test를 해보기 위해 DummyClient에서도 Packet 형식으로 맞춰준다.
: 이전에 Server의 Program.cs에서 OnConnected의 코드를 복사하여 DummyClient Programcs에 붙여준다.(Server의 Program.cs의 코드는 주석처리)
using ServerCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
class Packet
{
public ushort size;
public ushort packetId;
}
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Packet packet = new Packet() { size = 4, packetId = 7 };
for (int i = 0; i < 5; i++)
{
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(packet.size);
Send(sendBuff);
}
}
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}");
}
}
class Program
{
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
// Connector 연결
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
d) 실행
: 실행하면 아래와 같이 DummyClient에서 보낸 패킷이 Server에 도착한 것을 확인할 수 있다.
'Development > 게임 서버' 카테고리의 다른 글
[Part 4 게임 서버] 네트워크 프로그래밍 6 (RecvBuffer, SendBuffer) (0) | 2022.01.21 |
---|---|
[Part 4 게임 서버] 네트워크 프로그래밍 5(Connector, TCP vs UDP) (0) | 2022.01.19 |
[Part 4 게임 서버 ] 네트워크 프로그래밍 4 (Session #3, Session #4) (0) | 2022.01.18 |
[Part 4 게임 서버] 네트워크 프로그래밍 3(Session #1, Session #2) (0) | 2022.01.16 |
[Part 4 게임 서버] 네트워크 프로그래밍 2 (Listener) (0) | 2022.01.16 |