이번 시간에는 엔진단과 컨텐츠단을 분리하는 연습을 할 것이다.
먼저 Session 클래스를 추상 클래스로 정의하여 추상함수를 다음과 같이 만든다.
abstract class Session
{
Socket _socket;
int _disconnected = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
// 이런 식으로 하면 RegisterSend 할 때 마다 Argument를 인자로 전달할 필요 없음
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
abstract public void OnConnected(EndPoint endPoint);
abstract public void OnRecv(ArraySegment<byte> buffer);
abstract public void OnSend(int numOfBytes);
abstract public void OnDisconnected(EndPoint endPoint);
그 다음 Session을 상속 받은 GameSession 이라는 클래스를 만든다.
MMORPG Session, AOS Session, FPS Session 등 게임에 따라 각기 다른 Session을 가져야 할 수 도 있다. 상속은 그런 개념이다.
class GameSession : Session // 엔진딘과 컨텐츠단를 분리
{
public override void OnConnected(EndPoint endPoint) // 연결됬을 때 할 것
{
Console.WriteLine($"On connected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server");
Send(sendBuff);
Thread.Sleep(1000);
// 쫒아낸다.
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"On Disconnected : {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($"Transferd Bytes {numOfBytes}");
}
}
무엇보다 OnConnected 추상 메소드에서 Send( )를 쓴 것 처럼 Session 클래스의 메소드에 쉽게 접근할 수 있는 것이 상속의 장점이다.
On~~ 함수를 만들어서 OnEventCompleted 함수의 코드 중 내부에 위치할 필요가 없는 코드를 밖으로 빼낸다.
Listener 클래스도 조금 손을 보자.
Main(외부)에서 세션의 종류를 선택할 수 있게 아래와 같이 Listener.Init을 수정하자
static void Main(string[] args)
{
//DNS(Domain Name System)
string host = Dns.GetHostName(); // DESKTOP-S7EG95G
IPHostEntry ipHost = Dns.GetHostEntry(host); // System.Net.IPHostEntry
IPAddress ipAddr = ipHost.AddressList[0]; // fe80::b599:93ca:869a:5736%18
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // 주소와 포트번호
_listener.Init(endPoint, ()=> { return new GameSession(); });
// 외부에서는 어떤 세션을 만들지(GameSession)만 결정해준다.
// 전체적인 처리는 내부에서 처리한다.
Init의 매개변수를 Action 대신에 Func로 수정하였다.
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 첫 번째 매개변수: IP 버전4 인지 버전6인지
// 그 다음 매개변수: TCP를 쓸건지 UDP를 쓸건지
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint); // 문지기의 핸드폰에 비밀식당의 주소를 알려주는 것.
// 영업시작
_listenSocket.Listen(10);
for (int i = 0; i < 10; i++) // 문지기를 여러개 생성
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
}
리스너가 Accept 할 때 대리인에게 소켓을 건네주는 동작을 바로 하자.
public void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
// 실제로 유저가 왔으면 할 것
Session session = _sessionFactory.Invoke(); // 강제로 GameSession으로 만들어줘?
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
// 대리인에게 소켓을 주는 역활을 한다. args.AcceptSocket 한테 대리인의 소켓을 넣어준다.
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 낚시대를 바다에 다시 던진다.
}
이런 식으로 엔진단의 코드는 내부에서 관리하고
외부에서는 컨첸츠에 따라 간단한 수정만 필요하게 수정을 하자.