이번에는 이전의 포스트에서 사용한 람다식을 이용하여 Action을 자동으로 수행하는 방식이 아닌
새로운 Task 단위를 만들어 이를 수동으로 수행하는 방식으로 JobQueue를 구현해보겠습니다.
1) TaskQueue
interface ITask
{
void Execute();
}
// Task 중에서 Broadcast를 수행하는 Task
class BroadcastTask : ITask
{
GameRoom _room;
ClientSession _clientSession;
string _chat;
// 생성자에서 GameRoom, ClientSession, Chat을 초기화함
BroadcastTask(GameRoom room, ClientSession clientSession, string chat)
{
_room = room;
_clientSession = clientSession;
_chat = chat;
}
// 다른 ClientSession을 대상으로 Broadcast를 수행함
public void Execute()
{
_room.Broadcast(_clientSession, _chat);
}
}
// JobQueue와 동일한 역할을 수행하되, ITask를 저장하는 큐를 프로퍼티로 갖고 있음
class TaskQueue
{
Queue<ITask> _queue = new Queue<ITask>();
}
위와 같이 구성할 수 있습니다.
프로젝트에 따라서 람다식을 사용하지 않는 경우, 위와 같이 작성하여 사용하면 됩니다.
기존의 Invoke로 콜백함수를 실행시키지 않고 대신 Execute와 같은 사용자 정의 메서드를
사용하여 작업을 실행해야 하는 어쩌면 수동적인 방식이라고 생각하시면 됩니다.
2) 설계 방식
MMORPG의 경우, 많은 구현 방식이 존재하지만
이번에는 크게 2가지로 구분하여 JobQueue를 어떻게 적용할지 고민해보겠습니다.
2 - 1) Zone
Zone 방식의 게임에서는 큰 게임 맵을 Zone으로 여겨지는 각 영역으로 나눠서 제작합니다.
이때 하나의 Zone에서 다른 Zone으로 넘어가는 경우 로딩을 하는 시간동안
개별 Zone마다 존재하는 Job Queue를 처리하면 되므로 Job Queue가 유리합니다.
2 - 2) Seamless
Seamless 방식의 게임에서는 맵 간의 경계나 이동할 때 화면의 전환이 없는 방식으로 제작합니다.
이런 경우 개별 Job Queue를 배치하기도 힘들고 설령 배치하더라도
처리 시간이 존재하지 않아 Job Queue 방식보다는 다른 방식이 유리합니다.
이와 같은 차이점은
이번에 구현한 Job Queue 방식 외에도 많은 방식의 멀티 플레이 구현 방식이 존재하는 이유입니다.
3) 수정
3 - 1) Listener
public class Listener
{
Socket _listenSocket;
// Action과 다르게 리턴값이 존재하는 Func, 제네릭 자료형이 곧 리턴형이다
Func<Session> _sessionFactory;
// Server의 Client 소켓의 초기 상태를 설정
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 10)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// _sessionFactory로부터 리턴된 Session에 대해 호출할 이벤트를 연결
_sessionFactory += sessionFactory;
// ClientSocket에 대해 bind
_listenSocket.Bind(endPoint);
// Listen 상태 설정 - 최대 대기수
_listenSocket.Listen(backlog);
for (int i = 0; i < register; i++)
{
// 설정된 Client 소켓에 대해 비동기 방식으로 동작하도록 설정
// 비동기 방식으로 성공적으로 연결되는 경우, OnAcceptCompleted를 호출
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
}
.
.
.
}
Listener에서 설정한 Listen을 위한 최대 대기수와 Listen 소켓의 생성수를
하드 코딩 방식이 아닌 인자를 받아 동작하도록 수정하겠습니다.
새로 추가한 2개의 인자 backlog는 대기수를, register는 만드는 Listen 소켓의 수를 의미합니다.
3 - 2) Client
class Client
{
static void Main(string[] args)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return SessionManager.Instance.Generate(); }, 500);
while (true)
{
try
{
SessionManager.Instance.SendForEach();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(250);
}
}
}
Client에서 500개의 ServerSession을 생성하도록 수정하였습니다.
3 - 2) ClientSession
class ClientSession : PacketSession
{
.
.
.
public override void OnSend(int numOfBytes)
{
// System.Console.WriteLine($"Transferred bytes : {numOfBytes}");
}
}
또 서버에서 클라이언트로 ClientSession을 통해 OnSend하지 않도록 주석처리하였습니다.
3 - 3) Server
class Server
{
static Listener serverListener = new Listener();
// 새로운 채팅방을 위한 GameRoom 생성
public static GameRoom Room = new GameRoom();
static void Main(string[] args)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
serverListener.Init(endPoint, () => { return SessionManager.Instance.Generate(); }, 500, 500);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
마지막으로 Server에서 ClientSessiond에서 추가한 인자를 각각 500과 500으로 설정하였습니다.
최종 실행결과는 다음과 같습니다.
총 500개의 클라이언트가 잘 생성되고 통신까지 이뤄지는 것을 확인할 수 있습니다.
그러나 아직 문제가 완전히 해결된 것은 아닙니다.
현재 한 개의 클라이언트마다 할당된 스레드는 0.25초 주기로 작업을 수행합니다.
이때 500개의 클라이언트 별로 메세지를 Broadcast하고 있습니다.
다시 얘기하면 초당 만번되는 메세지가 브로드캐스트되고 있는데,
이를 시간복잡도로 치환하면 N^2의 이라는 무시하지 못할 수치가 됩니다.
따라서 해결책은 이런 메세지를 매 프레임마다 보내는 것이 아닌 한번에 모아서 전송하는 것인데,
다음에는 이 방법을 구현해보도록 하겠습니다.
'C#' 카테고리의 다른 글
JobTimer (0) | 2025.01.27 |
---|---|
패킷을 모아서 전송하기 (0) | 2025.01.27 |
JobQueue #1 (0) | 2025.01.22 |
Command 패턴 (0) | 2025.01.22 |
채팅 테스트 #2 (0) | 2025.01.21 |