728x90
반응형
이번 포스트에서는 기존의 Listener 예제에 Session을 추가하는 방식으로 멀티 네트워크를 구현하겠습니다.
그리고 기존의 Listener의 역할을 조금 더 확대하여
Listener에서 Accept를 수행하도록 하겠습니다.
이런 방식으로 수정하는 경우, 클라이언트가 아닌 서버에서 안전하게 멀티 네트워크를 개시할 수 있습니다.
1) Session
일단 현재 만들어진 세션은 서버에서만 동작할 예정입니다.
세션은 크게 2가지의 역할을 수행합니다.
- 서버에서 클라이언트로 데이터를 전송 = Send
- 클라이언트에서 전송된 데이터를 서버에서 받음 = Recv
따라서 위의 2가지 역할을 수행하기 위한 코드를 만들어보겠습니다.
abstract class Session
{
Socket _socket;
// 만약, 클라이언트 세션이 Disconnect 되었다면 Flag를 통해 이를 판단
int _disconnnected = 0;
// 락을 위한 오브젝트, 락을 걸어 자원에 대한 원자성을 보장
object _lock = new object();
// Send할 데이터를 저장하는 큐, Send가 들어올 때마다 데이터를 보내지 않고 대신 Head를 우선 보냄
Queue<byte[]> _sendQueue = new Queue<byte[]>();
// Send 이벤트에서 사용할 BufferList를 설정하기 위한 리스트 = 대기중인 목록
List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
// Send 이벤트마다 새로 생성되는 문제를 막고자 다음과 같이 프로퍼티로 설정하여 캐싱해놓음
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
// Recv 이벤트마다 새로 생성되는 문제를 막고자 다음과 같이 프로퍼티로 설정하여 캐싱해놓음
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
// Connect 이벤트 발생 시 호출
public abstract void OnConnected(EndPoint endPoint);
// Recv 이벤트 발생 시 호출
public abstract void OnRecv(ArraySegment<byte> buffer);
// Send 이벤트 발생 시 호출
public abstract void OnSend(int numOfBytes);
// Disconnect 이벤트 발생 시 호출
public abstract void OnDisconnected(EndPoint endPoint);
// ClietnSession 네트워크 통신 준비
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();
}
// 서버 -> 클라이언트로 데이터 전송
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendinglist.Count == 0)
RegisterSend();
}
}
// Client Session 연결 종료
public void Disconnect()
{
// Flag 변환을 멀티 스레드를 고려하여 Interlocked를 사용하여 수행함
if (Interlocked.Exchange(ref _disconnnected, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
// Client Session이 클라로부터 데이터를 받음
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
// Recieve 이후에 서버가 데이터를 바로 받았다면, OnRecvCompleted를 즉시 호출
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
// RegisterRecv 함수로부터 호출되어, 클라이언트로부터 받은 데이터를 처리
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
// 클라이언트로부터 받은 데이터의 양이 0보다 크고 동시에 Socket 연결이 성공했다면
if (_recvArgs.BytesTransferred > 0 && _recvArgs.SocketError == SocketError.Success)
{
try
{
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"From Client Recv Error : {e.ToString()}");
}
}
// 그렇지 않다면 연결 해제
else
{
Disconnect();
}
}
// Client Socket이 서버로부터 데이터를 전송
void RegisterSend()
{
// BufferList를 설정
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);
// Send 이후에 서버가 데이터를 바로 보냈다면, OnSendCompleted를 즉시 호출
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
// RegisterSend 함수로부터 호출되어, 서버로부터 보낼 데이터를 처리
void OnSendCompleted(object handler, SocketAsyncEventArgs args)
{
lock (_lock)
{
// 서버로부터 보낸 데이터의 양이 0보다 크고 동시에 Socket 연결이 성공했다면
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null;
_pendinglist.Clear();
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"From Server Send Error : {e.ToString()}");
}
}
// 그렇지 않다면 연결 해제
else
{
Disconnect();
}
}
}
}
Session 클래스의 경우, abstract 클래스 즉 추상 클래스로 선언되어 반드시 상속되어야 합니다.
상속하는 경우 이벤트 발생 시에 수행해야 하는 함수들을 오버라이드해야 합니다.
오버라이드하는 함수들은 다음의 4개입니다.
- OnConnected : 연결시에 호출
- OnDisconnected : 연결이 끊길시에 호출
- OnRecv : 데이터를 받는 경우 호출
- OnSend : 데이터를 보내는 경우 호출
또한 이번에는 SetBuffer가 아닌,
BufferList를 사용하여 서버가 Send하고자 하는 데이터를 배열 형태로 저장합니다.
이 경우에, Send할 때리스트에 저장된 모든 목록을 한번에 처리할 수 있습니다.
단 SetBuffer나 BufferList 중에서 반드시 하나만을 선택하여 사용해야 합니다.
Session의 진행 절차를 간략하게 설명하자면 다음과 같이 설명할 수 있습니다.
1 - 1) 클라이언트에서 보내진 데이터를 Recv 하는 경우
- 공통적으로 Start 함수에서 Recv와 Send 시에 사용하기 위한 비동기 소켓인자를 설정합니다.
- 이때 Recv의 비동기 소켓인자에는 OnRecvCompleted 함수를 연결합니다.
- 이어서 Recv한 데이터를 저장하기 위한 버퍼를 설정합니다.
- Start 함수에서 RegisterRecv를 호출하여 Recv하기 위한 준비를 수행합니다.
- RegisterRecv를 수행한 직후 바로, 데이터를 받은 경우 OnRecvCompleted를 호출합니다.
- 그렇지 않은 경우, 다른 작업을 수행합니다.
- OnRecvCompleted에서는 Recv된 데이터를 확인하여 OnRecv 함수를 수행합니다.
- 이후에 RegisterRecv를 호출하여 다시 Recv하기 위한 준비를 수행합니다.
1 - 2) 서버에서 클라이언트로 데이터를 Send 하는 경우
- Start 함수에서 Send 시에 사용하기 위한 비동기 소켓인자를 설정합니다.
- 이때 Send의 비동기 소켓인자에는 OnSendCompleted 함수를 연결합니다.
- Send 함수에서 Lock을 걸고 _sendQueue에 send할 데이터를 넣습니다.
- 만약 Send를 위한 예약목록인 _pendingList가 비어있다면 즉시 RegisterSend를 수행합니다.
- 비어있지 않다면 일단 SendQueue에만 넣어놓습니다.
- RegisterSend 함수에서는 _sendQuque에 있던 데이터를 하나하나 빼와 _pendingList에 넣습니다.
- 채워진 _pendingList를 _sendArgs의 BufferList로 설정합니다.
- 7의 절차로 인해 BufferList는 Send하기 위한 모든 예약 목록을 갖고 있게 됩니다.
- SendAsync를 통해 비동기적으로 Send를 수행합니다.
- 이때 바로 Send가 가능하다면 OnSendComplete를 수행하지만, 가능하지 않다면 다른 작업을 수행합니다.
- OnSendComplete 함수에서는 예약 목록에 있던 데이터를 Send합니다.
- 그리고 _sendQueue가 남아있다면 다시 RegisterSend 함수를 호출해 Send하기 위한 준비를 합니다.
728x90
반응형
'C#' 카테고리의 다른 글
Connect를 위한 Connector 생성 (0) | 2024.12.25 |
---|---|
Session_2 (0) | 2024.12.24 |
Listener (0) | 2024.12.18 |
소켓 프로그래밍 기초 (0) | 2024.12.16 |
네트워크 기초 + 통신 모델 (0) | 2024.12.11 |