Q1. 필요한 인터페이스 동작 4가지는 무엇이 있을까?
Q2. Session을 상속받아 GameSession을 사용하는 이유는 무엇인가?
Q3. 엔진단과 컨텐츠단으로 코드를 옮겨가며 정리해 보세요. 엔진단과 컨텐츠 단의 구분 기준은 무엇인가?
Q4. 무슨 작업을 했는지 나열해 보세요.
Q5. Session#1~Session#4까지 뭘 한건지 설명해 보세요
답
1.
OnConnected(EndPoint endPoint);
OnRecv(ArraySegment<byte> buffer);
OnSend(int numOfBytes);
OnDisconnected(EndPoint endPoint);
2.
Session 클래스는 세션(Session)을 생성하고 관리하는 기본적인 기능을 제공합니다. 하지만 게임에 특화된 기능을 추가하기 위해서는 Session 클래스를 상속받아 GameSession 클래스를 만들어야 합니다. GameSession 클래스는 Session 클래스의 기능을 상속받으면서, 게임에 특화된 기능을 추가하여 세션(Session)을 보다 유연하게 다룰 수 있습니다. 이러한 이유로 Session을 상속받아 GameSession을 사용합니다
3.
엔진단과 컨텐츠단은 게임 엔진의 핵심 로직과 게임에 특화된 로직을 담당하는 부분으로, 코드를 구분하여 관리합니다. 이 두 부분을 구분하는 기준은, 엔진단은 게임 엔진의 핵심적인 기능을 담당하고, 컨텐츠단은 게임에 특화된 기능을 담당합니다. 예를 들어, 엔진단에서는 게임의 물리엔진, 그래픽스 엔진 등과 같이 모든 게임에서 필요한 공통적인 기능을 구현하고, 컨텐츠단에서는 각각의 게임마다 필요한 특화된 기능을 구현합니다. 이러한 내용이 해당 문서에서 다루어지고 있습니다.
4.
class Session에 abstract를 붙이고 필요한 인터페이스를 적는다. abstract class Session { 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);
-> Program.cs에 Session을 상속받은 GameSession을 선언하고 인터페이스들을 override해서 재정의 할 준비를 한다
-> Progarm.cs의 OnAcceptHandler에서 session을 만들 때 Session은 abstract라 바로 사용할 수 없기 때문에 GameSession 타입으로 만든다.
-> Session의 Disconnect에서 OnDisconnected(_socket.RemoteEndPoint); 를 호출한다
-> Session의 OnRecvCompleted에서 OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); 를 호출한다. OnRecv를 넣어준 곳에 있던 받은 메시지를 출력하는 코드는 OnRecv의 재정의부로 옮겨준다. 인터페이스에 맞게 수정해준다.
-> Session의 OnSendCompleted에서 OnSend(_sendArgs.BytesTransferred); 를 호출한다. 원래 이 자리에 있던 보낸 데이터의 바이트수를 출력하는 코드는 OnSend의 재정의부로 옮겨준다.
-> OnConnected,OnDisconnected의 재정의부에 endPoint를 출력하는 로그를 찍어 준다.
-> Program의 OnAcceptHandler의 session을 만드는 코드와 session의 Start를 clientSocket을 받아 실행하는 코드를 Listener의 OnAcceptCompleted로 옮긴다. 옮기고 나서 Start의 매개변수 clientSocket의 자리에는 원래 envoke 할 때 전달되었던 args.AcceptSocket을 넣어준다. _OnAcceptHandler.Invoke(args.AcceptSocket); 하는게 아니라 session.OnConnected(args.AcceptSocket.RemoteEndPoint)를 해준다. Invoke 부분은 삭제해 준다.
-> Contents단의 GameSession을 엔진단의 Listener의 OnAcceptComplted에서 new로 강제로 만들어 주고 있는 모순을 해결하기 위해 일단 listener의 Action<Socket> _OnAcceptHandler;을 Func<Session> _sessionFactory;로 수정해 준다. Listener Init의 함수 인자도 Action<Socket> onAcceptHandler 에서 Func<Session> sessionFactory로 바꾸고, +=로 _sessionFactory와 연결한다.
-> Program의 Main에서 _listener.Init(endPoint, OnAcceptHandler);이 부분을 _listener.Init(endPoint, () => { return new GameSession(); });로 즉, 어떤 Session을 만들지 정의하는 코드로 바꾼다.
-> Program의 OnAcceptHandler안에 있는 메시지를 Send하고, Sleep, Disconnect하는 코드를 GameSession의 OnConnected의 재정의부로 옮겨준다.Session. 부분을 지워줘도 된다.
-> Program의 OnAcceptHandler를 삭제한다.
-> Listener의 OnAcceptCompleted에서 GameSession session = new GameSession();를 Session session = _sessionFactory.Invoke();로 수정한다.
4..
#1에서는 Recieve 기능을 Session이라는 클래스를 만들어 분리했다. #2에서는 Send기능을, #3에서는 BufferList를 이용해서 Queue에 데이터를 넣어 한번에 보내는 기능을, #4에서는 컨텐츠단과 엔진단을 분리하는 작업을 하였다.
지난 시간의 코드
Listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Listener
{
Socket _listenSocket;
Action<Socket> _OnAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
_OnAcceptHandler += onAcceptHandler;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegistterAccept(args);
}
void RegistterAccept(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) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
_OnAcceptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
Session
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
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();
}
}
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를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[FromClient] {recvData}");
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
Program.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
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 to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();**
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
DummyClient
namespace DummyClient
{
internal 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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while(true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 보낸다
for(int i=0; i<5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
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());
}
Thread.Sleep(100);// 너무 많이 보내도 문제 있으니
}
}
}
}
지난 시간에 Send를 효율적으로 고치는 방법에 대해 알아 봤었어. Queue에 쌓아서 한방에 보내는 것 까지 알아 봤었어.
오늘은 이벤트 핸들러를 추가 해 줄 것이다. 패킷을 보내는 경우에는 Send인터페이스를 이용해 보낼 수 있어. 반대로 어떤 패킷을 받는다고 가정 하고 어떻게 처리 해야 하는지 보면
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($"[FromClient] {recvData}");**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
그냥 로그만 찍고 아무것도 안하고 있었어.
여기다가 콜백이든 어떤 방식으로든 메시지를 받았다는 걸 연동을 해줘야 한다. 사실 비슷한 작업을 리스너에서도 한 적이 있다. Listener에는 어떻게 되어 있는지 유심히 보면
internal class Listener
{
Socket _listenSocket;
**Action<Socket> _OnAcceptHandler;**
Listener에서 이렇게 소켓을 받는 액션을 하나 만들었었어.
public void Init(IPEndPoint endPoint, Action<Socket> **onAcceptHandler**)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
_OnAcceptHandler += **onAcceptHandler**;
Listener의 Init에서 onAcceptHandler를 받아가지고
실제로 클라이언트가 접속을 했을 때 (AcceptAsync가 완료 되었을 때) callback 방식으로 아래와 같이 알려줬고,
Listener의 OnAcceptCompleted에서
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
아래와 같이 Main에서 _listener.Init 함수로 등록을 해가지고 어떤 방식으로 처리를 할지를 OnAcceptHandler여기다가 적어서 처리를 하고 있었다.
ServerCore의 Program.cs
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(10000);
session.Disconnect();
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
**_listener.Init(endPoint, OnAcceptHandler);**
Console.WriteLine("Listening...");
while (true)
{
;
}
}
Session도 비슷한 작업을 해줘야 한다는 얘기가 된다.
필요한게 무엇인지 곰곰히 생각을 해보자.
Session은 ServerCore에 들어가는 부분인데 ServerCore는 Core라이브러리다. Server엔진 같은 존재라 나중에 컨텐츠는 Server project쪽에다 만들게 될텐데 Session을 바로 사용하는게 아니라 Session이 뚫어준 인터페이스로 작업을 하게 될거야.
이벤트를 받아주는 방식을 어떻게 하느냐 두가지 방식으로 분류할 수 있다.
- 이벤트 핸들러를 만들어서 연결하는게 첫번쨰
- 아애 Session을 상속 받아 가지고, 예를 들면 Program에 Session을 상속받은 GameSession 클래스를 만들어서 만드는 방법이 있는데 장단점이 있다.
장단점이 있다. 일단 필요한 부분은 무엇인지 생각을 해보면 4가지를 생각할 수 있어.
일단 인터페이스만 맞춰 주도록 할거야.
public void OnConnected(EndPoint endPoint) { } // 클라이언트가 접속했다. 인자로 EndPoint를 받으면 좋을 거 같아.
public void OnRecv(ArraySegment<byte> buffer) { } // 누군가가 클라이언트 쪽에서 패킷을 보내서 받았다는 메시지가 올텐데 byte배열, array segment로 받아도 된다. 나중엔 패킷으로 받는 것도 고려할 수 있다.
public void OnSend(int numOfBytes) { } // 내가 보냈는데 성공했다. 중요한 부분은 아님. 인터페이스는 일단 뚫어준다.
public void OnDisconnected(EndPoint endPoint) { } // 디스커넥트 한 상태에서도 EndPoint받아 줄 수 있을 거야.
크게 4가지 동작을 뽑아서 사용하게 될거야.
결국 외부에서 session을 사용할 때에는 Send를 포함한 이런 몇 개의 이벤트만 사용할거지 실질적으로 내부에서 구현되는 부분은 관심도 없을 것이다. #region 네트워크 통신 부분은 숨겨있는 상태로 사용하게 될 거란 얘기.
이거를 어떻게 처리 하느냐.
첫번쨰 방법은 아까 말한대로 class SessionHandler를 만들어서 여기다가 필요한 4개를 넣어주고 class Session에서 인자로 SessionHandler를 받는게 첫번째 방법이 될거고,
Session.cs
namespace ServerCore
{
**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 _disconnect = 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**
}
두번쨰는 상속을 받는 거.
선생님은 상속을 받는게 만들기가 좀 더 편해서 선호한다.
GameSession에서도 방금 말한 4가지를 재정의 해야 하니까 abstract를 붙여 주도록 할거야.
class Session
{
Socket _socket;
int _disconnect = 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);**
그리고 Session클래스에도 abstract를 붙여줘야 한다.
abstract class Session
{
그러면 이제 Session은 이제 바로 사용할 수 없고 Session을 상속받아서 사용해야 한다.
namespace ServerCore
{
class GameSession : Session
{
}
잠재적 수정사항에서 추상클래스를 구현하면 디폴트로 구현 된 부분을 지우고 시작하면 된다.
namespace ServerCore
{
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)
{
}
}
그 다음에
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
기존의 코드에서 처럼 이렇게 Session을 바로 만들 수 없고 상속 받은 GameSession으로 만들게 수정해야 한다.
static void OnAcceptHandler(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
abstract 클래스는 바로 사용할 수 없으니까 Session을 바로 사용할 수 없다.
그 다음에 Session.cs로 가서 4가지 함수를 하나씩 연동을 해줘야 한다.
가장 간단한 거 부터 해주자면
public abstract void OnDisconnected(EndPoint endPoint);
Session.cs에 Disconenect라는 게 있었어. 여기에다가 그냥 호출을 해주면 된다.
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
**OnDisconnected(_socket.RemoteEndPoint);**
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
Disconnect가 성공하면 OnDisconnected가 한번 뜨게 될거고, 하면 되고 하고 싶은게 있으면 OnDisconnected 재정의부에 넣어주면 된다.
그 다음 중요한게 Send랑 Recv가 있는데 가장 중요한게 OnRecv니까 얘부터 해보도록 하자.
Session.cs의 OnRecvCompleted에서 이벤트를 연동시켜 주면 된다.
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($"[FromClient] {recvData}");
**OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
Receive가 Completed 되었다면 기존의 하드코딩된 메시지만 출력하고 끝낼 것이 아니라, 여기서 이벤트를 연동시켜 줄거야.
원래 이 자리에 있던 코드는 ServerCore의 Program.cs의
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
}
여기로 이사시켜 줬다. 인터페이스도 ArraySegment가 되면서 인터페이스가 약간 바뀐 부분을 수정해줬다.
OnRecv이벤트가 발생했을 때 무엇을 할지는 여기 컨텐츠 코드(ServerCore의 Program.cs의 GameSession 클래스는 나중에 Server project로 옮겨줄거라 컨텐츠단, ServerCore의 Session은 그대로 ServerCore 엔진단임)에서 넣어주면 된다.
결국 우리가 하는 건 엔진과 컨텐츠를 분리를 하는 거다. 컨텐츠 단(나중에 Server project로 옮길 부분)에서는 GameSession을 만들어서 사용할 것이고, 엔진단(Session의 OnRecvCompleted 처럼 ServerCore에 남길 부분)에서는 뭔지 모르겠지만 OnRecv라는 이벤트만 호출해준다고 보면 된다.
Send같은 경우에는 Session.cs의 OnSendCompleted에서
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock(_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
//Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
**OnSend(_sendArgs.BytesTransferred);**
if(_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
원래 이 자리에 있던 코드는
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
Program.cs의 GameSession 여기로 이사시켜줬다. 컨텐츠 코드에서 처리하게 분리시켜 준 것이다.
OnConnected, OnDiscoinnected에도 로그를 찍어 주자.
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
다시 Session에 가보면 OnConnected가 아직 안되어 있어. 좀 까다로원. Session 내부에서 당장 애를 호출하는 부분이 없어. 얘를 해야 하는 시점이 언제인지 생각을 해보면 클라이언트가 접속을 해서 클라이언트가 접속을 했습니다 라고 알리는 시점인데
엄밀히 말하면 Listener에서 해주고 있었어.
Listener.cs의 OnAcceptCompleted가 뜬 다음에 여기서 뭔가를 해줘야 하는데
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
기존의 방법을 살펴 보면 애당초 세션을 만들어주고 메시지를 날리는 걸 Program.cs의 Handler로 갖고 있었어. Program.cs에 OnAcceptHandler 라는 애 만들어서 연결을 해주고 있었어.
{Program.cs의 Main에서
_listener.Init(endPoint, **OnAcceptHandler**);
이렇게 Listener의 Init에 OnAcceptHandler함수를 전달해 줬고,
Listener의 Init에서는 Linstener의 Action<Socket> _OnAcceptHandler;에
_OnAcceptHandler += onAcceptHandler;
이렇게 연결해 주었다.
그리고 Listener의 OnAcceptComplted에서
_OnAcceptHandler.Invoke(args.AcceptSocket);
이렇게 Invoke를 해주는 식으로 연결하고 있었다. }
지금도 크게 잘못 된 건 아니지만 Session이라는 개념이 만들어져 있으니까 다시 살펴 보면,
Program.cs의 OnAcceptHandler라는 애가 컨텐츠 단으로 빠져있는 거야. 엔진에서는 얘를 관리하는게 아니라 처음에 Program.cs의 Main에서 _listener를 초기화 할 떄
_listener.Init(endPoint, **OnAcceptHandler**);
어떤 방식으로 처리를 할 지를 OnAcceptHandler로 만들어 주고 있었는데
유심히 보면 Session.Start를 하는 부분은 컨텐츠 단 보다는 엔진단에 들어가는게 조금 더 맞을 거 같다는 생각이 들어.
static void **OnAcceptHandler**(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
**session.Start(clientSocket);**
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(10000);
session.Disconnect();
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, **OnAcceptHandler**);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
그렇다는 건 기존처럼 Listener에서 이렇게 _OnAcceptHandler를 Invoke하는게 아니라.
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
Program.cs의 OnAcceptHandler에서 세션을 만들고 Start하는 이 부분을 엔진단으로 옮겨 보도록 할거야.
static void OnAcceptHandler(Socket clientSocket)
{
try
{
**GameSession session = new GameSession();**
**session.Start(clientSocket);**
Lisener.cs의
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
**GameSession session = new GameSession();
session.Start(args.AcceptSocket);**
// Invoke 할 때 실행될 부분을 바로 위로 옮겨 준 것
_OnAcceptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
OnAcceptCompleted로 옮겼다.
그리고 컨텐츠단의 함수를 받은 _OnAcceptHandler를 Invoke하는게 아니라, Listener.cs의
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
**GameSession** session = new GameSession();
**** session.**Start**(args.AcceptSocket);
**session.OnConnected(args.AcceptSocket.RemoteEndPoint)**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
session.OnConnected라는 얘를 이용할거야. endPoint를 넣어준다.
_OnAcceptHandle는 여기서 호출해 줄 필요가 없어졌기에 삭제해 준다.
이렇게 하면 코드 관리가 쉬워진 거 같긴 하다.
Start부분이 외부에 노출되기 보다는 안에서 하는게 더 맞기 때문.
이렇게 하면 문제는 GameSession을 new로 강제로 엔진단 안에서 만들고 있었어. 근데 GameSession은 컨텐츠 단에 빠져있는 애야. 어디서 만들어져 있나 보면 Program.cs의
namespace ServerCore
{
class GameSession : Session
{
Main쪽에 있는데 나중에는 Server project쪽(컨텐츠단)으로 빠지게 된다.
나중에는 MMOSession이라거나 다른 이름으로도 클래스를 만들어 줄 수 있을 거야. 나중에는 다른 애가 될 수도 있는건데 이런 식으로 만들어 주는 건 앞뒤가 안맞아 문제가 있다. 세션을 만들어 주는 부분은 밖에서 위임을 하거나 어떤 식으로 만들어 줄지는 밖에서 정해주는게 좋다. 안에서 만들거면 적어도 어떤 클래스를 만들지 어떤 식으로든 넘겨줘야 한다.
결국 어떻게 수정하면 되냐면,
Listener의
Action<Socket> _OnAcceptHandler;
을
Func<Session> _sessionFactory;
이렇게 이름을 바꿔주자.
session을 어떤 방식으로 만들어줄지를 정의하는 애가 된다.
Action과 다르게 Func는
Result타입이 있는 애다. 함수를 만들 때, 인자는 아무것도 안받고, Session이라는 형식을 뱉는 함수 형식을 받겠다는 거고,
public void Init(IPEndPoint endPoint, Func<Session> **sessionFactory**)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
**_sessionFactory += sessionFactory;**
이렇게 Listener의 Init에서 sessionFactory 를 연결해주도록 하자.
그럼 뭐가 달라진 것이냐면
Program의 Main에서
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, **OnAcceptHandler**);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
기존의 OnAcceptHandler를 받는 게 아니라
Session을 어떻게 누구를 만들 만들어 줄지를 정의를 해야 된다는 얘기가 된다.
지금은 간단하게 gameSession을 사용 할 것이다. 혹시라도 나중에 Session을 Manager에서 관리하거나 할 때에는 Manager를 통해 만들도록 수정을 하면 된다. 근데 지금은 간단하게 람다로 정의를 한다.
_listener.Init(endPoint, **() => { return new GameSession();** });
이렇게 정의 한거.
아까랑 비슷하면서 다른게 컨텐츠 단에서 뭔가를 하긴 하는데 OnAcceptHandle 같이 전체적 처리 특히 session.Start(clientSocket)같이 중요한 처리를 외부에서 처리를 하는게 아니라 무엇을 어떤 세션을 만들지만 결정을 해주고, 다머지 부분은
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
**GameSession session = new GameSession();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint)**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
이런 식으로 Listener 안에서 다 해주고 있다.
결국 밖에서는 내부에서는 어떻게 구현되는지 알지도 못하고 관심도 없지만 이런식으로 실제로 뭔가가 만들어졌으면 OnConnected가 호출이 되면서 여기다가 재정의부에 뭔가 시작할 코드를 넣어주면 된다.
이전 시간에는 클라이언트가 접속을 했을 때
static void OnAcceptHandler(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
session.Start(clientSocket);
**byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(10000);
session.Disconnect();**
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
이런걸 해주고 있었어. 이부분을 컨텐츠 코드에서 하고 있었으니까 ctrl+c를 해서 OnConnected에 넣어주도록 하자.
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(10000);
Disconnect();**
}
상속 방식으로 구현해서 자체가 Session이기에 session.은 지워도 된다.
만약 상속 방식이 아니라 GameSession이 Handler로 빠졌으면 아까와 마찬가지로 session.Send를 해야 한다.
try, catch를 넣어줘도 되지만 일단은 이렇게만 해준다.
Program의 OnAccessHandler는 이제 볼일이 없으니 삭제해 준다.
우리가 Listener에서
Func<Session> _sessionFactory;
를 받은 이유는
OnAcceptCompleted에서
GameSession session = new GameSession();
이렇게 new를 하지 않고
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());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
이렇게 해주기 위함이었다. SessionFactory는 어떤 타입인지 모르니까 일단 가 상위 부모 클래스인 Session으로 뱉어주니까 받아주는 GameSession session을 Session 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(10000);
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($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
이런 식으로 Session을 상속 받아가지고 override한 버전을 하나씩 만들어서 처리를 하면 될 거 같고,
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
**_listener.Init(endPoint, () => { return new GameSession(); });**
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
_listener같은 경우에도 이런 식으로 무엇을 만들어 줄지만 지정을 해주면 된다.
이렇게 어느정도 모양새가 나오기 시작했다.
수정할 때 마다 테스트를 해봐서 작동하는지 확인하면 좋다.
나중에 코드가 많아지면 알게 되겠지만 몇가지 문제 사항이 있다.
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());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
AcceptSocket에다 RemoteEndPoint를 추출해서 사용하고 있는데 Start에서 OnConnected로 넘어가는 순간 클라에서 연결을 끊어 버리면 애당초 연결이 끊어지니까 args.AcceptSocket.RemoteEndPoint여기에 접근하는게 허락이 되지 않는다.
이런거는 나중에 코드를 테스트 하면서 다 거르게 될거야. 서버를 초반에 만들 때는 간단한게 만들다가 테스트를 해보면서 문제가 생기면 하나씩 방어 코드를 넣어주는게 좋다.
초반에 책을 보고 외우는 방식으로 하면 남지가 않으니까 최대한 실험 정신을 발휘해서 작업을 해 나가면 좋다.
오늘 Session을 구분해서 관리하는 방법에 대해서 알아 봤고,
이어서 Connect에 관해서 알아볼건데
더미 클라이언트를 유심히 보면
socket.Connect(endPoint);
Connect를 이렇게 하고 있었어.
여기의 보내고 받는 부분도 우리가 만든 세션 방식으로 관리를 하면 RegisterSend, RegisterRecv같은 코드를 재사용할 수 있겠다는 생각이 든다.
그런 작업을 해볼거고, 오늘은 분리하는 간단한 작업을 해봤다.
질문. I worried about that GameSession is bigger than Session. Therefore when running Session session = _sessionFactory.Invoke(); this code, data will lose. right? because Session is smaller than GameSession.
답. By chatGPT
You don't need to worry about data loss in this case. When you assign a GameSession object to a Session variable, you're not actually copying the object or changing its size. Instead, you're just storing a reference to the GameSession object. The GameSession object still exists in memory as a GameSession, with all its data and methods intact.When you assign a GameSession object to a Session variable, you are using polymorphism, which allows a derived class (in this case, GameSession) to be treated as an instance of its base class (Session). This means that you can call any of the methods defined in the Session class on your GameSession object, even if it's stored in a Session variable.However, if you want to call a method specific to the GameSession class or access a property that is not present in the Session class, you would need to cast the Session variable back to a GameSession.
For example
Session session = _sessionFactory.Invoke();
GameSession gameSession = session as GameSession;
if (gameSession != null)
{
// Call a method or access a property specific to the GameSession class
}
In summary, you won't lose any data when assigning a GameSession object to a Session variable. The object is still a GameSession in memory, and you can cast it back to a GameSession when you need to access its specific features.
지금까지 코드
Listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Listener
{
Socket _listenSocket;
//Action<Socket> _OnAcceptHandler;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
//_OnAcceptHandler += onAcceptHandler;
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
for (int i = 0; i < 10; i++)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(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) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
// GameSession session = new GameSession();
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
Session
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
abstract class Session
{
Socket _socket;
int _disconnect = 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)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
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를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
//string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
//Console.WriteLine($"[FromClient] {recvData}");
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
/Program
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
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(5000);
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($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
//static void OnAcceptHandler(Socket clientSocket)
//{
// try
// {
// //GameSession session = new GameSession();
// //session.Start(clientSocket);
// byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
// session.Send(sendBuff);
// Thread.Sleep(1000);
// session.Disconnect();
// session.Disconnect();
// }
// catch (Exception e)
// {
// Console.WriteLine(e);
// }
//}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, ()=> { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
DummyClient
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
internal 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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
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());
}
Thread.Sleep(100);
}
}
}
}
end
02_09_네트워크 프로그래밍_Session #4_엔진단과 컨텐츠단 분리
Q1. 필요한 인터페이스 동작 4가지는 무엇이 있을까?
Q2. Session을 상속받아 GameSession을 사용하는 이유는 무엇인가?
Q3. 엔진단과 컨텐츠단으로 코드를 옮겨가며 정리해 보세요. 엔진단과 컨텐츠 단의 구분 기준은 무엇인가?
Q4. 무슨 작업을 했는지 나열해 보세요.
Q5. Session#1~Session#4까지 뭘 한건지 설명해 보세요
답
OnConnected(EndPoint endPoint); OnRecv(ArraySegment<byte> buffer); OnSend(int numOfBytes); OnDisconnected(EndPoint endPoint);
- Session 클래스는 세션(Session)을 생성하고 관리하는 기본적인 기능을 제공합니다. 하지만 게임에 특화된 기능을 추가하기 위해서는 Session 클래스를 상속받아 GameSession 클래스를 만들어야 합니다. GameSession 클래스는 Session 클래스의 기능을 상속받으면서, 게임에 특화된 기능을 추가하여 세션(Session)을 보다 유연하게 다룰 수 있습니다. 이러한 이유로 Session을 상속받아 GameSession을 사용합니다.
- 엔진단과 컨텐츠단은 게임 엔진의 핵심 로직과 게임에 특화된 로직을 담당하는 부분으로, 코드를 구분하여 관리합니다. 이 두 부분을 구분하는 기준은, 엔진단은 게임 엔진의 핵심적인 기능을 담당하고, 컨텐츠단은 게임에 특화된 기능을 담당합니다. 예를 들어, 엔진단에서는 게임의 물리엔진, 그래픽스 엔진 등과 같이 모든 게임에서 필요한 공통적인 기능을 구현하고, 컨텐츠단에서는 각각의 게임마다 필요한 특화된 기능을 구현합니다. 이러한 내용이 해당 문서에서 다루어지고 있습니다.
class Session에 abstract를 붙이고 필요한 인터페이스를 적는다. abstract class Session { 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); -> Program.cs에 Session을 상속받은 GameSession을 선언하고 인터페이스들을 override해서 재정의 할 준비를 한다 -> Progarm.cs의 OnAcceptHandler에서 session을 만들 때 Session은 abstract라 바로 사용할 수 없기 때문에 GameSession 타입으로 만든다. -> Session의 Disconnect에서 OnDisconnected(_socket.RemoteEndPoint); 를 호출한다 -> Session의 OnRecvCompleted에서 OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); 를 호출한다. OnRecv를 넣어준 곳에 있던 받은 메시지를 출력하는 코드는 OnRecv의 재정의부로 옮겨준다. 인터페이스에 맞게 수정해준다. -> Session의 OnSendCompleted에서 OnSend(_sendArgs.BytesTransferred); 를 호출한다. 원래 이 자리에 있던 보낸 데이터의 바이트수를 출력하는 코드는 OnSend의 재정의부로 옮겨준다. -> OnConnected,OnDisconnected의 재정의부에 endPoint를 출력하는 로그를 찍어 준다. -> Program의 OnAcceptHandler의 session을 만드는 코드와 session의 Start를 clientSocket을 받아 실행하는 코드를 Listener의 OnAcceptCompleted로 옮긴다. 옮기고 나서 Start의 매개변수 clientSocket의 자리에는 원래 envoke 할 때 전달되었던 args.AcceptSocket을 넣어준다. _OnAcceptHandler.Invoke(args.AcceptSocket); 하는게 아니라 session.OnConnected(args.AcceptSocket.RemoteEndPoint)를 해준다. Invoke 부분은 삭제해 준다. -> Contents단의 GameSession을 엔진단의 Listener의 OnAcceptComplted에서 new로 강제로 만들어 주고 있는 모순을 해결하기 위해 일단 listener의 Action<Socket> _OnAcceptHandler;을 Func<Session> _sessionFactory;로 수정해 준다. Listener Init의 함수 인자도 Action<Socket> onAcceptHandler 에서 Func<Session> sessionFactory로 바꾸고, +=로 _sessionFactory와 연결한다. -> Program의 Main에서 _listener.Init(endPoint, OnAcceptHandler);이 부분을 _listener.Init(endPoint, () => { return new GameSession(); });로 즉, 어떤 Session을 만들지 정의하는 코드로 바꾼다. -> Program의 OnAcceptHandler안에 있는 메시지를 Send하고, Sleep, Disconnect하는 코드를 GameSession의 OnConnected의 재정의부로 옮겨준다.Session. 부분을 지워줘도 된다. -> Program의 OnAcceptHandler를 삭제한다. -> Listener의 OnAcceptCompleted에서 GameSession session = new GameSession();를 Session session = _sessionFactory.Invoke();로 수정한다.
- #1에서는 Recieve 기능을 Session이라는 클래스를 만들어 분리했다. #2에서는 Send기능을, #3에서는 BufferList를 이용해서 Queue에 데이터를 넣어 한번에 보내는 기능을, #4에서는 컨텐츠단과 엔진단을 분리하는 작업을 하였다.
지난 시간의 코드
Listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Listener
{
Socket _listenSocket;
Action<Socket> _OnAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
_OnAcceptHandler += onAcceptHandler;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegistterAccept(args);
}
void RegistterAccept(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) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
_OnAcceptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
Session
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
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();
}
}
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를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[FromClient] {recvData}");
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
Program.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
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 to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();**
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
DummyClient
namespace DummyClient
{
internal 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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while(true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 보낸다
for(int i=0; i<5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
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());
}
Thread.Sleep(100);// 너무 많이 보내도 문제 있으니
}
}
}
}
지난 시간에 Send를 효율적으로 고치는 방법에 대해 알아 봤었어. Queue에 쌓아서 한방에 보내는 것 까지 알아 봤었어.
오늘은 이벤트 핸들러를 추가 해 줄 것이다. 패킷을 보내는 경우에는 Send인터페이스를 이용해 보낼 수 있어. 반대로 어떤 패킷을 받는다고 가정 하고 어떻게 처리 해야 하는지 보면
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($"[FromClient] {recvData}");**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
그냥 로그만 찍고 아무것도 안하고 있었어.
여기다가 콜백이든 어떤 방식으로든 메시지를 받았다는 걸 연동을 해줘야 한다. 사실 비슷한 작업을 리스너에서도 한 적이 있다. Listener에는 어떻게 되어 있는지 유심히 보면
internal class Listener
{
Socket _listenSocket;
**Action<Socket> _OnAcceptHandler;**
Listener에서 이렇게 소켓을 받는 액션을 하나 만들었었어.
public void Init(IPEndPoint endPoint, Action<Socket> **onAcceptHandler**)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
_OnAcceptHandler += **onAcceptHandler**;
Listener의 Init에서 onAcceptHandler를 받아가지고
실제로 클라이언트가 접속을 했을 때 (AcceptAsync가 완료 되었을 때) callback 방식으로 아래와 같이 알려줬고,
Listener의 OnAcceptCompleted에서
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
아래와 같이 Main에서 _listener.Init 함수로 등록을 해가지고 어떤 방식으로 처리를 할지를 OnAcceptHandler여기다가 적어서 처리를 하고 있었다.
ServerCore의 Program.cs
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(10000);
session.Disconnect();
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
**_listener.Init(endPoint, OnAcceptHandler);**
Console.WriteLine("Listening...");
while (true)
{
;
}
}
Session도 비슷한 작업을 해줘야 한다는 얘기가 된다.
필요한게 무엇인지 곰곰히 생각을 해보자.
Session은 ServerCore에 들어가는 부분인데 ServerCore는 Core라이브러리다. Server엔진 같은 존재라 나중에 컨텐츠는 Server project쪽에다 만들게 될텐데 Session을 바로 사용하는게 아니라 Session이 뚫어준 인터페이스로 작업을 하게 될거야.
이벤트를 받아주는 방식을 어떻게 하느냐 두가지 방식으로 분류할 수 있다.
- 이벤트 핸들러를 만들어서 연결하는게 첫번쨰
- 아애 Session을 상속 받아 가지고, 예를 들면 Program에 Session을 상속받은 GameSession 클래스를 만들어서 만드는 방법이 있는데 장단점이 있다.
장단점이 있다. 일단 필요한 부분은 무엇인지 생각을 해보면 4가지를 생각할 수 있어.
일단 인터페이스만 맞춰 주도록 할거야.
public void OnConnected(EndPoint endPoint) { } // 클라이언트가 접속했다. 인자로 EndPoint를 받으면 좋을 거 같아.
public void OnRecv(ArraySegment<byte> buffer) { } // 누군가가 클라이언트 쪽에서 패킷을 보내서 받았다는 메시지가 올텐데 byte배열, array segment로 받아도 된다. 나중엔 패킷으로 받는 것도 고려할 수 있다.
public void OnSend(int numOfBytes) { } // 내가 보냈는데 성공했다. 중요한 부분은 아님. 인터페이스는 일단 뚫어준다.
public void OnDisconnected(EndPoint endPoint) { } // 디스커넥트 한 상태에서도 EndPoint받아 줄 수 있을 거야.
크게 4가지 동작을 뽑아서 사용하게 될거야.
결국 외부에서 session을 사용할 때에는 Send를 포함한 이런 몇 개의 이벤트만 사용할거지 실질적으로 내부에서 구현되는 부분은 관심도 없을 것이다. #region 네트워크 통신 부분은 숨겨있는 상태로 사용하게 될 거란 얘기.
이거를 어떻게 처리 하느냐.
첫번쨰 방법은 아까 말한대로 class SessionHandler를 만들어서 여기다가 필요한 4개를 넣어주고 class Session에서 인자로 SessionHandler를 받는게 첫번째 방법이 될거고,
Session.cs
namespace ServerCore
{
**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 _disconnect = 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**
}
두번쨰는 상속을 받는 거.
선생님은 상속을 받는게 만들기가 좀 더 편해서 선호한다.
GameSession에서도 방금 말한 4가지를 재정의 해야 하니까 abstract를 붙여 주도록 할거야.
class Session
{
Socket _socket;
int _disconnect = 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);**
그리고 Session클래스에도 abstract를 붙여줘야 한다.
abstract class Session
{
그러면 이제 Session은 이제 바로 사용할 수 없고 Session을 상속받아서 사용해야 한다.
namespace ServerCore
{
class GameSession : Session
{
}
잠재적 수정사항에서 추상클래스를 구현하면 디폴트로 구현 된 부분을 지우고 시작하면 된다.
namespace ServerCore
{
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)
{
}
}
그 다음에
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
기존의 코드에서 처럼 이렇게 Session을 바로 만들 수 없고 상속 받은 GameSession으로 만들게 수정해야 한다.
static void OnAcceptHandler(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
abstract 클래스는 바로 사용할 수 없으니까 Session을 바로 사용할 수 없다.
그 다음에 Session.cs로 가서 4가지 함수를 하나씩 연동을 해줘야 한다.
가장 간단한 거 부터 해주자면
public abstract void OnDisconnected(EndPoint endPoint);
Session.cs에 Disconenect라는 게 있었어. 여기에다가 그냥 호출을 해주면 된다.
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
**OnDisconnected(_socket.RemoteEndPoint);**
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
Disconnect가 성공하면 OnDisconnected가 한번 뜨게 될거고, 하면 되고 하고 싶은게 있으면 OnDisconnected 재정의부에 넣어주면 된다.
그 다음 중요한게 Send랑 Recv가 있는데 가장 중요한게 OnRecv니까 얘부터 해보도록 하자.
Session.cs의 OnRecvCompleted에서 이벤트를 연동시켜 주면 된다.
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($"[FromClient] {recvData}");
**OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));**
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
Receive가 Completed 되었다면 기존의 하드코딩된 메시지만 출력하고 끝낼 것이 아니라, 여기서 이벤트를 연동시켜 줄거야.
원래 이 자리에 있던 코드는 ServerCore의 Program.cs의
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
}
여기로 이사시켜 줬다. 인터페이스도 ArraySegment가 되면서 인터페이스가 약간 바뀐 부분을 수정해줬다.
OnRecv이벤트가 발생했을 때 무엇을 할지는 여기 컨텐츠 코드(ServerCore의 Program.cs의 GameSession 클래스는 나중에 Server project로 옮겨줄거라 컨텐츠단, ServerCore의 Session은 그대로 ServerCore 엔진단임)에서 넣어주면 된다.
결국 우리가 하는 건 엔진과 컨텐츠를 분리를 하는 거다. 컨텐츠 단(나중에 Server project로 옮길 부분)에서는 GameSession을 만들어서 사용할 것이고, 엔진단(Session의 OnRecvCompleted 처럼 ServerCore에 남길 부분)에서는 뭔지 모르겠지만 OnRecv라는 이벤트만 호출해준다고 보면 된다.
Send같은 경우에는 Session.cs의 OnSendCompleted에서
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock(_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
//Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
**OnSend(_sendArgs.BytesTransferred);**
if(_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
원래 이 자리에 있던 코드는
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
Program.cs의 GameSession 여기로 이사시켜줬다. 컨텐츠 코드에서 처리하게 분리시켜 준 것이다.
OnConnected, OnDiscoinnected에도 로그를 찍어 주자.
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
다시 Session에 가보면 OnConnected가 아직 안되어 있어. 좀 까다로원. Session 내부에서 당장 애를 호출하는 부분이 없어. 얘를 해야 하는 시점이 언제인지 생각을 해보면 클라이언트가 접속을 해서 클라이언트가 접속을 했습니다 라고 알리는 시점인데
엄밀히 말하면 Listener에서 해주고 있었어.
Listener.cs의 OnAcceptCompleted가 뜬 다음에 여기서 뭔가를 해줘야 하는데
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
기존의 방법을 살펴 보면 애당초 세션을 만들어주고 메시지를 날리는 걸 Program.cs의 Handler로 갖고 있었어. Program.cs에 OnAcceptHandler 라는 애 만들어서 연결을 해주고 있었어.
{Program.cs의 Main에서
_listener.Init(endPoint, **OnAcceptHandler**);
이렇게 Listener의 Init에 OnAcceptHandler함수를 전달해 줬고,
Listener의 Init에서는 Linstener의 Action<Socket> _OnAcceptHandler;에
_OnAcceptHandler += onAcceptHandler;
이렇게 연결해 주었다.
그리고 Listener의 OnAcceptComplted에서
_OnAcceptHandler.Invoke(args.AcceptSocket);
이렇게 Invoke를 해주는 식으로 연결하고 있었다. }
지금도 크게 잘못 된 건 아니지만 Session이라는 개념이 만들어져 있으니까 다시 살펴 보면,
Program.cs의 OnAcceptHandler라는 애가 컨텐츠 단으로 빠져있는 거야. 엔진에서는 얘를 관리하는게 아니라 처음에 Program.cs의 Main에서 _listener를 초기화 할 떄
_listener.Init(endPoint, **OnAcceptHandler**);
어떤 방식으로 처리를 할 지를 OnAcceptHandler로 만들어 주고 있었는데
유심히 보면 Session.Start를 하는 부분은 컨텐츠 단 보다는 엔진단에 들어가는게 조금 더 맞을 거 같다는 생각이 들어.
static void **OnAcceptHandler**(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
**session.Start(clientSocket);**
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(10000);
session.Disconnect();
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, **OnAcceptHandler**);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
그렇다는 건 기존처럼 Listener에서 이렇게 _OnAcceptHandler를 Invoke하는게 아니라.
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
**_OnAcceptHandler.Invoke(args.AcceptSocket);**
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
Program.cs의 OnAcceptHandler에서 세션을 만들고 Start하는 이 부분을 엔진단으로 옮겨 보도록 할거야.
static void OnAcceptHandler(Socket clientSocket)
{
try
{
**GameSession session = new GameSession();**
**session.Start(clientSocket);**
Lisener.cs의
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
**GameSession session = new GameSession();
session.Start(args.AcceptSocket);**
// Invoke 할 때 실행될 부분을 바로 위로 옮겨 준 것
_OnAcceptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
OnAcceptCompleted로 옮겼다.
그리고 컨텐츠단의 함수를 받은 _OnAcceptHandler를 Invoke하는게 아니라, Listener.cs의
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
**GameSession** session = new GameSession();
**** session.**Start**(args.AcceptSocket);
**session.OnConnected(args.AcceptSocket.RemoteEndPoint)**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
session.OnConnected라는 얘를 이용할거야. endPoint를 넣어준다.
_OnAcceptHandle는 여기서 호출해 줄 필요가 없어졌기에 삭제해 준다.
이렇게 하면 코드 관리가 쉬워진 거 같긴 하다.
Start부분이 외부에 노출되기 보다는 안에서 하는게 더 맞기 때문.
이렇게 하면 문제는 GameSession을 new로 강제로 엔진단 안에서 만들고 있었어. 근데 GameSession은 컨텐츠 단에 빠져있는 애야. 어디서 만들어져 있나 보면 Program.cs의
namespace ServerCore
{
class GameSession : Session
{
Main쪽에 있는데 나중에는 Server project쪽(컨텐츠단)으로 빠지게 된다.
나중에는 MMOSession이라거나 다른 이름으로도 클래스를 만들어 줄 수 있을 거야. 나중에는 다른 애가 될 수도 있는건데 이런 식으로 만들어 주는 건 앞뒤가 안맞아 문제가 있다. 세션을 만들어 주는 부분은 밖에서 위임을 하거나 어떤 식으로 만들어 줄지는 밖에서 정해주는게 좋다. 안에서 만들거면 적어도 어떤 클래스를 만들지 어떤 식으로든 넘겨줘야 한다.
결국 어떻게 수정하면 되냐면,
Listener의
Action<Socket> _OnAcceptHandler;
을
Func<Session> _sessionFactory;
이렇게 이름을 바꿔주자.
session을 어떤 방식으로 만들어줄지를 정의하는 애가 된다.
Action과 다르게 Func는
Result타입이 있는 애다. 함수를 만들 때, 인자는 아무것도 안받고, Session이라는 형식을 뱉는 함수 형식을 받겠다는 거고,
public void Init(IPEndPoint endPoint, Func<Session> **sessionFactory**)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
**_sessionFactory += sessionFactory;**
이렇게 Listener의 Init에서 sessionFactory 를 연결해주도록 하자.
그럼 뭐가 달라진 것이냐면
Program의 Main에서
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
_listener.Init(endPoint, **OnAcceptHandler**);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
기존의 OnAcceptHandler를 받는 게 아니라
Session을 어떻게 누구를 만들 만들어 줄지를 정의를 해야 된다는 얘기가 된다.
지금은 간단하게 gameSession을 사용 할 것이다. 혹시라도 나중에 Session을 Manager에서 관리하거나 할 때에는 Manager를 통해 만들도록 수정을 하면 된다. 근데 지금은 간단하게 람다로 정의를 한다.
_listener.Init(endPoint, **() => { return new GameSession();** });
이렇게 정의 한거.
아까랑 비슷하면서 다른게 컨텐츠 단에서 뭔가를 하긴 하는데 OnAcceptHandle 같이 전체적 처리 특히 session.Start(clientSocket)같이 중요한 처리를 외부에서 처리를 하는게 아니라 무엇을 어떤 세션을 만들지만 결정을 해주고, 다머지 부분은
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
**GameSession session = new GameSession();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint)**
}
else
Console.WriteLine(args.SocketError.ToString());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
이런 식으로 Listener 안에서 다 해주고 있다.
결국 밖에서는 내부에서는 어떻게 구현되는지 알지도 못하고 관심도 없지만 이런식으로 실제로 뭔가가 만들어졌으면 OnConnected가 호출이 되면서 여기다가 재정의부에 뭔가 시작할 코드를 넣어주면 된다.
이전 시간에는 클라이언트가 접속을 했을 때
static void OnAcceptHandler(Socket clientSocket)
{
try
{
GameSession session = new GameSession();
session.Start(clientSocket);
**byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
session.Send(sendBuff);
Thread.Sleep(10000);
session.Disconnect();**
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
이런걸 해주고 있었어. 이부분을 컨텐츠 코드에서 하고 있었으니까 ctrl+c를 해서 OnConnected에 넣어주도록 하자.
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(10000);
Disconnect();**
}
상속 방식으로 구현해서 자체가 Session이기에 session.은 지워도 된다.
만약 상속 방식이 아니라 GameSession이 Handler로 빠졌으면 아까와 마찬가지로 session.Send를 해야 한다.
try, catch를 넣어줘도 되지만 일단은 이렇게만 해준다.
Program의 OnAccessHandler는 이제 볼일이 없으니 삭제해 준다.
우리가 Listener에서
Func<Session> _sessionFactory;
를 받은 이유는
OnAcceptCompleted에서
GameSession session = new GameSession();
이렇게 new를 하지 않고
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());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
이렇게 해주기 위함이었다. SessionFactory는 어떤 타입인지 모르니까 일단 가 상위 부모 클래스인 Session으로 뱉어주니까 받아주는 GameSession session을 Session 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(10000);
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($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
이런 식으로 Session을 상속 받아가지고 override한 버전을 하나씩 만들어서 처리를 하면 될 거 같고,
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
**_listener.Init(endPoint, () => { return new GameSession(); });**
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
_listener같은 경우에도 이런 식으로 무엇을 만들어 줄지만 지정을 해주면 된다.
이렇게 어느정도 모양새가 나오기 시작했다.
수정할 때 마다 테스트를 해봐서 작동하는지 확인하면 좋다.
나중에 코드가 많아지면 알게 되겠지만 몇가지 문제 사항이 있다.
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());
RegistterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
AcceptSocket에다 RemoteEndPoint를 추출해서 사용하고 있는데 Start에서 OnConnected로 넘어가는 순간 클라에서 연결을 끊어 버리면 애당초 연결이 끊어지니까 args.AcceptSocket.RemoteEndPoint여기에 접근하는게 허락이 되지 않는다.
이런거는 나중에 코드를 테스트 하면서 다 거르게 될거야. 서버를 초반에 만들 때는 간단한게 만들다가 테스트를 해보면서 문제가 생기면 하나씩 방어 코드를 넣어주는게 좋다.
초반에 책을 보고 외우는 방식으로 하면 남지가 않으니까 최대한 실험 정신을 발휘해서 작업을 해 나가면 좋다.
오늘 Session을 구분해서 관리하는 방법에 대해서 알아 봤고,
이어서 Connect에 관해서 알아볼건데
더미 클라이언트를 유심히 보면
socket.Connect(endPoint);
Connect를 이렇게 하고 있었어.
여기의 보내고 받는 부분도 우리가 만든 세션 방식으로 관리를 하면 RegisterSend, RegisterRecv같은 코드를 재사용할 수 있겠다는 생각이 든다.
그런 작업을 해볼거고, 오늘은 분리하는 간단한 작업을 해봤다.
질문. I worried about that GameSession is bigger than Session. Therefore when running Session session = _sessionFactory.Invoke(); this code, data will lose. right? because Session is smaller than GameSession.
답. By chatGPT
You don't need to worry about data loss in this case. When you assign a GameSession object to a Session variable, you're not actually copying the object or changing its size. Instead, you're just storing a reference to the GameSession object. The GameSession object still exists in memory as a GameSession, with all its data and methods intact.When you assign a GameSession object to a Session variable, you are using polymorphism, which allows a derived class (in this case, GameSession) to be treated as an instance of its base class (Session). This means that you can call any of the methods defined in the Session class on your GameSession object, even if it's stored in a Session variable.However, if you want to call a method specific to the GameSession class or access a property that is not present in the Session class, you would need to cast the Session variable back to a GameSession.
For example
Session session = _sessionFactory.Invoke();
GameSession gameSession = session as GameSession;
if (gameSession != null)
{
// Call a method or access a property specific to the GameSession class
}
In summary, you won't lose any data when assigning a GameSession object to a Session variable. The object is still a GameSession in memory, and you can cast it back to a GameSession when you need to access its specific features.
지금까지 코드
Listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Listener
{
Socket _listenSocket;
//Action<Socket> _OnAcceptHandler;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
//_OnAcceptHandler += onAcceptHandler;
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
for (int i = 0; i < 10; i++)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(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) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
// GameSession session = new GameSession();
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
Session
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
abstract class Session
{
Socket _socket;
int _disconnect = 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)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
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를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
//string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
//Console.WriteLine($"[FromClient] {recvData}");
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
Program
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
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(5000);
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($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
//static void OnAcceptHandler(Socket clientSocket)
//{
// try
// {
// //GameSession session = new GameSession();
// //session.Start(clientSocket);
// byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
// session.Send(sendBuff);
// Thread.Sleep(1000);
// session.Disconnect();
// session.Disconnect();
// }
// catch (Exception e)
// {
// Console.WriteLine(e);
// }
//}
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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, ()=> { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
DummyClient
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
internal 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);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기한테 입장 문의
socket.Connect(endPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
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());
}
Thread.Sleep(100);
}
}
}
}
'Server programming' 카테고리의 다른 글
02_11_네트워크 프로그래밍_TCP vs UDP (0) | 2023.04.06 |
---|---|
02_10_네트워크 프로그래밍_Connector (0) | 2023.04.06 |
02_08_네트워크 프로그래밍_Session #3_BufferList (0) | 2023.04.04 |
02_07_네트워크 프로그래밍_Session #2_Send (0) | 2023.04.04 |
02_06_네트워크 프로그래밍_Session #1_Recieve (0) | 2023.04.04 |
댓글