테스트 질문
Q1. RecvBuffer를 만들고 사용하기 위해 작업한 내용을 나열해 보세요.
Q2. 작업한 내용을 분류해 정리해 보세요
답변
답1.
ServerCore에 RecvBuffer클래스를 추가한다. public을 붙여준다. ->클래스 안에 **ArraySegment<byte> _buffer;**를 선언한다.
->int bufferSize를 인자로 받는 RecvBuffer 생성자를 만든다. new ArraySegment를 해서 강제로 할당을 해 _buffer에 대입한다. int _readPos; int _writePos; 변수를 선언한다.
->DataSize랑 FreeSize를 public int 타입의 getter-only property(읽기 전용 속성)로 선언하고 계산한 return 값을 get으로 포장한 코드를 만든다. DataSize는 _writePos-_readPos한 값 즉 버퍼에 들어 있는 아직 처리 되지 않는 데이터의 사이즈이고, FreeSize는 _buffer.Count - _writePos한 값, 즉 버퍼에 남아있는 공간이다.
->public ArraySegment<byte> ReadSegment, public ArraySegment<byte> WriteSegment를 getter-only property(읽기 전용 속성)으로 return값을 get으로 감싼 인터페이스를 만들어 준다. 각 **ArraySegment<byte>**를 이용해서 각각 어디부터 어디까지 읽으면 되는지와 어디부터 어디까지 리시브할 때 쓰면 되는지를 return 해 준다.
->Clean 함수를 만든다. 일단 int dataSize를 선언하고 getter-only property(읽기 전용 속성)인 DataSize로부터 값을 받아온다. dataSize가 0이면 _readPos, _writePos에 0을 대입한다. dataSize가 0이 아니면, Array.Copy를 이용해서 복사할 유효 데이터를 _buffer의 시작 위치로 복사하고, _resPos를 0으로, _writePos를 dataSize로 초기화 한다.
-> _readPos와 _writePos를 이동시키는 함수인 public bool OnRead(int numOfBytes), public bool OnWrite(int numOfByte)를 만들어준다. OnRead에서 매개변수로 받아준 처리한 크기인 numOfBytes가 DataSize보다 크면 return false, 아니면 _readPos에 numOfBytes를 더해준다. return true;를 해준다. OnWrite에서는 numOfByte(receive한 크기) > FreeSize이면 return false, 아니면 _writePos에 numOfByte를 더하고, return true를 해준다.
->Session class에 RecvBuffer _recvBuffer = new RecvBuffer(1024); 이렇게 recvBuffer를 만들어 주고, Start의 _recvBuffer를 SetBuffer를 하는 부분은 삭제한다.
->RegisterRecv에서 ArraySegment<byte> segment에 _recvBuffer.WriteSegment로 다음으로 데이터를 받을 공간을 추출해서 넣어준다. 추출한 segment의 Array, Offset, Count를 _recvArgs.SetBuffer()의 매개변수로 넣어 SetBuffer를 해준다. RegisterRecv가 시작하자마자 Clean을 호출한다. -> OnRecvCompleted에서 _OnWrite를 호출해 Write커서를 이동시킨다. if(_recvBuffer.OnWrite(args.BytesTransferred) == false)면 Disconnect하고 return 한다. 그리고OnRecv를 호출해 컨텐츠로 데이터 넘겨주고 처리 결과를 받는다. OnRecv(_recvBuffer.ReadSegment);
->결과를 받아야 하니 Session의 public abstract int OnRecv(ArraySegment<byte> buffer);의 반환값을 void에서 int로 수정해 준다.
->Server의 GameSession 클래스의 OnRecv도 int를 return 하게 수정한다. return buffer.Count; 이렇게 임시로 수정해 놓는다.
->DummyClient의 OnRecv도 마찬가지로 수정한다.
->다시 Session의 OnRecvCompleted으로 돌아와서 int processLen =OnRecv(_recvBuffer.ReadSegment); 이렇게 얼마만큼의 데이터를 받았는지를 processLen로 받아준다. 혹시라도 processLen이 0보다 작거나 recvBuffer보다 데이터 사이즈가 크다면 Disconnect를 해주고 return을 해준다. 다음은 Read커서를 이동 시키기 위해 _recvBuffer.OnRead(processLen)를 해준다. 이게 false면 Disconnect, return을 해준다.
답2.
1. ServerCore에 새로운 RecvBuffer 클래스를 추가했습니다. 이 클래스는 버퍼를 관리하고 읽기/쓰기 위치를 추적합니다.
2. RecvBuffer 클래스에 다음 멤버를 추가했습니다:
- RecvBuffer생성자,
- _buffer: byte 배열을 저장하는 ArraySegment
- _readPos와 _writePos: 읽기와 쓰기 위치를 추적하는 정수 변수
- getter-only 프로퍼티
- DataSize : 버퍼에 들어 있는 아직 처리 되지 않는 데이터의 사이즈
- FreeSize: 버퍼에 남아있는 공간
- ReadSegment: 데이터 유효 범위의 세그먼트로 어디부터 데이터를 읽으면 되는지
- WriteSegment:다음에 리시브를 할 때 어디부터 어디가 유효범위인지
- Clean 메서드: 버퍼를 앞으로 당겨준다.
- OnRead 및 OnWrite 메서드: Read 및 Write 커서의 위치를 이동
3. Session 클래스를 수정하여 RecvBuffer 인스턴스를 생성하고 사용했습니다. 또한 onRegisterRecv, OnRecvCompleted 메서드에서 버퍼 처리를 수정했습니다.
4. ServerCore의 Session 클래스, Server 클래스의 GameSession 클래스와 DummyClient 클래스의 GameSession클래스를 수정하여 OnRecv 메서드의 반환 값을 void에서 int로 변경했습니다.
이러한 변경으로 인해 RecvBuffer 클래스가 데이터 수신을 관리하며, 각 세션에서 이 클래스를 사용하여 버퍼 처리를 수행할 수 있습니다.
RecvBuffer를 개선해보는 시간을 가질거야.
패킷으로 넘어가기 위한 기초 작업의 일환.
지금까지 어떻게 사용하고 있었는지 살펴 보면,
Session에서
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
**_recvArgs.SetBuffer(new byte[1024], 0, 1024);**
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
_recvArgs를 초창기에 Start에서 SetBuffer를 해주고 있었어. 매개 변수로 버퍼랑 버퍼 시작 오프셋, 사이즈를 넘겨주고 있었어.
한번 설정해 준 다음에 어떤 변화도 주지 않았다. 설정만 하고 안바꿨으니까 나중에 쭉 내려가서,
Recv를 할 때 RgisterRecv 이 부분에서
void RegisterRecv()
{
bool pending = **_socket.ReceiveAsync(_recvArgs);**
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
건내준 메시지는 무엇이냐면,
**_recvArgs.SetBuffer(new byte[1024], 0, 1024);**
byte[1024]에다가 시작 오프셋은 0이고, 크기는 1024만큼을 Read를 할 수 있다고 간접적으로 얘기를 해준거야.
TCP 개론 시간에 애기 해봤지만 클라에서 100바이트를 보냈다고 해서 100바이트 온다는 보장이 없다. 클라에서 100바이트 짜리 패킷을 보냈으면 100바이트를 받아야 분석을 할 수 있음에도 불구하고, TCP 특성 때문에 80바이트만 올 수도 있다.
어떻게 처리해야 하냐면 80바이트만 왔다고 가정을 하면, 걔를 바로 처리할 수 없으니 그냥 recvBuffer에 보관만 하고 있다가 나중에 추가로 마지막 20바이트가 오면 걔를 조립해서 한번에 처리를 할 수 있도록 수정을 해야 한다.
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred >0 && args.SocketError == SocketError.Success)
{
try
{
// Start에서 **_recvArgs.SetBuffer(new byte[1024], 0, 1024); 이렇게 Buffer, Offset, BytesTransferred를 세팅해 줬었어.**
**OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
이런 식으로 OnRecv가 완벽한 상태로 왔다고 가정을 하고 그 다음 턴에서 또 Recv를 할 때 아까와 마찬가지로 처음 시작 오프셋 0부터 덮어쓰는게 아니라
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
**_recvArgs.SetBuffer(new byte[1024], 0, 1024);**
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
오프셋을 조정해서 80바이트 다음 위치부터 받는다는 얘기가 된다. SetBffer에서 0이 아니라 80으로 조정을 해서 다음 위치에서 부터 데이터를 받아가지고 조합을 해야 된다는 얘기가 된다.
코드를 만들어 보면서 설명을 해볼거야.
ServerCore에 가서 새로운 클래스를 만들어 주도록 할거야. RecvBuffer라는 새 클래스를 추가해준다.
RecvBuffer와 관련된 모든 기능들을 넣어줄거야. 클래스에 public을 붙인다.
using System;
using System.Collections.Generic;
using System.Text;
namespace ServerCore
{
public class RecvBuffer
{
// 얘가 버퍼가 되는 거. 10바이트짜리 배열이라고 가정을 하고 진행을 한다.
// [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
ArraySegment<byte> _buffer; // 엄청나게 큰 바이트 배열에서 부분적으로 잘라서 사용하고 싶을 수 있으니까, byte배열 대신 ArraySegment를 들고 있게 했다.
int _readPos;
int _writePos;
// 1. 일반적인 경우
// 이 두개의 변수는 커서라고 생각하면 된다. 처음 시작할 때는 둘다 첫번쨰 위치에서 시작할거야. 버퍼가 비어있는 상태
// [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
// 이 상태에서 클라이언트가 데이터를 5바이트 보냈다고 가정하면,
// [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
// 다음에 클라로부터 패킷을 받는다고 하면 w가 위치한 부분부터 이어서 받겠다는 얘기
// r은 반대로 Contents코드에서
// [r][ ][ ][ ][ ]
// 이쪽을 처리할지 말지를 결정을 해야한다. 만약 5바이트를 받았는데 5바이트가 하나의 패킷의 크기여서 처리할 수 있었다면
// [ ][ ][ ][ ][ ][r w][ ][ ][ ][ ]
// r도 이런식으로 이동을 하는 거. 근데 그게 아니라 패킷이 5바이트짜리가 아니라 8바이트 짜리 였다면
// [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
// r은 이 상태에서 다음 패킷까지 기다리면 변화를 주지 않을거야. 만약 추가로 3바이트가 온다고 가정하면
// [r][ ][ ][ ][ ][ ][ ][ ][w][ ]
// 이렇게 이동할거야. 이제 완성된 패킷이 있으니까 얘를 처리할 수 있을거야. 성공적으로 처리를 하면
// [ ][ ][ ][ ][ ][ ][ ][ ][r w][ ]
// 중간 중간에 정리를 해줘서
// [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
// 이렇게 처음으로 돌려보내 줄거야.
// 2. 패킷들이 다 2바이트 짜리라고 가정을 해보자.
// 정말 운이 나빴는지 4바이트 짜리 패킷이 와야 하는데 3바이트 짜리가 왔다가 가정해보자.
// [r][ ][ ][w][ ][ ][ ][ ][ ][ ]
// 데이터유효범위는 3번째 까지가 될거야. 여기서 클라쪽으로 넘기면 분석을 해보니까 첫2바이트는 처리를 할 수 있어.
// [ ][ ][r][w][ ][ ][ ][ ][ ][ ]
// 이제 데이터 유효 범위가 1바이트 밖에 안되는데 얘 같은 경우는 처리를 할 수 없어. 이 상태에서 대기를 해야 한다는 얘기.
// 이런식으로 계속 뒤로 밀리면 나중가면 버퍼의 공간이 부족하게 될 테니까 r이랑 w를 맨 처음으로 복사를 하는거.
// [r][w][ ][ ][ ][ ][ ][ ][ ][ ]
// 이 상태를 다시 분석을 해보면 읽기 시작하는 커서의 포지션은 첫칸이고, 쓰는 write 커서의 포지션은 두번쨰 칸이니까
// 나중에 AsyncRecv를 요청할 때는 w가 있는 칸부터 써달라고 요청을 하면 된다.
public RecvBuffer(int bufferSize) // 생성
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize); //초기화
}
public int DataSize { get { return _writePos - _readPos; } } // 버퍼에 들어 있는 아직 처리 되지 않는 데이터의 사이즈
public int FreeSize { get { return _buffer.Count - _writePos; } } // 버퍼에 남아있는 공간
public ArraySegment<byte> ReadSegment // 데이터 유효 범위의 세그먼트로 어디부터 데이터를 읽으면 되냐 요청, 이름이 마음에 안들면 DataSegment로 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
// ArraySegment의 새 인스턴스를 초기화합니다.<T> 지정된 배열에 있는 요소의 지정된 범위를 구분하는 structure입니다.
// 버퍼의 시작위치, 시작할 수 있는 오프셋, 처리되지 않은 데이터 크기를 넣어준다. 이게 데이터의 범위라고 볼 수 있다.
}
public ArraySegment<byte> WriteSegment // 다음에 리시브를 할 때 어디부터 어디가 유효범위인지, DataSegment라 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
// 버퍼의 시작 위치, w의 위치, 크기는 FreSize를 넣어주면 된다.
}
public void Clean() // 정리를 안하면 r,w가 버퍼 끝까지 가기 때문에 한번씩 처음으로 당겨줄 필요가 있다. 버퍼 고갈 방지
{
// [ ][ ][r][ ][ ][w][ ][ ][ ][ ]
// 이런 경우 데이터 유효범위 r부터 w전까지의 3바이튼 건들면 안되고 3바이트를 복사를 하고 w를 옮겨 줘야해
// [r][ ][ ][w][ ][][ ][ ][ ][ ]
// 근데 만약에 r이랑 w랑 위치가 겹치고 있다면?
// [ ][ ][ ][rw][ ][][ ][ ][ ][ ]
// 데이터가 없는 상태이니까
// [rw][ ][ ][ ][ ][][ ][ ][ ][ ]
// 커서만 시작 위치로 돌려 보내면 된다.
int dataSize = DataSize;
if(dataSize == 0) // r과 w가 겹치는 상태, 클라에서 보낸 데이터를 모두 처리한 상태
{
// 남은 데이터가 없으면 복사하지 않고 커서 위치만 리셋
_readPos = _writePos = 0;
}
else
{
// 남은 찌끄레기가 있으면 시작 위치로 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
//복사할 소스, 소스의 오프셋, 목적지, 목적지의 첫 위치, 크기
_readPos = 0; // 데이터를 시작 위치로 보냈으니 초기화
_writePos = dataSize; // 데이터 사이즈 만큼에 위치
}
}
public bool OnRead(int numOfBytes) // 컨텐츠 코드에서 데이터를 가공해서 처리를 할건데 성공적으로 처리했으면 OnRead를 호출해서 커서 위치를 이동해준다.
{
if (numOfBytes > DataSize) // numOfBytes만큼을 처리 했다 하는데 이게 DataSize보다 크면 문제있다는 것이니
return false;
_readPos += numOfBytes;
return true;
}
public bool OnWrite(int numOfByte) // 클라에서 데이터를 싸줘가지고 recive를 했을 때 그 때 write 커서를 이동시켜 주는 게 되는 거다.
{
if (numOfByte > FreeSize) // FreeSize보다 많이 받으면 말이 안될거야.
return false;
_writePos += numOfByte;
return true;
}
}
}
다시 Session으로 돌아가서
recvBuffer를 여기서 만들어 주고 있었는데
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
**_recvArgs.SetBuffer(new byte[1024], 0, 1024);**
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
그냥 class Session 처음 부분에
public abstract class Session
{
Socket _socket;
int _disconnect = 0;
**RecvBuffer _recvBuffer = new RecvBuffer(1024);**
이렇게 들고 있게 할거야. 크기는 일단 1024로 맞춰준다. 나중엔 늘리긴 해야 한다.
Start의 SetBuffer를 삭제해주고,
recvBuffer가 어느 시점에 호출이 되어야 하느냐
RegisterRecv할 때 여기서 뭔가가 이루어져야 한다.
Send 할 때도 Session의 RegisterSend를 하는 부분을 보면
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
**_pendingList**.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
**_sendArgs.BufferList = _pendingList;**
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
여기 저기서 데이터를 긁어가지고 _sendArgs에 연결을 해주고 있었는데
Recv도 마찬가지고 무조건 초기에 설정한 버퍼로만 하는게 아니라 RegisterRecv에서 현재 유효한 범위를 다시 한번 찝어줘야 한다.
void RegisterRecv()
{
**_recvBuffer.Clean(); // 혹시라도 커서가 너무 뒤로 이동하는 것을 방지한다.
ArraySegment<byte> segment = _recvBuffer.WriteSegment; // 버퍼에서 다음으로 데이터를 받을 공간을 WriteSegment로 관리하고 있었어.
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // segment.Count가 애당초 freeSize였어.**
// 이렇게 세팅해 준 것은 유효범위를 찝어준 것이다. Offset부터 Count만큼이 빈 공간이라고 찝어 줬으니까
// 그 상태에서 RecieveAsync를 하게 되면 어찌됐건 OnRecvCompleted가 호출될거야.
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
그리고 OnRecvCompleted가 호출이 될텐데 코드를 좀 넣어준다.
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred >0 && args.SocketError == SocketError.Success)
{
try
{
// 먼저 해야 할 건 Write 커서 이동
if(_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect(); // 혹시라도 버그 있는 경
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); // 기존코드
// OnRecv가 컨텐츠 코드에다가 데이터를 넘겨주는 부분이었는데 원래 코드 처럼 처음부터 끝까지가 유효범위가 아니라
// 유효 데이터 범위 만큼을 찝어서 넘겨줘야 하는데 그 인터페이스를 _recvBuffer.ReadSegment로 만들어 줬으니 그걸 넘겨준다.
**//OnRecv(_recvBuffer.ReadSegment);
// OnRecv를 하면 Server쪽의 가서 보면 OnRecv에서 받아서 최대한 처리 하려 하다가 만약에 패킷이 완성되지 않은 부분적인 데이터였다고 하면
// 다 처리를 못할테니 일부만 처리를 하게될거고, 그게 아니라 일반적인 경우라면 모든 데이터를 처리 할거야.
// 처리를 했는지 여부를 받고 싶으니까 OnRecv에 void를 받는게 아니라 int를 받게 인터페이스를 수정하자. Session, Server, DummyClient쪽 OnRecv의 코드를 수정한다.**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
Session으로 가서 OnRecv의 인터페이스를 수정해서 void가 아닌 int를 반환하도록 한다.
public abstract **int** OnRecv(ArraySegment<byte> buffer);
얼마만큼의 데이터를 처리했느냐를 뱉어주게 될거다.
Session에서 바꿔 줬으면 Server 쪽에 가서도 바꿔줘야 한다.
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; // 임시로 수정해 놓는다.**
}
그 다음에 DummyClient 쪽에서도 마찬가지로 수정한다.
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;
}
다시 Session으로 돌아가서 OnRecvCompleted에 가서
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred >0 && args.SocketError == SocketError.Success)
{
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();
}
}
3종 세트야. Write 커서를 이동 시키고, Contents코드로 패킷을 처리하려 하다가, 그걸 처리한 byte 수를 processLen으로 받아 줘서, 최종적으로 Read커서를 이동시켜 준거.
다시 RegisterRecv로 가면
void RegisterRecv()
{
**_recvBuffer.Clean();**
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
버퍼를 한번 정리하고 이어지게 된다.
일반적으로 이걸 빼지 않고 Session에다 합쳐서 만드는 경우가 많은데 선생님은 무조건 빼서 만드는게 좋다고 생각한다. 그래야 로직이 깔끔해지고 버그 확률도 낮아진다.
이렇게 패킷으로 만들 때를 대비해서 일부분만 처리 할 수 있게 기능을 만들어 줬다.
작업한 코드
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 Session
{
Socket _socket;
int _disconnect = 0;
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<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(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_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();
}
#region 네트워크 통신
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
}
}
RecvBuffer
using System;
using System.Collections.Generic;
using System.Text;
namespace ServerCore
{
public class RecvBuffer
{
// 얘가 버퍼가 되는 거. 10바이트짜리 배열이라고 가정을 하고 진행을 한다.
// [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
ArraySegment<byte> _buffer; // 엄청나게 큰 바이트 배열에서 부분적으로 잘라서 사용하고 싶을 수 있으니까, byte배열 대신 ArraySegment를 들고 있게 했다.
int _readPos;
int _writePos;
// 1. 일반적인 경우
// 이 두개의 변수는 커서라고 생각하면 된다. 처음 시작할 때는 둘다 첫번쨰 위치에서 시작할거야. 버퍼가 비어있는 상태
// [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
// 이 상태에서 클라이언트가 데이터를 5바이트 보냈다고 가정하면,
// [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
// 다음에 클라로부터 패킷을 받는다고 하면 w가 위치한 부분부터 이어서 받겠다는 얘기
// r은 반대로 Contents코드에서
// [r][ ][ ][ ][ ]
// 이쪽을 처리할지 말지를 결정을 해야한다. 만약 5바이트를 받았는데 5바이트가 하나의 패킷의 크기여서 처리할 수 있었다면
// [ ][ ][ ][ ][ ][r w][ ][ ][ ][ ]
// r도 이런식으로 이동을 하는 거. 근데 그게 아니라 패킷이 5바이트짜리가 아니라 8바이트 짜리 였다면
// [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
// r은 이 상태에서 다음 패킷까지 기다리면 변화를 주지 않을거야. 만약 추가로 3바이트가 온다고 가정하면
// [r][ ][ ][ ][ ][ ][ ][ ][w][ ]
// 이렇게 이동할거야. 이제 완성된 패킷이 있으니까 얘를 처리할 수 있을거야. 성공적으로 처리를 하면
// [ ][ ][ ][ ][ ][ ][ ][ ][r w][ ]
// 중간 중간에 정리를 해줘서
// [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
// 이렇게 처음으로 돌려보내 줄거야.
// 2. 패킷들이 다 2바이트 짜리라고 가정을 해보자.
// 정말 운이 나빴는지 4바이트 짜리 패킷이 와야 하는데 3바이트 짜리가 왔다가 가정해보자.
// [r][ ][ ][w][ ][ ][ ][ ][ ][ ]
// 데이터유효범위는 3번째 까지가 될거야. 여기서 클라쪽으로 넘기면 분석을 해보니까 첫2바이트는 처리를 할 수 있어.
// [ ][ ][r][w][ ][ ][ ][ ][ ][ ]
// 이제 데이터 유효 범위가 1바이트 밖에 안되는데 얘 같은 경우는 처리를 할 수 없어. 이 상태에서 대기를 해야 한다는 얘기.
// 이런식으로 계속 뒤로 밀리면 나중가면 버퍼의 공간이 부족하게 될 테니까 r이랑 w를 맨 처음으로 복사를 하는거.
// [r][w][ ][ ][ ][ ][ ][ ][ ][ ]
// 이 상태를 다시 분석을 해보면 읽기 시작하는 커서의 포지션은 첫칸이고, 쓰는 write 커서의 포지션은 두번쨰 칸이니까
// 나중에 AsyncRecv를 요청할 때는 w가 있는 칸부터 써달라고 요청을 하면 된다.
public RecvBuffer(int bufferSize) // 생성
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize); //초기
}
public int DataSize { get { return _writePos - _readPos; } } // 버퍼에 들어 있는 아직 처리 되지 않는 데이터의 사이즈
public int FreeSize { get { return _buffer.Count - _writePos; } } // 버퍼에 남아있는 공간
public ArraySegment<byte> ReadSegment // 데이터 유효 범위의 세그먼트로 어디부터 데이터를 읽으면 되냐 요청, 이름이 마음에 안들면 DataSegment로 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
// ArraySegment의 새 인스턴스를 초기화합니다.<T> 지정된 배열에 있는 요소의 지정된 범위를 구분하는 structure입니다.
// 버퍼의 시작위치, 시작할 수 있는 오프셋, 처리되지 않은 데이터 크기를 넣어준다. 이게 데이터의 범위라고 볼 수 있다.
}
public ArraySegment<byte> WriteSegment // 다음에 리시브를 할 때 어디부터 어디가 유효범위인지, DataSegment라 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
// 버퍼의 시작 위치, w의 위치, 크기는 FreSize를 넣어주면 된다.
}
public void Clean() // 정리를 안하면 r,w가 버퍼 끝까지 가기 때문에 한번씩 처음으로 당겨줄 필요가 있다. 버퍼 고갈 방지
{
// [ ][ ][r][ ][ ][w][ ][ ][ ][ ]
// 이런 경우 데이터 유효범위 r부터 w전까지의 3바이튼 건들면 안되고 3바이트를 복사를 하고 w를 옮겨 줘야해
// [r][ ][ ][w][ ][][ ][ ][ ][ ]
// 근데 만약에 r이랑 w랑 위치가 겹치고 있다면?
// [ ][ ][ ][rw][ ][][ ][ ][ ][ ]
// 데이터가 없는 상태이니까
// [rw][ ][ ][ ][ ][][ ][ ][ ][ ]
// 커서만 시작 위치로 돌려 보내면 된다.
int dataSize = DataSize;
if (dataSize == 0) // r과 w가 겹치는 상태, 클라에서 보낸 데이터를 모두 처리한 상태
{
// 남은 데이터가 없으면 복사하지 않고 커서 위치만 리셋
_readPos = _writePos = 0;
}
else
{
// 남은 찌끄레기가 있으면 시작 위치로 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
//복사할 소스, 소스의 오프셋, 목적지, 목적지의 첫 위치, 크기
_readPos = 0; // 데이터를 시작 위치로 보냈으니 초기화
_writePos = dataSize; // 데이터 사이즈 만큼에 위치
}
}
public bool OnRead(int numOfBytes) // 컨텐츠 코드에서 데이터를 가공해서 처리를 할건데 성공적으로 처리했으면 OnRead를 호출해서 커서 위치를 이동해준다.
{
if (numOfBytes > DataSize) // numOfBytes만큼을 처리 했다 하는데 이게 DataSize보다 크면 문제있다는 것이니
return false;
_readPos += numOfBytes;
return true;
}
public bool OnWrite(int numOfByte) // 클라에서 데이터를 싸줘가지고 recive를 했을 때 그 때 write 커서를 이동시켜 주는 게 되는 거다.
{
if (numOfByte > FreeSize) // FreeSize보다 많이 받으면 말이 안될거야.
return false;
_writePos += numOfByte;
return true;
}
}
}
DummyClient
using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
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);
}
}
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(100);
}
}
}
}
Server
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
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($"[FromClient] {recvData}");
return buffer.Count;
}
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)
{
;
}
}
}
}
'Server programming' 카테고리의 다른 글
02_14_네트워크 프로그래밍_PacketSession (0) | 2023.04.08 |
---|---|
02_13_네트워크 프로그래밍_SendBuffer (0) | 2023.04.07 |
02_11_네트워크 프로그래밍_TCP vs UDP (0) | 2023.04.06 |
02_10_네트워크 프로그래밍_Connector (0) | 2023.04.06 |
02_09_네트워크 프로그래밍_Session #4_엔진단과 컨텐츠단 분리 (0) | 2023.04.04 |
댓글