C#

채팅 테스트 #1

monstro 2025. 1. 21. 12:11
728x90
반응형

이번 포스트에서는 기존에 만든 프로그램을 채팅 프로그램으로 만들어보도록 하겠습니다.

시작하기에 앞서 기존의 자동생성하는 코드를 일부 수정하겠습니다.

 

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 코드를 위와 같이 수정하였습니다.

RegisterSendRegisterRecv_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를 추가하고, 채팅방을 프로퍼티로 갖고 있게끔 설정하였습니다.

또한 연결을 수행하고, 해제하는 OnConnectedOnDisconnected에서

채팅방에 관련한 로직을 추가하였습니다.

 

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을 생성합니다.

 

다음 포스트에서는 클라이언트에서 마저 설정을 수행하도록 하겠습니다.

728x90
반응형

'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