네트워크 프로그래밍
지난 시간에는 SendBuff 클래스를 만들었다. 클래스 내부에는 버퍼로 활용해 줄 recvBuffer, 사용한 영역을 표기하는 usedSize, 남은 공간의 크기를 나타내는 FreeSize를 선언해준다. 또한 메서드로는 Receive와 같이 Open, Close 함수를 만들어었지만, Receive와는 다르게 Clean 함수는 만들어주지 않았다. 🤔
이 후, TreadLocal을 이용한 SendBufferHelper를 만들어 SendBuff 클래스 내부를 건드리지 않고 사용하였다.
패킷 전송하기 📤
패킷을 전송하는 방법까지 알아보았지만, 정작 패킷만 보았을 때 패킷의 사이즈가 어느정도인지 아직은 알 수 없다. 따라서 보통 패킷의 첫 번째 인자로는 사이즈를 넣어주고, 두 번째 인자로는 패킷의 ID를 넣어 사용한다. 여기서 말하는 패킷의 ID란 해당 패킷이 어떤 패킷인지에 대한 구분 값이다. 예를 들어 패킷 ID가 1이라면 이동, ID가 2라면 스킬과 같은 형태로 나뉜다. 이 때 중요한 것은 기존처럼 int를 사용하는 것이 아니라 ushort를 사용하여 값을 표현한다는 것이다.
❓ 그냥 평소처럼 Int 쓰면 되는거아냐?
이제는 서버와 전송을 하기 때문에 최대한 용량을 압축해서 보내야 한다. 예를들어 패킷을 보낼 때에도 int 형으로 Size와 Id를 만들경우 ushort 대비 4바이트를 더 사용하게 되기 때문이다. 이게 만약 10,000명이라면 40,000 byte를 낭비하게 되는 것이다. 따라서 패킷을 보낼 때에는 최대한 압축해서 보내는 것이 중요하다.
이제 진짜 작업을 해보자! 😤
기존 Session Class에서 사용하던 OnRecv를 오버라이딩하여 사용할 것이다. Send 메서드를 실행할 때에도 size, Id 등과 같은 설정등의 변화가 있지만, 현재는 패킷을 받을 때 (OnRecv) 해당 패킷의 이상 여부를 검사하여 작업을 처리할 것이다.
ServerCore의 Session 클래스
public abstract class Session
{
Socket _socket;
int _disconnected = 0;
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer);
// ⭐ OnSend를 오버라이딩하여 사용한다!
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
...
}
ServerCore의 Session 클래스 스크립트 내부에 새롭게 생성하는 PacketSession 클래스
Packet 검사를 진행하고 이를 넘겨주어 처리하기 위한 Class를 생성해준다.
자세히 보면 oveeride 하는 OnRecv 함수에 sealed 키워드가 붙어 있는 것을 볼 수 있다. 해당 키워드는 PacketSession을 상속 받는 곳에서, 이를 다시금 오버라이드 할 수 없게 봉인해주는 역활을 한다. 따라서 PacketSession을 상속받은 객체는 OnRecv를 사용하지 못한다. 따라서 해당 함수를 이용하여 내가 원하는 패킷이 모두 도착했는지 확인하는 과정을 거친다.
예외처리 🚧
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));
}
return 0;
}
먼저 예외 처리 1. 최소한의 헤더는 파싱할 수 있는지 확인한다. 이 후 패킷이 완전체로 도착했는지 ushort를 이용하여 검사한다.
도착했음에도 불구하고 또 다른 예외처리를 진행한다. 예외 처리 2. 패킷이 완전체가 아닌 부분적으로 온 상태인지 확인한다. 위 두가지의 예외처리를 만족하면, buffer를 받아온 datasize 만큼 넘겨주어 첫 번째 패킷을 (현재 size, id만 담긴 값) OnRecvPacket 메서드에게 넘겨준다.
// 별도로 보내주는 OnRecvPacket 인터페이스로 받아야 한다.
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
ArraySegment는 Sturct를 사용하여 Heap 영역에 할당되지 않기 때문에 new 키워드를 사용해도 문제가 되지 않는다.
이 후, 기존 Server에서 Session을 상속받 부분을 PacketSession으로 변경하여 준다.
Server의 Program.cs
class GameSession : PacketSession // 수정 ⭐
{
public override void OnConnected(EndPoint endPoint)
{
...
}
...
}
Server의 Program.cs
GameSession이 PacketSession을 상속받았기 때문에 abstract로 생성한 OnRecvPacket을 구현해주어야 한다. 다음과 같이 구현하여 BitConverter를 통해 받아온 인자 값이 정상적으로 잘 들어오는지 출력해보자.
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}");
}
다시 ServerCore의 Session.cs에 PacketSession 클래스로 이동하여 살펴보자. OnRecv로 돌아와서 processLength에 받음 받은 datasize 만큼 증가시키고, buffer에서는 사용한 부분 만큼을 제외하여, 다음 패킷으로 넘겨준다.
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 = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return 0;
}
Dummy Client
DummyClient 프로젝트 내 Progrm 스크립트로 이동하여 Send를 테스트하자. 기존에 테스트했던 패킷 클래스와 Server의 Program.cs Onconnect 내부에 있던 스크립트를 복사해서 붙여넣고 테스트를 해보자.
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);
}
}
...
}
진행 순서 🔡
1. 클라이언트 연결 요청
2. 서버 승인
→ session.Start()를 통해 Recv 함수 동작
→ OnConnected() 진입 및 패킷 전송 (size, packetId)
3. Recv > Register > Complete > 서버의 OnRecv 동작하여 size, packetId 출력!
위와 같은 과정을 거치며 동작한다.
결과 😎
임의로 넣어주었던 packetId 값인 7이 정상적으로 뜨는 것을 볼 수 있다. 어렵다 어려워!
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-4-2. 패킷 직렬화 : Serialization #2 (0) | 2023.12.04 |
---|---|
Part 4-4-1. 패킷 직렬화 : Serialization #1 (1) | 2023.11.30 |
Part 4-3-12. 네트워크 프로그래밍 : SendBuffer (0) | 2023.11.05 |
Part 4-3-11. 네트워크 프로그래밍 : RecvBuffer (1) | 2023.11.02 |
Part 4-3-10. 네트워크 프로그래밍 : TCP/UDP (0) | 2023.10.31 |