Listener
우리가 이전에 만든 소켓 프로그래밍 코드에서 ServerCore 메인 함수에서 모든걸 다루니까 그것을 분리해보자.
Listener 클래스를 만들어서 작성해보자.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Listener
{
Socket _listenSokcet;
public void Init(IPEndPoint endPoint)
{
Socket _listenSokcet = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSokcet.Bind(endPoint);
// 영업 시작
// backiog : 최대 대기수
_listenSokcet.Listen(10);
}
public Socket Accept()
{
return _listenSokcet.Accept();
}
}
}
코드를 이렇게 옮겨 오면 되는데 여기서 맘에 안드는 부분이 하나 있다. 바로 Accept함수이다. Accept함수는 블로킹 계열의 함수로 클라이언트의 socket이 Connect하기 전까지 프로그램이 멈추고 기다리는 특성이 있다.
게임 서버를 개발할 때, 만약 블로킹 계열의 함수를 사용한다면 유저들간 소통을 해야하는데 한명 한명 소통할 때마다 계속 멈추게 될테니 최대한 사용을 자제해야한다.
그렇기 때문에 입출력 계열의 함수는 대부분 비동기, 즉 Non 블로킹 계열을 사용해야한다.
_listenSokcet.AcceptAsync();
이 코드를 통해 비동기 방식으로 처리할 수 있다. Connect를 하던 말던 관심 없고 일단 값을 return하게 된다. 값이 return 되는 것이 무조건 좋은 것은 아니다. 만약 서버쪽에서 제대로 Accept가 되지 않았는데 값이 return 되어서 다음 코드로 넘어가 send나 receive를 하게된다면 에러가 날 것이다. 그렇기 때문에 어떤 방식이 되었든 유저가 연결 되었다는 것을 꼭 알려주어야 한다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Listener
{
Socket _listenSokcet;
public void Init(IPEndPoint endPoint)
{
Socket _listenSokcet = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSokcet.Bind(endPoint);
// 영업 시작
// backiog : 최대 대기수
_listenSokcet.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
bool pending = _listenSokcet.AcceptAsync(args);
if (pending == false)
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
// TODO
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
}
public Socket Accept()
{
return _listenSokcet.Accept();
}
}
}
이런식으로 코드를 작성할 수 있다. 이 부분은 진짜 진짜 진짜 중요한데 살펴보면 처음 Init을 통해 우리가 SocketAsynsEventArgs 를 하나 만들고 그것을 RegisterAccept를 통해 등록하게 된다. RegisterAccept 함수에서는 소켓의 비동기 Accept가 동작하게 되니 Client의 Connect가 아직 오지 않았다면 pending을 true로 설정하게 될것이다. 만약 운이 좋게 Client와 Server가 잘 맞아 떨어져 pending이 false로 설정된다면 바로 OnAcceptCompleted가 실행되게 하는 것이다.
pending이 true 인 상황에서 시간이 흘로 Client가 Connect를 하게되면 콜백함수로 등록되었던 OnAcceptCompleted가 실행된다. 그리고 처리가 끝나면 다시 서버를 비동기 Accpet 상황으로 만든다.
이 행위 자체가 기존의 동기형태의 Accept의 단점인 멈추고 기다리는 것을 막을 수 있다.
위의 흐름이 잘 이해되지 않는다면 낚시를 생각하면 된다.
맨 처음 우리가 강에 낚시대를 던질 것이다.(이벤트 등록 및 RegisterAccept 실행) 낚시대를 던지자 마자 입질이 온다면(pending이 false라면) 바로 낚시대를 들어올려(OnAcceptCompleted 실행) 물고기를 통안에 넣을 것이다.(에러 없이 실행되면 하고싶은거 처리)
만약 입질이 바로 오지 않는다면(pending이 true라면) 입집이 올때까지 기다린다.(Connect가 될때까지) 입질이 왔다면 그 위와 동일하다.
낚시가 끝나면(하고 싶은거 처리가 끝나면) 다시 강에 낚시대를 던져 반복한다.(다시 RegisterAccept를 통해 등록한다.)
그렇다면 우리가 하고싶은 일을 처리할 때는 어떻게 해야할까. 그것 역시 이벤트로 등록했다가 실행 될 때 그 이벤트를 동작하면 된다.
Listener.cs
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
internal class Listener
{
Socket _listenSokcet;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSokcet = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler = onAcceptHandler;
// 문지기 교육
_listenSokcet.Bind(endPoint);
// 영업 시작
// backiog : 최대 대기수
_listenSokcet.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null;
bool pending = _listenSokcet.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);
}
}
}
Action 이벤트로 내가 실행하고 싶은 함수를 등록하고 Socket이 Connect 됐을때 인자로 Connect된 clientSocket이 누군지 인자로 넘겨줌으로써, send와 recive를 실행하는 방식이다.
ServerCore.cs
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
// 받는다.
byte[] recvBuff = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"From Client {recvData}");
// 보낸다.
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome");
clientSocket.Send(sendBuff);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
// 쫓아낸다.
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
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);
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
DummyClient.cs
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient
{
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);
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Conneted to {socket.RemoteEndPoint}");
// 보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello, World!");
int sendBytes = socket.Send(sendBuff);
// 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"From Server {recvData}");
// 나간다.
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
Session(recive)
session을 만들어보고 send와 recive를 비동기를 만들어보자.
먼저 send보다 recive가 더 만들기 쉽기 때문에 먼저 다뤄보자.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
session.Start(clientSocket);
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
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);
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.UserToken = this;
recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_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)
{
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(e.ToString());
}
}
else
{
// TODO
// Disconnect
}
}
#endregion
}
}
위에서 다뤘던 Accept와 동일한 방식이다. 이벤트를 예약해두고 세션이 만들어졌을 때, 주어진 이벤트를 실행하는 것이다.
그렇기 때문에 recv는 조금 쉽다.
Session(send)
우리가 생각해보면 Recive랑 크게 차이가 없을거 같다. 그런데 문제가 하나 있는데 그 문제는 바로 Recive 같은 경우는 보낼 메시지를 미리 알고 있었기 때문에 이벤트로 걸어두고 연결이 완료되면 그 메세지를 보내는 형식이었다. 하지만 send는 다르게 우리가 보내는 메시지를 미리 알 수 있는가?? 미래를 예측하는것도 아니고 그건 절대 불가능하기 때문에 다음과 같은 방식이 필요하다.
public void Send(byte[] sendBuff)
{
SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
sendArgs.SetBuffer(sendBuff,0,sendBuff.Length);
RegisterSend(sendArgs);
}
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
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
else
{
Disconnect();
}
}
이렇게 할 수 있을것이다. 그런데 여기서 아쉬운점은 Recive같은 경우는 SocketAsyncEventArgs를 재 사용 가능했는데 Send는 그게 불가능 하다. 만약 우리가 rpg게임이 흥행해서 한 장소에 1000명의 유저가 있다고 해보면. 그 유저들마다 움직이는 정보를 서버에 보내야하는데 그럴 경우 Event가 무수히 만들어지면서 과부화가 생길 가능성이 있다. 실제로 mmo에서 가장 속도를 느리게 만드는 요인이 send에 있다고 봐도 무방하다.
그럼 어떤 방식이 있을지 생각해보자.
그냥 AsyncEvent 객체를 Session class안에 내부적으로 가지고 있으면 되지 않을까 싶다. 문제는 어느정도 해결이 되지만 문제는 멀티스레드 환경에서 같은 Session이 Send를 두번 이상 해서 겹치는 경우에 문제가 될 수 있다. 한쪽이 아직 완료되지 않았는데 다시 재사용하다보니 문제가 나는 것이다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
Queue<byte[]> _sendQueue = new();
bool _pending = false;
object _lock = new object(); //lock
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
recvArgs.UserToken = this;
recvArgs.SetBuffer(new byte[1024], 0, 1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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(e.ToString());
}
}
else
{
Disconnect();
}
}
}
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
OnRecvCompleted(null, args);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
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(e.ToString());
}
}
else
{
Disconnect();
}
}
#endregion
}
}
이렇게 수정해 볼 수 있을 것이다. 물론 이게 완벽하진 않다. 그 이유는 send를 100개를 보내면 결국 asyncsend도 100번 실행 될것이다. 단순히 queue를 통해 그 순서를 조금 늦췄을 뿐이다.
이걸 조금 더 개선해보자.
Session Send 개선
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
Queue<byte[]> _sendQueue = new();
object _lock = new object(); //lock
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
List<ArraySegment<byte>> _pendingList = new();
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_recvArgs.UserToken = this;
_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 _disconnect, 1) == 1)
return;
_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();
if(_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
else
{
Disconnect();
}
}
}
void RegisterRecv()
{
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)
{
try
{
// TODO
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"From Client {recvData}");
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
else
{
Disconnect();
}
}
#endregion
}
}
한번에 패킷들을 모아서 SendAsync를 하게되기 때문에 성능이 조금이나마 향샹되었다고 볼 수 있다.
패킷이 1000개가 왔다면 1개의 패킷이 먼저 lock을 잠구고 실행할 거고 그 패킷이 sendAsync를 실행하는 순간 그리고 pending을 처리하는 순간 lock을 놓을 것이고 그 사이 나머지 패킷들이 queue에 담길 것이다. 다음 send는 모인 패킷을 모와서 한번의 배열에 담아 보내는 형식이다.
Session EventHandler 추가
우리가 지금까지 한 Session을 통해 통신하는 건 TODO가 없다. 즉, 그냥 단순히 데이터를 받았을 때, 그것을 출력하는 것에 그쳤다.
우리가 Listener에서 이미 해본 방식이다. 그렇기에 Session에서도 비슷한 작업을 해주자.
크게 이벤트를 어디다 생성하냐에 따라 두가지 방식이 있다.
첫 번째는 우리가 기존에 만들어둔 Session 클래스 안에 이벤트 핸들러를 생성하는 것이고
두 번째는 Session 클래스를 상속받은 어떤 클래스 안에 생성하는 방법이다.
두개의 방식 모두 장단점이 존재한다.
조금 더만들기가 편한 후자의 방식으로 해보자.
결국 우리가 하고자 하는건 엔진과 서버를 분리하는 작업인것이다.
Session은 다음과 같이 수정한다.
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 ServerCore
{
abstract class Session
{
Socket _socket;
int _disconnect = 0;
Queue<byte[]> _sendQueue = new();
object _lock = new object(); //lock
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
List<ArraySegment<byte>> _pendingList = new();
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.UserToken = this;
_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 _disconnect, 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(e.ToString());
}
}
else
{
Disconnect();
}
}
}
void RegisterRecv()
{
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)
{
try
{
// TODO
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
else
{
Disconnect();
}
}
#endregion
}
}
보면 send와 recv등 서버단에서 하는건 굳이 관심 없고 추상클래스로 선언을 해 엔진단에서 필요한 액션을 사용하기 위해 함수들을 정의해줬다.
그거에 맞춰 Linstenr와 Main 서버는 다음과 같이 수정한다.
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
internal class Listener
{
Socket _listenSokcet;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSokcet = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSokcet.Bind(endPoint);
// 영업 시작
// backiog : 최대 대기수
_listenSokcet.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null;
bool pending = _listenSokcet.AcceptAsync(args);
if (pending == false)
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
// TODO
try
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
}
}
}
Func를 통해 우리가 사용하고자 하는 Session(특정 컨텐츠를 담은)을 생성하고 Connect를 여기서 해준다.(물론 Start 하는 순간 클라쪽에서 서버와 연결이 끊기면 에러가 난다. 이부분은 나중에 수정)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using static System.Collections.Specialized.BitVector32;
namespace ServerCore
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome");
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(Domain Name System)
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)
{
}
}
}
}
컨텐츠단에서는 우리가 원하는 행동을 Session을 상속받아 사용하면 된다.
길고 길었던 Listener와 Session에 대해 다뤄봤다 다음은 Connector와 관련된 내용을 다룰 것이다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[게임 서버] 패킷 직렬화 - Serialization (1) | 2024.01.24 |
---|---|
[게임 서버] 네트워크 프로그래밍 - Connector, TCP? UDP?, Buffer와PacketSession (1) | 2024.01.22 |
[게임 서버] 네트워크 프로그래밍 - 네트워크 기초 및 소켓 프로그래밍 (0) | 2024.01.17 |
[게임 서버] 멀티쓰레드 프로그래밍 - Context Switching, Event등 Lock의 다양한 기법 (1) | 2024.01.11 |
[게임 서버] 멀티쓰레드 프로그래밍 - Lock에 대해서 (1) | 2024.01.10 |