<Session #3>
1. 이전시간 강의 회고
- Receive Buffer는 빈 Buffer로 연결 vs Send Buffer는 보낼 데이터가 담겨있는 Buffer
- _sendArgs를 재사용하기 위해 클래스의 멤버 변수로 선언 -> rcvArgs도 재사용을 위해 멤버 변수로 가능(기호의 차이)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
class Session
{
Socket _socket;
int _disconnected = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false; // 누가 RegisterSend를 하고 있으면 True, OnSendComplete 완료 후에 False로 바꾼다: Send에서 누가 Registr를 사용하고 있으면 Queue에다가 저장만 하겠다.
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
// Recv Buffer 설정
_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 (_pending == false)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크통신
// 비동기는 두단계로 나눠짐
// 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();
}
}
}
// Receive
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
#endregion
}
}
2. 더 최적화 가능
:이전과 같이 Buffer를 하나만 두고 Buffer 별로 SendAsync를 하는 것이 아닌 BufferList를 통해 여러개의 버퍼를 리스트로 달아놓고 한번에 보낼 수도 있다.
* 이때, SetBuffer와 BufferList는 둘 중에 하나만 써야함
1) while문으로 _sendQueue.Count가 0일때까지 Dequeue로 꺼내온 byte[]를 buff로 _sendArgs.BufferList에 추가한다.
a) ArraySegment: Array의 일부를 나타내는 구조체: Stack에다가 할당
-> 사용하는 이유: C#에는 포인터 개념이 없기때문에 배열의 첫주소만 알 수 있다. 따라서, index를 넘겨줘서 몇번째부터 시작해야하는지 알려줘야한다.
void RegisterSend()
{
_pending = true;
//
while(_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
//
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
2) 이때, 위에 처럼 BufferList에 바로 Add를 해서 사용하면 안되고 무조건 BufferList = list; 이꼬르 상태로 List를 넘겨줘야한다.
* 이유는 없다.
void RegisterSend()
{
_pending = true;
List<ArraySegment<byte>> list = new List<ArraySegment<byte>>();
//
while(_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
list.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = list;
//
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
3) RegisterSend 함수를 호출할때마다 List를 만들지 말고 클래스의 멤버 변수로 하여 재사용하도록 한다. (중간 생략)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
class Session
{
Socket _socket;
int _disconnected = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false; // 누가 RegisterSend를 하고 있으면 True, OnSendComplete 완료 후에 False로 바꾼다: Send에서 누가 Registr를 사용하고 있으면 Queue에다가 저장만 하겠다.
// BufferList를 위한 List
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
#region 네트워크통신
// 비동기는 두단계로 나눠짐
// Send
void RegisterSend()
{
_pending = true;
_pendingList.Clear();
//
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);
}
#endregion
}
}
4) _pending bool 값을 통해 송신을 위해 대기 중인 정보의 여부를 판단 -> _pendingList.Count를 통해 대기 중인 정보가 있는지 없는지를 알 수 있다.
a) bool _pending값 필요없다.
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
5) OnSendCompleted 함수의 흐름도 약간 바꿈
a) Send한 후에 OnSendCompleted 함수로 돌아오면, _pending을 false로 하지않고 _sendArgs.BufferList를 null로 밀어버리고 이미 보낸 데이터가 저장되어 있는 _pendingList를 Clear 한다. -> _pending = false와 똑같은 동작
b) 완료된 동안에 또 _sendQueue에 데이터가 쌓여있으면 RegisterSend로 다시 등록한다.
c) OnSendCompleted 함수에서 _pendingList를 Clear하기 때문에RegisterSend 함수에서 Clear할 필요없다.
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// BufferList 와 _pendingList 초기화
_sendArgs.BufferList = null;
_pendingList.Clear();
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if(_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
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);
}
3. 코드의 전체 흐름 파악
1) Send를 했다고 가정하면 lock을 잡고 _sendQueue에 보낼 데이터를 넣어준다. 후에 _pendingList.Count 가 0이어서 보낼 데이터가 없었다고 하면 바로 RegisterSend를 하고 이미 보낼 데이터가 있었으면 단순히 Queue에 sendBuff만 저장한다.
2) RegisterSend 함수에 들어오면 (_pendingList에 아무것도 없으면) _sendQueue.Count가 0이 될 때까지 _sendQueue에서 데이터를 꺼내서 BufferList에 저장
3) 해당 BufferList를 SendAsync로 보내준다. 만약 바로 보낼 수 있으면 pending = false로 OnSendComplete를 해야하고 바로 보낼 수 없다면 나중에 보내진 후(비동기적으로) OnSendComplete를 한다 .
4) OnSendComplete 함수가 불려지면 현재 Send를 완료한 것이므로 다음 Send를 할 준비를 해야한다.
a) BufferList와 _pendingList를 초기화한다. (BufferList = null을 굳이 해줄 필요없지만 혹시 몰라 해줌)
b) _sendQueue의 Count가 0이 아니면 보내는 사이에 어떤 스레드가 보내야할 데이터를 Queue에 저장한 것이므로 또 한번 RegisterSend를 해준다.
* 위에는 굳이 안해줘도 어차피 OnSendComplete 함수가 종료되면 _pendingList.Count가 0이 되면서 RegisterSend가 되겠지만 먼저 해주면서 우아하게 코딩이 가능하다.
* Sessionc.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
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 void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
// Recv Buffer 설정
_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();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크통신
// 비동기는 두단계로 나눠짐
// Send
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
{
// BufferList 와 _pendingList 초기화
_sendArgs.BufferList = null;
_pendingList.Clear();
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if(_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
// Receive
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
#endregion
}
}
4. 결론
a) 짧은 시간동안 얼마의 데이터를 보냈는지를 추적하여 너무 심하게 많이 보내지 않도록 조절해야한다. (Throughput 조절해야함)
b) 작은 패킷으로 보내지않고 큰 Buffer를 보내어 성능을 개선할 수 있다.
- 아주 짧은 시간동안 100명의 User들이 어떤 행위를 하였는지에 대해 각각의 패킷을 뭉쳐서 큰 Buffer로 각 Users들에게 뿌리면 성능이 훨씬 개선된다.
- 콘텐츠 단에서 모아서 보내야하는지 vs 서버에서 모아서 보내야하는지에 대해서는 게임마다 다르다.
<Session #4>
1. Listener에서 OnAcceptHandler를 통해 Accept 한 후에 어떤 행동을 해줘야하는지 정의
-> Receive도 마찬가지로 어떻게 행동해야하는지 정의를 해줘야한다.
1) Listener.cs에서 Action<Socket> _onAcceptHandler를 통해 onAcceptHandler를 받고 콜백으로 OnAcceptComplete 함수에 들어오면 해당 함수에서 _onAcceptHandler를 Invoke 시킨다.
2. ServerCore는 서버 엔진 같은 존재, 콘텐츠는 Server에다가 만들게 됨
1) Session을 바로 사용하지 않고 Interface로 사용한다.
2) Event를 받는 방식을 어떻게 하는가?
- EventHandler를 받아서 연결
- Session을 상속받아서 만드는 방법
3) 우리가 필요한 부분 (인터페이스로 정리)
- OnConnected: 클라이언트의 접속이 완료되었다
- OnRecv: 클라이언트에서 받은 패킷을 받았다.
- OnSend: 보내는 것에 성공했다.
- OnDisconnected: 클라이언트와의 연결 해제
=> 외부에서는 위의 인터페이스만을 이용하지 이전시간에 구현했던 Register..., On~Complete 사용하지않는다.
// 인터페이스
public void OnConnected(EndPoint endPoint) { }
public void OnRecv(ArraySegment<byte> buffer) { }
public void OnSend(int numOfBytes) { }
public void OnDisconnected(EndPoint endPoint) { }
d) 위의 인터페이스를 Event를 받는 방식으로 한다면
- EventHandler로 받아서 연결: 아래와 같이 SessionHandler 클래스를 정의하고 Session 클래스에 SessionHandler를 받아서 연결
class SessionHandler
{
// 인터페이스
public void OnConnected(EndPoint endPoint) { }
public void OnRecv(ArraySegment<byte> buffer) { }
public void OnSend(int numOfBytes) { }
public void OnDisconnected(EndPoint endPoint) { }
}
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();
SessionHandler
- 상속을 받아서 정의 -> 더 편함
namespace SeverCore
{
class GameSession: Session
{
}
3. Session 클래스를 abstract 클래스로
1) 아래와 같이 추상클래스로 만들어서 Program.cs와 Session 부분을 연결시켜줘야한다.
-> 엔진과 컨텐츠를 분리를 하는 작업
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);
namespace SeverCore
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
}
public override void OnDisconnected(EndPoint endPoint)
{
}
public override void OnRecv(ArraySegment<byte> buffer)
{
}
public override void OnSend(int numOfBytes)
{
}
}
2) Disconnect 부분
a) Disconnect가 성공하면 OnDisconnected 호출하면 된다.
b) Program.cs에서 Disconnected한 후에 어떻게해야하는지 OnDisconnected에 정의하면 된다.
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
3) Receive 부분
a) OnRecvCompleted에서 args.Buffer를 인코딩해서 콘솔에 띄운 부분을 지우고 OnRecv를 호출하여 컨텐츠 단에서 해결하도록 한다. (Receive Handler와 연결)
b) OnRecv 인터페이스에서는 컨텐츠가 해야할 일을 정의: 여기서는 기존의 콘솔에 띄운 것을 코딩
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Receive Handler 연결
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
public override void OnRecv(ArraySegment<byte> buffer)
{
// 성공 -> 버퍼의값을 받는다.
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Client] {recvData}");
}
4) Send 부분
a) 위에와 마찬가지로 OnSend로 Handler를 연결한다.
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// BufferList 와 _pendingList 초기화
_sendArgs.BufferList = null;
_pendingList.Clear();
// Send Handler 연결
OnSend(_sendArgs.BytesTransferred);
if(_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
5) Connected 부분
a) 클라이언트가 서버와 Connected를 하는 부분은 Program.cs에서 OnAcceptHandler로 정의하여 Listener.cs에서 OnAcceptComplete 함수에서 onAcceptHandler를 Invoke 켜서 클라이언트와 연결이 됐음을 알렸다.
- OnAcceptHandler는 현재 콘텐츠 단에 있는데 OnAcceptHandler 함수 안에 Session.Start하는 부분은 엔진 단으로 해야함
b) OnAcceptHandler에서 session.Start하는 부분을 Listener.cs의 OnAcceptCompleted로 옮겨서 콘텐츠단과 엔진 단 분리
- 문제: GameSession을 강제로 OnAcceptHandler에서 만들어주고 있음. 하지만 GameSession은 콘텐츠단에서 인터페이스를 정의 중
-> 나중에 GameSession이 아닌 다른 클래스의 이름으로 상속받을 수 있는데 Listener.cs(엔진)에서 GameSession으로 강제할 수 없다.
void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
{
// 에러가 없이 실행이 되었다.
if (args.SocketError == SocketError.Success)
{
// TO DO
GameSession session = new GameSession();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
// 다음 턴을 위해 다시 Register 등록
RegisterAccept(args);
}
c) Listener.cs에서 Action<Socket>으로 OnAcceptHandler를 받는게 아니라 Func<Session> _sessionFactory로 받도록한다.
a) Func 델리케이트와 Action 델리게이트는 같은 역할을 하지만, 가장 큰 차이점은 Func는 결과값이 반환되느냐의 차이
-> 반환값이 없이 사용하고 싶으면 Action을 사용한다.
b) 아래의 코드와 같이 Listener.cs.에서 Func<Session> _sessionFactory로 Session값을 return하는 Func를 만든다.
- Init 함수에서 OnAcceptHandler를 받지않고 Func<Session>을 인자로 받아서 delegate로 연결해준다.
class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory; // _onAcceptHandler에다가 연결
c) OnAcceptComplete 함수에서 new GameSession 객체를 생성하는 것이 아닌 delegate로 연결된 _sessionFactory.Invoke() 로 생성된 Session 객체를 반환하도록한다.
d) Program.cs에서는 _listener.Init에서 람다로 GameSession을 생성하여 반환하도록한다.
-> 이렇게하면 컨텐츠 단에서는 Session을 상속받은 GameSession을 생성하여 엔진에게 넘겨주고 엔진은 컨텐츠의 내용에 상관없이 클라이언트와 Connect, Receive, Send, Disconnect 하게 됨
-> 컨텐츠단과 서버 엔진단이 분리가 되었다.
* Program.cs에서 클라이언트한테 Send하고 Thread.Sleep하고 Disconnect하는 부분은 우선 OnConnected에다가 하도록: OnAcceptHandler 필요없게 됨.
6) 실행하면 이전과 같이 프로그램이 동작
4. 문제 사항
- Listener.cs에서 OnAcceptComplete 함수에서 session.OnConnected의 인자로 args.AcceptSocket.RemoteEndPoint를 주고 있는데 그 사이에 클라이언트가 연결을 끊으면 문제가 발생한다.
* Program.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
// 컨텐츠 단
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// SendBuff만 남겨둠
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($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening....");
while (true)
{
}
}
}
}
* Listener.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory; // _onAcceptHandler에다가 연결
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
for(int i = 0; i < 10; i++)
{
// 초기화 시에 이벤트 등록
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args); // 최초 한번은 args를 등록
}
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 이전의 데이터 날리기
args.AcceptSocket = null;
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
{
// 에러가 없이 실행이 되었다.
if (args.SocketError == SocketError.Success)
{
// TO DO
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket); // AcceptSocket이 클라이언트의 소켓
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
// 다음 턴을 위해 다시 Register 등록
RegisterAccept(args);
}
public Socket Accept()
{
return null;
}
}
}
* Session.cs 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
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);
// Recv Buffer 설정
_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();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
// Disconnected Handler 연결
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크통신
// 비동기는 두단계로 나눠짐
// Send
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
{
// BufferList 와 _pendingList 초기화
_sendArgs.BufferList = null;
_pendingList.Clear();
// Send Handler 연결
OnSend(_sendArgs.BytesTransferred);
if(_sendQueue.Count > 0)
{
RegisterSend();
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
// Receive
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Receive Handler 연결
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed{e}");
}
}
else
{
// TO DO Disconnect
Disconnect();
}
}
#endregion
}
}
'Development > 게임 서버' 카테고리의 다른 글
[Part 4 게임 서버] 네트워크 프로그래밍 6 (RecvBuffer, SendBuffer) (0) | 2022.01.21 |
---|---|
[Part 4 게임 서버] 네트워크 프로그래밍 5(Connector, TCP vs UDP) (0) | 2022.01.19 |
[Part 4 게임 서버] 네트워크 프로그래밍 3(Session #1, Session #2) (0) | 2022.01.16 |
[Part 4 게임 서버] 네트워크 프로그래밍 2 (Listener) (0) | 2022.01.16 |
[Part 4 게임 서버] 네트워크 프로그래밍 1(네트워크 기초 이론 ~ 소켓 프로그래밍 입문 #2 (0) | 2022.01.14 |