이번 포스트에서는 기존에 만든 프로그램을 채팅 프로그램으로 만들어보도록 하겠습니다.
시작하기에 앞서 기존의 자동생성하는 코드를 일부 수정하겠습니다.
1) 조상 Session
.
.
.
public abstract class Session
{
.
.
.
object _lock = new object();
.
.
.
void Clear()
{
lock (_lock)
{
_sendQueue.Clear();
_pendingList.Clear();
}
}
.
.
.
public void Send(ArraySegment<byte> sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnnected, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
Clear();
}
// 클라이언트의 메세지를 받기 위해 비동기로 대기를 수행하는 RegisterRecv에서
// _recvBuffer를 사용하여 매번 새로운 버퍼를 만들지 않고 WritePos를 기준으로 클라이언트의 메세지를 저장함
void RegisterRecv()
{
if (_disconnnected == 1)
return;
_recvBuffer.Clean();
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
try
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
catch (Exception e)
{
Console.WriteLine($"RegisterRecv Failed {e}");
}
}
.
.
.
void RegisterSend()
{
if (_disconnnected == 1)
return;
while (_sendQueue.Count > 0)
{
ArraySegment<byte> buff = _sendQueue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
try
{
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
catch (Exception e)
{
Console.WriteLine($"RegisterSend Failed {e}");
}
}
.
.
.
}
기존의 조상 Session 코드를 위와 같이 수정하였습니다.
RegisterSend와 RegisterRecv에 _disconnected 상태인 경우 수행하지 않도록 로직을 추가하였고,
또한 try-catch문으로 구조를 변경하여 소켓을 다루도록 하였습니다.
또한 초기화하는 Clear 메서드를 추가하였고 Disconnect에서 이를 수행합니다.
마지막으로 Send를 멀티 스레드 환경에 맞게 Lock을 걸어 수행합니다.
2) 패킷 구조 xml
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name ="C_Chat">
<string name="chat"/>
</packet>
<packet name ="S_Chat">
<int name ="playerID"/>
<string name="chat"/>
</packet>
</PDL>
패킷 구조를 위와 같이 변경하였습니다.
또한 이번 포스트에서는 서버 단계에서 채팅방을 위한 준비를 수행합니다.
따라서 서버 위주로 작성되었습니다.
3) GameRoom
class GameRoom
{
List<ClientSession> _sessions = new List<ClientSession>();
object _lock = new object();
public void Broadcast(ClientSession session, string chat)
{
S_Chat packet = new S_Chat();
packet.playerID = session.ClientSessionID;
packet.chat = chat;
ArraySegment<byte> segment = packet.Write();
lock (_lock)
{
foreach (ClientSession eachSession in _sessions)
eachSession.Send(segment);
}
}
public void Enter(ClientSession session)
{
lock (_lock)
{
_sessions.Add(session);
session.Room = this;
}
}
public void Leave(ClientSession session)
{
lock (_lock)
{
_sessions.Remove(session);
}
}
}
서버에 새로운 클래스 GameRoom을 추가합니다.
GameRoom은 채팅창의 역할을 수행합니다.
Lock을 통해 비동기적으로 입장과 퇴장을 수행하도록 설계하였습니다.
또 Broadcast를 통해 방에 입장한 다른 ClientSession들에게 메세지를 전파합니다.
4) ClientSesison
class ClientSession : PacketSession
{
public int ClientSessionID {get; set;}
public GameRoom Room { get; set; }
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Program.room.Enter(this);
}
.
.
.
public override void OnDisconnected(EndPoint endPoint)
{
SessionManager.Instance.Remove(this);
if (Room != null)
{
Room.Leave(this);
Room = null;
}
Console.WriteLine($"OnDisconnected : {endPoint}");
}
.
.
.
}
ClientSession에 세션 판별을 위한 ID를 추가하고, 채팅방을 프로퍼티로 갖고 있게끔 설정하였습니다.
또한 연결을 수행하고, 해제하는 OnConnected와 OnDisconnected에서
채팅방에 관련한 로직을 추가하였습니다.
5) SessionManager
class SessionManager
{
static SessionManager _session = new SessionManager();
public static SessionManager Instance { get { return _session; } }
int _sessionID = 0;
Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession>();
object _lock = new object();
public ClientSession Generate()
{
lock (_lock)
{
int sessionID = ++_sessionID;
ClientSession session = new ClientSession();
session.ClientSessionID = sessionID;
_sessions.Add(sessionID, session);
Console.WriteLine($"Connnected : {sessionID}");
return session;
}
}
public ClientSession Find(int ID)
{
lock (_lock)
{
ClientSession session = null;
_sessions.TryGetValue(ID, out session);
return session;
}
}
public void Remove(ClientSession session)
{
lock (_lock)
{
_sessions.Remove(session.ClientSessionID);
}
}
}
많은 수의 ClientSession을 효율적으로 관리하는 SessionManager 클래스도 생성하였습니다.
ClientSession을 생성하는 Generate 함수와 특정 ClientSession을 찾는 Find 함수 그리고
마지막으로 특정 ClientSession을 삭제하는 Remove 함수로 구성되어 있습니다.
6) PacketHandler
class PacketHandler
{
// 인자는 어떤 세션에서 , 어떤 패킷이 조립되었는지를 의미함
public static void C_ChatHandler(PacketSession session, IPacket packet)
{
C_Chat chatPacket = packet as C_Chat;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
clientSession.Room.Broadcast(clientSession, chatPacket.chat);
}
}
패킷 구조가 변경되었으므로 PacketHandler 역시 수정하였습니다.
C_Chat의 패킷의 경우 채팅방에서 Broadcast를 수행하도록 설정하였습니다.
7) Server
class Server
{
static Listener serverListener = new Listener();
// 새로운 채팅방을 위한 GameRoom 생성
public static GameRoom room = new GameRoom();
static void Main(string[] args)
{
.
.
.
serverListener.Init(endPoint, () => { return SessionManager.Instance.Generate(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
Server의 코드를 위와 같이 변경하였습니다.
유일한 GameRoom의 인스턴스를 멤버 변수로 포함하였고,
Server의 메인 로직에서 Listen을 수행하는 경우 SessionManager를 통해 ClientSession을 생성합니다.
다음 포스트에서는 클라이언트에서 마저 설정을 수행하도록 하겠습니다.
'C#' 카테고리의 다른 글
Command 패턴 (0) | 2025.01.22 |
---|---|
채팅 테스트 #2 (0) | 2025.01.21 |
Packet Generator #6 (0) | 2025.01.20 |
Packet Generator #5 (0) | 2025.01.20 |
Packet Generator #4 (0) | 2025.01.08 |