서론
비유 : 휴대폰을 래핑해서 간단하게 사용할 수 있게만든 클래스
Session에는
Receive Send DisConnect 기능을 모듈형으로 만들어놓은 클래스다
연결, 연결해제 및 데이터를 주고 받고 해주는 클래스를 만들것이다
먼저 Receive → Send → DisConnect순으로 설명하겠다
우리는 이전시간에 Async계열로 Listener를 만들었다
이번에 Session도 Async계열로 만들것.
Connect는 따로 만듬
✨미리 흝어보면 좋은 클래스 및 구조체
1. EndPoint
2. SocketAsyncEventArgs
3. ArraySegment1️⃣ EndPoint
네트워크 통신의 주소를 표현하는 추상 클래스다
즉 데이터를 보내거나 받을 때 어디로 보낼지 어디서 받을지 지정하는 개념이라 보면 됨
c#에서는 EndPoint 상속 받은 IPEndPoint를 사용
IPEndPoint는 Ip주소 + 포트 번호로 구성됨
IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 7777);RemoteEndPoint
원격 측의 주소 정보를 나타내는 프로퍼티
즉 현재 소켓이 연결되어 있는 상대방(클리어언트, 서버)의 IP+ Port정보를 담고있음
서버 측 사용
Socket clientSocket = listener.Accept();
Console.WriteLine($"클라이언트 접속: {clientSocket.RemoteEndPoint}");클라 측 사용
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect("127.0.0.1", 7777);
Console.WriteLine($"서버에 연결됨: {socket.RemoteEndPoint}");Aysnc 측 사용 (IOCP 환경)
void OnAcceptCompleted(object sender, SocketAsyncEventArgs e)
{
Socket client = e.AcceptSocket;
Console.WriteLine($"[접속] {client.RemoteEndPoint}");
}2️⃣ SocketAsyncEventArgs
비동기 소켓 통신을 위해 설계된 이벤트 인자 클래스
해당 기능에는 AcceptAsync나 ReceiveAsync, SendAsync등 비동기 이벤트 기반으로 처리가 가능
주요 프로퍼티 설명
1. SocketAsyncEventArgs.Buffer : 송수신할 데이터 버퍼
2. SocketAsyncEventArgs.BytesTransferred : 실제 전송된 바이트 수
3. SocketAsyncEventArgs.SocketError : 작업 성공 여부
4. SocketAsyncEventArgs.AcceptSocket : 새로 연결된 소켓 Accept 시
5. SocketAsyncEventArgs.Completed : 비동기 작업 완료 시 발생하는 이벤트
6. SocketAsyncEventArgs.ConnectSocket : 연결된 소켓 Connect 시사용 예시
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted)
_recvArgs.SetBuffer(new byte[1024], 0, 1024)
-----------------------------------------------------------------------
private void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (!pending)
OnRecvCompleted(null, _recvArgs);
}
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
OnReceive(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch (Exception ex)
{
Console.WriteLine($"OnRecvCompleted Failed {ex.Message}");
}
}
else
{
Disconnect();
}
}
3️⃣ ArraySegment
배열의 일부분을 잘라 참조하기 위한 구조체
즉 배열의 일부 영역을 복사 없이 가리키는 슬라이스 개념
byte[] buffer = new byte[1024];
ArraySegment<byte> segment = new ArraySegment<byte>(buffer, 0, 512);이렇게 하면 buffer 배열의 0~511번 인덱스 영역만 가리키는 구조체가 만들어짐.
장점
메모리 복사 없이 배열 조각을 전달 가능 버퍼를 효율적으로 전달하기 위해서 Send를 하면 최적화가 필요하다 그래서 ArraySegment이용해서 모아서보내기 최적화를 사용할 수 있다
이전 버전
byte[] sendBuff = _sendQueue.Dequeue();
_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length);최적화 버전
while (_sendQueue.Count > 0)
{
byte[] sendBuff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(sendBuff, 0, sendBuff.Length));
}
_sendArgs.BufferList = _pendingList;Receive
Receive를 호출하기 전 이벤트 설정
- Async계열을 사용할 거 이기에 콜백이 필요함
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
//버퍼 셋팅
_recvArgs.SetBuffer(new byte[1024], 0, 1024);Receive등록 시켜놓는 함수
- 처음 Start나 Init부분에 한번만 호출해서 한개의 루프만 돌리게끔 처리 해야함
private void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (!pending)
OnRecvCompleted(null, _recvArgs);
}Receive 데이터가 왔을 때 호출되는 함수
- 받는 데이터가 있고 해당 데이터를 문제없이 받았으면
다시 RegisterRecv를 돌려서 루프를 만들어줌
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//BytesTransferred 내가 데이터를 얼마나 받앗는지
//0이 오는경우도있다
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
//Offset 어디서부터 시작하는지
//BytesTransferred 몇 바이트를 받았는지
try
{
OnReceive(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
//string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
//Console.WriteLine($"[From Client] {recvData}");
RegisterRecv();
}
catch (Exception ex)
{
Console.WriteLine($"OnRecvCompleted Failed {ex.Message}");
}
}
else
{
Disconnect();
}
}Send
Send는 Receive랑 비슷하지만 다르게 짜야한다
왜냐? Receive는 커널에서 호출해주는 이벤트에 반응하는
이벤트 성이라 한개의 루프만 돌리면되지만
Send는 내가 직접 원하는 시점에 호출해야한다 즉, 여러 번 Async가 호출될 수 도 있는데 성능에 문제가 생길 수 있으므로 Queue나 ArraySagment를 사용해서 한번에 모아서 처리해주는 방식으로 해주면 일단은! 최적화에 도움이 된다
또한 데이터가 바뀔수 있는 RaceCondition상황이 있기에 Lock을 사용해서 처리해준다
Send를 호출하기 전 이벤트 설정
- Async계열을 사용할 거 이기에 콜백이 필요함
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);Send함수 (Lock 처리)
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
//1) _pendingList 대기중인 데이터가 없다고 판별하니 RegisterSend
//2) 만약 예약중인 데이터가 있으면_sendQueue만 넣고 나옴
if (_pendingList.Count == 0)
RegisterSend();
}
}Send등록 시켜놓는 함수
- Queue와 ArraySegment를 사용해서 한번에 데이터를 모아서 보내는 방식으로 처리
private void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] sendBuff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(sendBuff, 0, sendBuff.Length));
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}Send 데이터가 왔을 때 호출되는 함수
private void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null;
_pendingList.Clear();
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception ex)
{
Console.WriteLine($"OnSendCompleted Failed {ex.Message}");
}
}
else
{
Disconnect();
}
}
}DisConnected
작업을 종료 할때 호출함 연속으로 호출되면 문제가 발생하기에 Interlocked계열을 사용해서 내가 만든 변수가 변경이 되었다면 DisConnected가 된거이기에 연속으로 호출을 막아줌
public void Disconnect()
{
int desired = 1;
if (desired == Interlocked.Exchange(ref _disconnected, desired))
return;
OnDisConnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}