확인 테스트
Q1.Packet이 완전체로 왔는지 짤려서 왔는지 구분하려면 어떻게 해야 할까요? 유동적으로 커졌다 작아졌다 하는 패킷의 경우 어떻게 해야 할까요?
-> 첫인자로 size를 넣어준다. (두번째 인자로 Id를 넣어준다.)
Q2. Session의 Session을 상속받은 PacketSession클래스를 추가하고 OnRecv는 sealed 를 붙이고, OnRecvPacket 함수를 abostract를 붙여 새로 선언한 이유는 무엇인가요?
-> PacketSession을 상속받는 애들은 이 OnRecv인터페이스가 아니라 별도로 건내주는 public abstract void OnRecvPacket();이라는 인터페이스로 받아 사용하라는 의도
Q3. PacketSession의 OnRecv에서 buffer에 Packet이 잘 도착했는지 확인하기 위해 2가지를 무엇을 체크했나요?
-> buffer의 크기가 헤더 사이즈 보다 큰지, 버퍼의 크기가 packet의 첫번째 인자인 데이터 사이즈 보다 큰지 체크
Q4. PacketSession의 OnRecv에서 패킷이 잘 독착했는지 확인하고 OnRecvPacket을 실행해 패킷을 조립한 다음 무엇을 해주었나요?
-> 몇 바이트 체크 했는지 peossesinLen에 dataSize를 더해주고, 다음으로 처리한 buffer의 범위를 ArraySegment로 집어준다.
Q5. PacketSession의 OnRecv에서 OnRecvPacket의 매개변수로 무엇을 넘겨 줍니까?
-> 처리할 buffer의 ArrsySegment를 넘겨 줍니다.
Q6. ArraySegment는 new를 붙여도 부담이 없는 이유는 무엇입니까
-> struct기 때문에 new로 생성해도 heap이 아닌 stack에 복사된다.
Q7. Server의 GameSession에서 PacketSession을 상속 받고 나서 OnRecv를 사용 못하는 이유는?
-> PocektSession에서 OnRecv에 sealed를 달았기 때문이다.
Q8. sealed는 어떤 기능을 가지는가?
-> 상속 받은 클래스에서 재정의를 못하게 막는다
Q9. Sever에서 OnRecvPacket에서 무엇을 할 수 있는가?
-> buffer로 부터 받은 size와 id를 추출할 수 있다.
Q10. Server의 OnRecvPacket은 어떤 경로로 호출이 될까?
-> Server의 Main에서 _Listener.Init을 할 때 new GameSession을 하는데 GamseSession은 PacketSession을 상속 받았고, PacketSession은 Session을 상속 받았다. Client가 보내면 Recieve할 거고, RegisterRecv의 RecvAsync가 완료되면 Session의 OnRecvCompleted가 호출되고 그 안의 OnRecv가 호출 될거고, PacketSession의 OnRecv로 들어오게 된다. 그리고 그 안에서 OnRecvPacket가 하나의 Packet의 ArraySegment가 전달되며 호출된다.
Q11. 새롭게 수정한 서버를 테스트하기 위해 DummyClient를 어떻게 수정 했나요?
-> class packet을 복붙, GameSession의 OnConnected에 packet을 생성하고, packet의 내용을 byte[]로 전환해 sendBuff에 넣고 SendBufferHelper를 이용해 packet의 size로 sendBuff의 크기를 할당해 Send에 sendBuff를 전달했다. Packet의 size와 packetId도 수정했다.
Q12. string이 아닌 Packet을 받으려면 public abstract int OnRecv(ArraySegment<byte> buffer);를 어떻게 수정해야 하나요? 이번 챕터에서 한 작업 과정을 나열해 보세요.
→
Packet 클래스 정의
→Server의 class Knight를 Packet으로 이름을 바꾼다.(Knight클래스를 사용하고 있던 부분도 다 바꿔준다) Packet의 attack을 packetId로 이름을 바꾼다. Packet의 hp를 size로 바꾼다.
packet 파싱 해주기
→Session에 Session을 상속받은 PacketSession클래스를 추가한다. public sealed override int OnRecv(ArraySegment<byte> buffer)를 override를 붙여 복붙하고, public abstract void OnRecvPacket();를 추가한다
→OnRecv를 구현한다 int processLen = 0; 추가 PacketSession 클래스에 public static readonly int HeaderSize = 2; 추가 while문 돌면서 buffer.count가 [size(2)]의 크기인 2보다 작으면 break; buffer에서 size부분을 ToUIInt16을 이용해 얻어 buffer.Count가 더 작으면 break; 두 조건을 통과 했으면 OnRecvPacket을 호출해 조립을 해준다. processLen += dataSize; 처리한 바이트 수를 더해준다. 다음 while문에서 작업할 버퍼의 부분을 new ArraySegment로 집어 buffer에 넣어준다. buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize); while문으로 할만큼 처리하다 break로 while문을 나오면 return processLen으로 처리한 바이트 수를 return 한다
→ OnRecvPacket()에 매개변수를 입력을 해준다. OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
PacketSession 사용해서 컨텐츠 단(Server)에서 OnRecvPacket으로 buffer 추출해서 처리하기
→Server로 가서 GameSession이 Session이 아닌 PacketSession을 상속받는 걸로 수정한다.
에러가 뜨는 것을 해결한다.
OnRecv를 삭제한다.
GameSession의 빨간 줄에 커서를 대면 나오는 잠재적 수정사항->추상클래스 구현을 누르면 OnRecvPacket이 뜬다. OnRecvPacket을 구현한다. buffer로부터 BitConverter.ToUInt16함수를 이용해서 size와 id를 각각 추출해서 로그를 출력한다.
테스트를 위해 DummyClient쪽에 class Packet을 복붙한다. Server의 OnConnected의 packet을 생성하고, packet의 내용을 byte[]로 전환하고 sendBuff를 할당하는 코드를 DummyClient의 OnConnect에 복붙을 하고 Close에 packet.size를 전달하는 걸로 수정한다. ArraySegment<byte> sendBuff = SendBufferHelper.Close(packet.size); size와 packetId도 수정한다.Packet packet = new Packet() { size = 4, packetId = 7 };
→Server에서 OnRecvPacket이 잘 되는지 보기 위한 테스트니 Server의 OnConnected에서 Send와 관련된 코드는 주석처리를 해준다. Sleep시간도 5초로 늘려준다.
→ 실행해 잘 packet을 받는지 테스트 한다.
지난 시간까지 해서 보내는 인터페이스와 받는 인터페이스가 윤곽이 나온 상태야.
기존에 사용하고 있던 세션에 기능을 더 추가할거야. 지금까지는 OnRecv라는 인터페이스로 데이터를 받고 있었어.
public abstract int OnRecv(ArraySegment<byte> buffer);
그럼 패킷을 받을 때는 OnRecv가 어떻게 바뀌어야 할지 고민을 해보자.
지난 시간에는 Server에서 Knight라는 클래스를 byte[]로 만드는 걸 해봤었어.
그런데 이제 Knight가 아니라 그냥 일반적인 Packet 클래스라고 가정을 해보자.
namespace Server
{
class Packet
{
public int hp;
public int attack;
}
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Packet packet = new Packet() { hp = 100, attack = 10 };
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
byte[] buffer = BitConverter.GetBytes(packet.hp);
byte[] buffer2 = BitConverter.GetBytes(packet.attack);
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(1000);
Disconnect();
}
그러면 OnRecv 어떻게 바뀌어야지 이걸 처리할 수 있을지 생각해 보자.
TCP같은 경우에는 패킷이 100바이트를 보냈다고 100바이트가 한번에 오는게 아니라 중간중간 짤려서 올 수 있다고 했었어. 그렇다면 Packet이 완전체로 왔는지 짤려서 왔는지 구분하는 무엇인가가 필요하다.
그럼 처음으로 생각할 수 있는 건 ID를 쓰는 거다.
class Packet
{
public int hp;
public int packetId;
}
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Packet packet = new Packet() { hp = 100, packetId = 10 };
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
byte[] buffer = BitConverter.GetBytes(packet.hp);
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(1000);
Disconnect();
}
packetId로 packet을 구분하는 방법이 하나 있을거야.
이 번호에 따라 1번이면 이동 패킷, 2번이면 공격패킷, 3번이면 로그인 패킷 이런 식으로 패킷을 구분하는 방법이 하나 있을 거야.
그런데 이렇게 Id만으로 하면 문제가 있는게 경우에 따라 패킷 자체가 유동적으로 사이즈가 커졌다가 작아졌다가 하는 패킷일 수 있다.
예를 들어 LoginOkPacket이라는 애를 만든다고 가정을 해보자.
class LoginOkPacket : Packet
{
}
packetId는 분명 들어가 있어. LoginOkPacket은 로그인 하는 캐릭터의 정보를 모두 리스트로 보내준다고 하면 LoginOkPacket의 크기는 쉽게 알 수 없다는 문제가 있다.
아애 PacketId만 보고 해당 클래스의 사이즈를 맞추는 건 불가능하니까 결국에는 Packet의 첫 인자로 size를 넣으면 좋을 거라는 생각이 든다.
class Packet
{
public int size;
public int packetId;
}
실제로 MMORPG 뿐만 아니라 대부분의 게임이 패킷을 설계할 때 첫 인자로 size를 넣어주고, 두번째 인자로 패킷의 id를 넘겨주는 경우가 많다.
근데 size와 packetId를 int로 할것이냐 short로 할것이냐가 갈리긴 하는데 대부분 ushort로 하면 충분하다. 2byte 짜리만 해도 블편함 없이 사용할 수 있다.
class Packet
{
public ushort size;
public ushort packetId;
}
패킷이 들어가기 시작하면 네트워크 상으로 송수신 하는 부분은 최대한 압축해서 사이즈를 줄이는게 중요하다. 이제는 얘를 다른 코드 만든 것처럼 메모리를 쿨하게 막 쓸 수는 없다. 스노우볼 처럼 굴러가기 때문이야. 여기서 int→ushort로 해서 4바이트를 아낄 수 있는데 10000명의 유저한테 보낸다고 하면 40000바이트씩 아낄 수 있다는 얘기가 된다. 최대한 압축해서 보내는게 중요하다.
이제 이 패킷을 파싱하는 걸 만들기 위해서 일반적인 세션이 아니라 패킷을 사용하는 세션은 구분을 해서 만들거야.
Session에 Session을 상속받은 PacketSession 클래스를 추가한다.
public **abstract** class PacketSession : Session
{
}
클래스에 abstract를 붙여줬기 때문에 Session에서 꼭 구현을 해야했던
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
이 4개의 아이들을 구현하라는 에러 메시지가 뜨지 않아.
그치만 OnRecv를 복붙해서 override를 해줄거야.
그 뿐만 아니라 sealed 키워드 까지 같이 붙여준다. sealed를 쓰면 혹시라도 다른 클래스가 PacketSession을 상속받은 다음에 OnRecv 이걸 오버라이드 하려고 하면 에러가 난다. 봉인을 했다는 의미.
결국 PacketSession을 상속받는 애들은 이 OnRecv인터페이스가 아니라 별도로 건내주는 public abstract void OnRecvPacket();이라는 인터페이스로 받아라 하고 바꿔치기 해주는 거.
내부에서 OnRecv를 대충 처리해줘서 OnRecvPacket을 보내 줄 것이다. 상세하게 인자로 뭘 넣을지는 아직 모르겠지만 인터페이스를 맞춰준거.
public abstract class PacketSession : Session
{
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
return 0;
}
public abstract void OnRecvPacket();
}
이제 OnRecv를 구현 해주자.
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
// 패킷이 왔다는 건 처음 2바이트 짜리가 size가 될거고,그 다음 Id가 오고, 옵션으로 내용이 들어간다
// PacketSession에서 OnRecv를 할 때는 얘를 파싱을 하는데 일단은 여기 2바이트가 왔는지 먼저 확인을 할거야.
// 만약 잘려서 1바이트만 왔다면 걔를 좀 더 기다려야 한다. 일단 2바이트까지 왔으면 일단 size를 까봐서
// 지금 분석하고 있는 패킷이 몇바이트 짜리 패킷인지 봐가지고 해당하는 데이터가 다 올 떄 까지 기다렸다가
// 패킷 전체를 처리하는 방식으로 이루어질 거야.
// 결국 processLen라고해서 몇 바이트를 처리 했느냐를 변수로둘거야.
// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
int processLen = 0;
while(true) // 패킷을 처리할 수 있을 때 까지 계속 뻉뻉이를 돌거야.
{
// 최소한 헤더는 파싱할 수 있는지 확인
if (buffer.Count < HeaderSize) // 최소한 2바이트 보다는 커야 한다. 2보다 작으면 break를 해줄거야. [size(2)]가 2바이트 짜리기 때문이야.
break;
// 패킷이 완전체로 도착했는지 확인
// [size(2)]를 까봐야 된다는 얘기. ToUInt16은 UINT16(ushort)만큼 긁어서 뱉어준다. dataSize가 있는 공간이었어.
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize) // 패킷이 완전체가 아니라 부분적으로만 왔다는 얘
break;
// 여기까지 왔으면 패킷 조립 가능
OnRecvPacket();
processLen += dataSize; // 몇 바이트 처리했냐
// 여기까지 왔으면
// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]중에
// [size(2)][packetId(2)[ ... ] 이 부분을 처리 했다는 말이 되니까
// 버퍼가 [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]이만큼이 유효 범위 인데
// [size(2)][packetId(2)[ ... ] 이 부분까지 찝어서 옮겨줘야 한다는 얘기
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize); // 바뀐 애는 다음 부분을 찝어주는 애가 된다.
// 어떻게 옮길거냐면 Offset을 시작 지점부터 dataSize 만큼 앞으로 가야 한다.
// 버퍼의 크기는 처리한 dataSize 만큼 빼준다.
} // 이걸 계속 반복하면 된다. 처리할 수 있는데 까지 계속 패킷을 처리 하다가
return processLen; // 더이상 못하겠다 싶으면 break해서 처리한 바이트 수를 return 해준다.
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
이걸 반복하는데 처리할 수 있는애까지 패킷을 처리하다가 더이상 못하겠다 싶으면 break해서 처리한 byte수를 processLen으로 뱉어주는 거.
이제 OnRecvPacket()에다가 뭔가 입력해줘야 하는데
패킷을 만들어서 보내줘도 되고,
**public abstract void OnRecvPacket(ArraySegment<byte> buffer);이런 식으로 해당 영역을 다시 찝어서 보내줘도 된다.**
패킷이 해당하는 영역을 다시 집어서 넘겨 줄건데
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
[size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]중에
[size(2)][packetId(2)[ ... ] 까지가 패킷의 유효범위니까 여기서 여기까지만 알아서 잘 파싱해서 사용하라고 넘겨주는 거다.
그리고 또 고민되는 부분중 하나는 [size(2)][packetId(2)[ ... ] 여기에서 **[size(2)]**가 [packetId(2)[ ... ] 여기에 있는 실질적인 패킷 내용의 사이즈를 넣어줄 것인지, 아니면 [size(2)][packetId(2)[ ... ]이렇게 사이즈 헤더를 포함한 전체로 넣어줄 것인지 정해야 하는데 여기서는 전체를 포함해서 넣어주고 있다.
이건 하나를 정해서 해주면 된다. 케바케
마찬가지로 OnRecvPacket을 할 때도 내용만 넘길지 전체를 넘길지 정해야 하는데 선생님 개인적으로 ****패킷 전체 크기를 내부에서도 알 수 있기 때문에 전체 크기를 넘겨주는게 조금 더 좋은 거 같아. 그게
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
이 부분이야.
참고로 하나 더 예를 들자면, buffer에서
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
이런 식으로 new ArraySement를 이용해서 다시 만드는 것도 가능하지만
**buffer.Slice()**라는 함수를 이용해서 커팅을 하는 것도 가능하다.
선생님은 개인적으로
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
이게 더 가독성이 좋은 거 같아서 더 좋아한다고 함.
지난번에 살펴 봤지만 ArraySegment는 class가 아니라 struct 니까 new 키워드를 붙여줘도 heap 영역에 할당 되는게 아니다. 그냥 stack에 복사하는 개념이기 때문에 부담없이 new ArraySegment로 이렇게 만들어 줘도 된다.
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]
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 processLen;
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
그래서 어쨌든 이렇게까지 만들어 줬면 PacketSession이라는 애를 받아서 사용을 하면 파싱해서 return processLen 해주는 부분은 내부에서 알아서 해줄거고,
실제로 유효범위만
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
이렇게 찝어서 넘어오게 될 건데 그걸 컨텐츠 단에서 ArraySegment<byte> buffer 얘를 다시 추출을 해가지고 이게 어떤 패킷인지 패킷 아이디에 따라서 어떤 행동을 하라는 걸 switch case문으로 쭉 분기 해서 처리를 하면 된다.
사용 예제는 Server의 Program에 가서 해보자.
GameSession이 Session이 아니라 PacketSession을 상속받는 버전으로 바꿔줘 보자.
class GameSession : PacketSession
{
그럼 바로 에러가 뜬다.
일단 구현 안한 부분이 있고, 반대로 구현 하면 안되는 부분이 있으니까.
public override int OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
return buffer.Count;
}
OnRecv는 이제 사용 하면 안된다. PacketSession에서 sealed로 막아놨기 때문이다.
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
OnRecv를 삭제 해준다.
반대로 OnRecvPacket같은 경우에는 구현을 꼭 해줘야 한다. 잠재적 수정사항→추상클래스 구현을 하면 OnRecvPacket이 뜬다.
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
throw new NotImplementedException();
}
그래서 OnRecvPacket에 뭔가를 처리를 해줘야 한다.
여기까지 찝어서 온 거는 [size(2)][packetId(2)[ ... ] 이런 식으로 유효한 범위를 찝어서 온 거였어.
2바이트는 size고, 2바이트는 packetId가 들어가 있을테니까 이거를 그냥 추출해서 로그를 찍어 본다.
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
// 비트 컨버터를 또 다시 이용할건데 바이트 배열을 UInt16 즉 ushort로 뽑아 달라는 요청
// 시작 offset에다 첫 인자를 2바이트를 뽑으면 얘가 ushort가 되는 거.
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
// offset에 2를 더한 거. 즉 다음 2바이트가 된다.
// 지금은 하드코딩 하지만 나중에 자동화 하니 숫자 헷갈려도 걱정 안해도 된다.
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset+2);
// 로그 찍을 거야.
Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}
그래서 Server쪽은 GameSession으로 만들어 줬는데 얘가 PacketSession을 상속 받으니까 Session의 네트워크 통신 단에서
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
// Write 커서 이동
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
int processLen = **OnRecv**(_recvBuffer.ReadSegment);
이 부분에서 다시 쭉 위로 올리면
다시 여기로 들어오게 될거고
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]
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 processLen;
}
여기서 다시 패킷을 어느정도 조립을 해가지고,
카운팅을 해서 만약에 유효한 범위다 하면 OnRecvPacket으로 넘겨줘서 그거를 다시
Server의
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}");
}
여기로 바꾸는 거.
타고 타고 가고 있다.
얘를 테스트 하기 위해서는 더미 클라이언트 쪽에서도 똑같은 방법으로 보내줘야 한다. 최소한
class Packet
{
public ushort size;
public ushort packetId;
}
이 형식대로 size랑 packetId를 보내줘야 정상적으로 파싱할 수 있으니까 임시로 DummyClient 쪽에다도 class Packet을 복붙해 놓자.
DummyClient의 OnConnected에서 더미 메시지를 몇개 보내고 있었는데
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
Send(sendBuff);
}
}
Server의 OnConnected의 packet을 생성하고, packet의 내용을 byte[]로 전환해 sendBuff에 넣고 SendBufferHelper를 이용해 sendBuff의 크기를 할당하는 코드를 복붙을 해 약간 수정을 한다.
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);
}
}
지금 서버 쪽에서 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}");
}
이게 뜨는지 보는 거니까
서버쪽 Send와 관련된 부분을 주석처리 해준다.
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 size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset+2);
Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}
Sleep하는 시간도 5초 정도로 늘려서 종료 되기 전에 OnRecvPacket이 되는지 확인한다.
f5를 눌러 테스트를 해보면
RecvPacketId: 7, Size: 4로 잘 오는 거 확인할 수 있다.
이렇게 한 게 중요한 선수작업이었어.
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
**// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]**
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 processLen;
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
패킷을 설계하기 앞서서 항상 이렇게 2바이트로 사이즈를 확인한 다음에 조립해서 넘겨주는 것 까지 작업을 해봤다.
이제부터 패킷 프로토콜에 대한 작업을 시작할 수 있다는 말이다.
작업한 코드
DummyClient
using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
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}");
}
}
internal class Program
{
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(1000);
}
}
}
}
Server
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class Packet
{
public ushort size;
public ushort packetId;
}
class GameSession : 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 size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
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}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
Session
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
// [size(2)][packetId(2)[ ... ][size(2)][packetId(2)[ ... ]
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 processLen;
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
public abstract class Session
{
Socket _socket;
int _disconnect = 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 _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
public void Send(ArraySegment<byte> sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
#region 네트워크 통신
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
ArraySegment<byte> buff = _sendQueue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
void RegisterRecv()
{
_recvBuffer.Clean(); // 혹시라도 커서가 너무 뒤로 이동하는 것을 방지한다.
ArraySegment<byte> segment = _recvBuffer.WriteSegment; // 버퍼에서 다음으로 데이터를 받을 공간을 WriteSegment로 관리하고 있었어.
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // segment.Count가 애당초 freeSize였어.
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
// Write 커서 이동
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
int processLen = OnRecv(_recvBuffer.ReadSegment);
if (processLen < 0 || _recvBuffer.DataSize < processLen) // 혹시 컨텐츠 단에서 이상한 값으로 넣어줘서 처리가 안됐거나, recvBuffer보다 처리된 데이터 사이즈가 크면 이상한 거니 체크
{
Disconnect();
return;
}// 여기까지 했으면 데이터를 처리 했거나 보류를 했거나 한 상태가 될건데 이제 Read커서를 이동 시키
// Read 커서 이동
if (_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
'Server programming' 카테고리의 다른 글
03_02_패킷 직렬화_Serialization #2_자동화 (0) | 2023.04.11 |
---|---|
03_01_패킷 직렬화_Serialization #1_기본 (0) | 2023.04.11 |
02_13_네트워크 프로그래밍_SendBuffer (0) | 2023.04.07 |
02_12_네트워크 프로그래밍_RecvBuffer (0) | 2023.04.06 |
02_11_네트워크 프로그래밍_TCP vs UDP (0) | 2023.04.06 |
댓글