반응형
Listener 복습
using System.Net;
using System.Net.Sockets;
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler; //연결 수락시 실행할 콜백 함수
//문지기 교육
_listenSocket.Bind(endPoint);
//영업 시작
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs(); //한번 만들면 계속 재사용
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); //완료되면, OnAcceptCompleted 실행
RegisterAccept(args);
}
//비동기에선 Accept 요청과 완료가 분리되어야 함
//Register 와 Completed가 뺑뺑이를 돌면서 계속 실행됨
void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null; //재사용이므로 초기화 시키고 사용
bool pending = _listenSocket.AcceptAsync(args); //비동기로 예약
if(pending == false) //완료가 된 상태
{
OnAcceptCompleted(null, args);
}
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
//TODO
_onAcceptHandler.Invoke(args.AcceptSocket);
//완료됐으므로, 동작 처리
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
//이번 꺼는 완료가 됐으므로, 다음 아이를 위해 재등록
}
}
- pending이 계속 false로 걸린다면?
- Completed와 Register가 계속 뺑뻉이를 돌면서 stack overflow가 나지 않을까?
- 그러나 10개의 대기열 제한을 걸어 두었기 때문에 이 상황에선 일어나지 않음
- Completed와 Register가 계속 뺑뻉이를 돌면서 stack overflow가 나지 않을까?
- 문지기가 하나밖에 없는데, 유저가 몰린다면?
- 다음과 같이 코드 수정 -> 문지기 늘리기
SocketAsyncEventArgs args = new SocketAsyncEventArgs(); //한번 만들면 계속 재사용
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); //완료되면, OnAcceptCompleted 실행
RegisterAccept(args);
//이 부분을 아래처럼
for(int i=0; i<10; i++) {
SocketAsyncEventArgs args = new SocketAsyncEventArgs(); //한번 만들면 계속 재사용
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); //완료되면, OnAcceptCompleted 실행
RegisterAccept(args);
}
- Main에서는 while(true)를 계속 돌고 있는데, 도대체 콜백함수 실행 될 때 어떻게 알고 껴드는가?
- 우리가 쓰레드를 따로 만들지는 않았지만, Async를 썼으므로 쓰레드가 생기는 것
- 그렇기 때문에, 만약 Main과 OnAcceptCompleted에서 같은 데이터를 건드린다면?
- Race Condition 문제가 발생함 -> Red Zone이다
- 현재는 Accept만 해주기 때문에 문제가 없지만, Recv, Send를 한다면 문제가 생긴다.
- 그렇기 때문에, 만약 Main과 OnAcceptCompleted에서 같은 데이터를 건드린다면?
- 우리가 쓰레드를 따로 만들지는 않았지만, Async를 썼으므로 쓰레드가 생기는 것
1) Recv 분리, 비동기화 -> Session
Session은 항상 멀티쓰레드라고 염두에 두고 코딩할 것
- Send 부분은 일단은 임시로 블로킹 코드로 구현, 이후에 수정
//Recv 담당
class Session
{
Socket _socket;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼, 시작점, 사이즈
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
public void Disconnect()
{
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false) //바로 성공
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//0개의 바이트가 오거나 Success가 아닌 경우, 모두 실패한 경우임
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//TODO
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv(args);
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
class Program //Main
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
session.Start(clientSocket);
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
static void Main(string[] args)
{
//DNS 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
- 만약 멀티쓰레드 환경에서 돌다가 Disconnect 한 세션이 한번 더 Disconnect 한다면?
- 당연히 문제가 발생함 -> 해결하자 -> Lock을 써보자!
int _disconnected = 0;
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
//이미 Disconnect가 되었다면 1이 반환되므로 바로 리턴
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
- 이걸 추가해보면, 한 세션당 한 번의 disconnect만 발생할 것이다.
2) Send 분리 -> Session에 이어서
- Send의 경우 Recv와 얘기가 다르다.
- Recv와 다르게 Send의 시점이 정해져 있지 않음! -> 예약이 안된다.
- Recv는 상대가 보내면, 그때 예약걸린 일을 처리하면 되지만
- Send는 내가 보내야 하니까 예약이란게 없음
public void Send(byte[] sendBuff)
{
SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length); //버퍼, 시작점, 사이즈
RegisterSend(sendArgs); //얘는 보낼 내용물이 전부 들어있는 것
//보낼 때마다, 새로운 통을 준비해서 내용물을 담는 것
//Recv의 경우에는 무언가 오면 담을 통만 준비해뒀음
}
void RegisterSend(SocketAsyncEventArgs args)
{
bool pending = _socket.SendAsync(args);
if (pending == false)
OnSendCompleted(null, args);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
//성공, 실패 판단은 똑같음
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//Send는 재사용이 불가능
//여기에서 args에는 전에 보냈던 버퍼가 그대로 남아있음 -> 그대로 재사용하면 안되지
//사실 Send는 성공하고 나면 다음에 할 게 없음
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
3) Send 개선 -> 재사용성, 보낼 버퍼 모아서 처리하기
- 하지만 많은 유저가 움직이고 있고, 다른 모든 유저에게 그 움직임의 정보를 전달해야 함
- 그럴 때마다 새로 만들어서 SendAsync를 하나하나 한다는건 말도 안됨
- Send를 좀 모아서 한번에 보내는게 좋지 않을까?
- SendAsync도 재사용하면 좋고
- SendAsync를 재사용하면서, Send - Completed가 계속 순환하게
- Completed가 완료될때까지는 Send Queue에 쌓아두다가, 완료 되면 비우고 다시 쌓기
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false; //한번이라도 send를 했으면 true, 전부 완료되면 false
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼, 시작점, 사이즈
//이 부분 추가, 최초 1회만 만들기
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv(recvArgs); //얘는 받을 통만 준비한 것
}
public void Send(byte[] sendBuff)
{
lock (_lock) //Send가 여러 쓰레드에서 호출되므로 락 필수
{
_sendQueue.Enqueue(sendBuff); //큐에다가 집어넣고
if (_pending == false) //1빠로 Send요청을하여 등록도 안된 경우
RegisterSend();
//true라면, Send작업이 진행중인 것이고, 지금 진행중인게 완료될때까지 큐에 넣기만 하고 대기
}
}
void RegisterSend() //얘는 이미 락이 걸려있는 부분에서 호출한게 아니면 호출될 여지가 없음
{
_pending = true; //작업중이라고 해놓고
byte[] buff = _sendQueue.Dequeue(); //큐에서 뽑기
_sendArgs.SetBuffer(buff, 0, buff.Length);
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
{
if(_sendQueue.Count > 0) //보내는 동안 또 누군가 전송 요청을 한 경우
RegisterSend(); //큐가 남아있다면 다시 전송
else
_pending = false; //성공적으로 끝났으므로 초기화
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
}
4) Send 개선 -> Queue에 있는 모든 버퍼를 한번에 처리하기
- 그런데 한번에 하나씩 Register - Complete하는것도 비효율적이지 않나?
- 아래처럼 수정하자
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
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.Clear();
Console.WriteLine($"Transferrd bytes: {_sendArgs.BytesTransferred}");
if(_sendQueue.Count > 0) //보내는 동안 또군가 전송 요청을 한 경우
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
}
- 여기서 주의할 점은 반드시 리스트를 따로 만들고, BufferList = list 를 해줘야 정상 작동한다는 것이다.
- 이렇게 수정하면 _pending 플래그도 필요 없어진다.
- 큐에 있는 버퍼들을 한번에 List로 모아서 처리를 하는 것
5) Send 개선 -> 트래픽 몰릴 경우, 비정상적 요청 처리
- 하지만 문제가 또 있다.
- 한번에 과도하게 많은 양을 보내게 되면 오류가 발생
- 특히 디도스로 의미 없는 정보를 계속 보내는 경우
- 그러므로, 몰렸을때 조금 쉬는 등의 동작이 필요
- 한번에 과도하게 많은 양을 보내게 되면 오류가 발생
- 또한, 수천명의 유저가 움직이고 스킬쓰고 한 모든 동작들을 하나의 큰 버퍼로 모아서
- 순회하면서 각 유저에게 뿌린다면 더 효율적이지 않을까?
Session 개선 -> 받고 보내고 끝이 아닌, 사후 처리 + 내부, 외부 동작 분리
- 싹 뜯어고치자.
- 내부동작부분과 외부 동작부분을 분리
- 내부: Register, Completed 전부
- 외부: 동작 완료된 후 사후 처리
//Accept 담당
class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory; //세션을 만들어 주는 것
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory;
//문지기 교육
_listenSocket.Bind(endPoint);
//영업 시작
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs(); //한번 만들면 계속 재사용
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); //완료되면, OnAcceptCompleted 실행
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args);
if(pending == false)
{
OnAcceptCompleted(null, args);
}
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
}
}
//Recv, Send 담당
abstract class Session
{
Socket _socket;
int _disconnected = 0;
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 void 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);
_recvArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼, 시작점, 사이즈
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
public void Send(byte[] sendBuff)
{
lock (_lock) //Send가 여러 쓰레드에서 호출되므로 락 필수
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0) //1빠로 Send요청을하여 등록도 안된 경우
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
OnDisConnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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.Clear();
OnSend(_sendArgs.BytesTransferred);
if(_sendQueue.Count > 0) //보내는 동안 또군가 전송 요청을 한 경우
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
}
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) //바로 성공
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//0개의 바이트가 오거나 Success가 아닌 경우, 모두 실패한 경우임
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
//Session의 사후 처리 담당
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(1000);
Disconnect();
}
public override void OnDisConnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisConnected : {endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Client] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferrd bytes: {numOfBytes}");
}
}
//메인 코어
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
//DNS 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
반응형
'C# > 네트워크' 카테고리의 다른 글
[c#][서버] TCP vs UDP (0) | 2024.06.07 |
---|---|
[c#][서버] Connector (0) | 2024.06.07 |
[c#][서버] Listener (0) | 2024.06.05 |
[c#][서버] 소켓 프로그래밍 (0) | 2024.06.05 |
[c#][서버] 네트워크 기초 이론 (0) | 2024.06.04 |