Server programming

04_01_Job Queue_채팅 테스트 #1_서버

devRiripong 2023. 4. 25.
반응형

다룰 내용 및 요약

이번 시간에는 서버 코어에서 발생하는 크래시를 수정하고 새로운 패킷을 설계하는 방법을 다룹니다. 또한, 클라이언트가 접속하면 자동으로 채팅방에 입장하게 하고, 게임 룸에 입장 및 퇴장 기능을 추가하는 방법을 소개합니다. 이를 통해 클라이언트가 채팅 패킷을 보낼 때 서버에서 처리하는 방법을 구현할 수 있습니다.

이러한 구현을 통해 서버와 클라이언트 간의 채팅 기능이 원활하게 동작하도록 할 수 있으며, 게임 내 채팅 기능을 구현하는 데 필요한 기본 구조를 마련할 수 있습니다. 이를 통해 채팅 기능이 추가된 게임 서버를 구축할 수 있습니다.

 

 

 

 

패킷까지는 어느정도 준비가 됐고, 세션에 대한 네트워크 부분도 어느정도 준비가 됐으니까, 이어서 컨텐츠를 작업할 대략적인 준비는 끝났다고 볼수 있다.

하지만 이 상태에서 해보라고 넘겨주면 난해할거야.

네트워크 상으로 뭔가를 흘려 보내서 클라와 서버랑 통신을 할 수 있다는 거 까지는 알겠는데 이거를 어떻게 구조를 짜서 게임이든 MMO 같은 온라인 게임을 만들어야 할지 굉장히 아리달송 할거다.

온라인 게임에 관한 책들도 초반에 보면 여기까지가 끝이다. 이 다음이 굉장히 아리달송 하다.

선생님은 여기까지 하는 작업을 5번은 해봤다.

여기서 어떻게 진행을 해야 할까?

 

왼쪽이 클라이언트고 식당이 서버 역할을 한다.

코드에서 보면 listener가 문지기 역할을 하는 거였고,

문지기를 통해서 문지기가 accept를 해서 들여 보내면 Session이 하나 탄생을 하는 거.

이 세션의 이름은 ClientSession 이라고 부르는 아이다.

클라쪽에서도 서버와 통신을 할 수 있는게 생기는데 ServerSession의 역할을 한다고 볼 수 있다.

클라이언트 쪽의 ServerSession은 크게 중요하지 않고 사실 Server쪽에 있는 애가 중요하다. MMO 관점에서 본다 치면 결국 클라이언트가 하나하나 접속할 때 마다 서버에 클라이언트의 대리인이 생기는 현상이생기게 되겠고, 결국 클라이언트 세션이 점점 늘어나게 될거야. 5000명의 유저가 있으면 클라이언트 세션도 5000개가 붙어있을텐데 그럼 여기부터 이걸 어떻게 응용해서 게임을 만들어야 하느냐에 대한 내용을 오늘 시작을 하고자 한다.

채팅 서버를 구현해 보면 감이 훨씬 잘 오기 때문에 그 부분부터 진행을 해보고, 그렇게 간단하게 구현 했을 때 어떤 문제점이 있을지 알아본 다음에 이번 파트의 핵심적인 내용인 job Queue도 알아 볼 것이다.

1. 서버 코어에서 크래시 나는 부분 수정

이번 시간 부터 작업을 하면서 기존에 챙기지 않았던 나머지 문제들까지 챙길건데 지금 서버 코어에 보면 한번씩 크래시가 나는 상황이 발생할거야. 그이유는 꼼꼼하게 처리를 해주지 않은 부분이 몇 개가 있다.

1_1. _socket 다루는 부분 Disconnect 할 때 대비

예를 들어 ServerCore의 Session에서 Disconnect를 할 때

public void Disconnect()
{
    if (Interlocked.Exchange(ref _disconnect, 1) == 1)
        return;

    OnDisconnected(_socket.RemoteEndPoint); 
    _socket.Shutdown(SocketShutdown.Both);
    _socket.Close();
}

Disconnect를 중복해서 호출하는 걸 방지하기 위해서 이렇게 안전 처리를 해놓긴 했지만 이것만으로는 모든 상황에 대한 처리를 하기엔 부족하다.

예를 들면 RegisterSend라든가 RegisterRecv하는 부분에서는 끊겼을 때에 대한 처리가 없다.

누군가는 socket을 Shutdown까지 했는데 동시 다발적으로 Send나 Recv를 해서

void RegisterRecv()
{
    _recvBuffer.Clean(); 
    ArraySegment<byte> segment = _recvBuffer.WriteSegment;
    _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); 

    **bool pending = _socket.ReceiveAsync(_recvArgs);
    if (pending == false)
        OnRecvCompleted(null, _recvArgs);** 
}

아래 이 부분이 호출이 된다고 하면

아마 여기서 뻑이 날 확률이 있다.

C#같은 경우는 뭔가 exception으로 뱉어주고, 그걸 처리하지 않으면 크러시가 나기 때문에 그 부분을 챙겨줘야 한다.

간단하게 예외처리를 몇개 하고 부족하면 나중에 추가를 하면 되는데

RegisterSend를 하는 부분에서도

void RegisterSend()
{
    if (_disconnect == 1)
        return;

이건 물론 최소한의 방어이고, 이것만으로는 멀티스레드 환경에서 보호할 수 있다고 할 수는 없다.

if (_disconnect == 1)

이 조건을 통과해서

void RegisterSend()
{
    if (_disconnect == 1)
        return; 

    while (_sendQueue.Count > 0)
    {
        ArraySegment<byte> buff = _sendQueue.Dequeue();
        _pendingList.Add(buff); 
    }
		_sendArgs.BufferList = _pendingList; 

    bool pending = _socket.SendAsync(_sendArgs);
    if (pending == false)
        OnSendCompleted(null, _sendArgs); 
}

while문으로 넘어 왔는데 하필이면 이 상태에서 다른 스레드가 _disconnected를 해가지고 socket이 끊긴 상태라고 하면

	  bool pending = _socket.SendAsync(_sendArgs);
    if (pending == false)
        OnSendCompleted(null, _sendArgs); 

얘는 실행하면 안되는 애가 되는 거니까,

안전하게 하기 위해서 socket을 다루는 부분try catch로 감싸주도록 한다.

void RegisterSend()
{
    if (_disconnect == 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}");
    }
}

RegisterRecv에도 try catch로 예외처리를 해주자.

void RegisterRecv()
{
    if (_disconnect == 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}"); 
    }
}

지금은 다 콘솔 로그로 찍고 있지만 나중엔 파일로 찍어서 꼼꼼하게 처리를 해야 한다.

일단은 간단하게만 테스트 하기 위해서 빨리 진행을 한다.

1_2. Disconect할 때 _sendQueue랑 _pendingList를 정리하는 작업

public void Disconnect()
{
    if (Interlocked.Exchange(ref _disconnect, 1) == 1)
        return;

    OnDisconnected(_socket.RemoteEndPoint); 
    _socket.Shutdown(SocketShutdown.Both);
    _socket.Close();
}

Disconnect에서 소켓을 닫아주는 거 까지 했는데 _sendQueue_pendingList는 정리를 안해 놨으니까 얘를 정리하는 작업까지 추가로 해주도록 한다.

ServerCore의 Session에 Clear를 정의한다.

void Clear()
{
    lock(_lock)
    {
        _sendQueue.Clear();
        _pendingList.Clear(); 
    }
}

Clear는 Disconnect를 한 다음에

public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnect, 1) == 1)
                return;

            OnDisconnected(_socket.RemoteEndPoint); 
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
            Clear(); 
        }

이 즈음에다가 호출하도록 한다.

이정도만 추가를 해주고 다른 문제가 생기면 그 때 가서 이어서 처리를 해준다.

네트워크는 이정면 어느정도 땜빵이 된 거 같고,

 

오늘 이어서 할 것은 일단 PDL에 간 다음에

간단한 채팅을 만들어 보도록 할거야.

사실은 서버를 만들 때 처음에는 채팅 프로그램으로 테스트를 해보는게 좋은데 그 이유는 사실은 채팅을 만들 수 있으면 MMO도 똑같이 만들 수 있기 떄문이야. 실제로 채팅 프로그램을 만들어 보면 알게 될거야.

같은 방에 있는 모든 애들한테 채팅 내용을 뿌린다는게 사실은 게임이랑 거의 비슷하기 때문인데 게임이라는 것도 별게 아니라 어떤 한 지역에 다수의 유저들이 몰려 있는데 어떤 한 유저가 패킷 요청이 와서 움직이고 스킬을 쓰거나 하면 주변에 있는 모든 애들한테 뿌리거나 하는게 사실 온라인 게임이다. 크게 보면 그거 밖에 없는 거.

2. PDL에서 패킷 2개 설계

굉장히 간단하게 만들어 보기 위해서 PDL에 가서 일단 패킷은 2개만 설계 하도록 한다.

<?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>

.bat 파일을 실행해 보면

GenPackets랑 ClientPacketManager에도 잘 바뀌어 있는 걸 확인할 수 있다.

이어서 얘에 맞춰서 작업을 해주면 된다.

 

패킷을 판 다음에는 복잡하게 생각할 거 없이 그냥 코드의 작업 순서대로 작업을 하면 편리하다.

회사에서 클라랑 서버를 동시에 다루는 팀은 많지 않고, 대부분 서버팀이나 클라팀 둘중 하나로 들어가게 될거야. 그러면 내가 서버 파트면 서버와 관련된 부분을 만들거고, 클라 팀이라면 클라와 관련된 부분을 만든 다음에 클라와 서버와 담당자끼리 다 완성이 됐으면 합체를 해서 테스트하는 작업을 거치게 되는데 그렇게 하면 당연히 작업 속도가 느릴거야.

근데 지금 현재는 클라, 서버 양쪽을 다 쓰고 있으니까 굉장히 편리하게 작업을 할 수 있다.

3. 클라이언트가 접속하면 강제로 채팅방에 입장하게 하기

ClientSession에 가서 살펴 보면

클라이언트가 접속했을 때 지금까지는 아무것도 해주는 것이 없었지만

이제는 클라이언트가 접속을 했으면 강제로 어떤 채팅방에 입장하게 해주자.

물론 정확하게 나중에 온라인게임을 만든다면 이 단계에서 입장 시키는게 아니라 클라이언트 쪽에서도 모든 리소스 로딩을 끝냈다고 신호를 보내면 그 때 입장을 시키는게 맞긴 하겠지만 여기선 일단 그정도까진 꼼꼼하게 구현하진 않는다.

class ClientSession : PacketSession
{
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"OnConnected : {endPoint}");
                    
        Thread.Sleep(5000);
        Disconnect();
    }

여기다가 이제 Disconnect를 하는게 아니라 뭔가를 넣어 주도록 할거야.

 

Server쪽에서 일단 방이라는 개념을 만들어 주기 위해서 추가-새항목-클래스로 GameRoom이라는 클래스를 추가해준다.

일단은 GameRoom에는 ClientSession들이 옹기종기 모여있을 것이다.

class GameRoom
{
    List<ClientSession> _sessions = new List<ClientSession>();
    
    public void Enter(ClientSession session)
    {
        _sessions.Add(session);
    }

    public void Leave(ClientSession session)
    {
        _sessions.Remove(session); 
    }
}

이렇게 EnterLeave를 추가해준다.

 

그리고 Client 세션 쪽에서도 내가 어떤 방에 있는지 궁금해 할 수 있으니까

ClientSession에 가가지고 여기서도 룸의 존재를 알려주기 위해서

그리고 SessionId를 지녀서 SessionId로 구분을 할 수 있게 파준다.

public int SessionId { get; set; }
public GameRoom Room { get; set; }

결국 다시 한번 GameRoom에 돌아가가지고 이어서 작업을 해주면

public void Enter(ClientSession session)
{
    _sessions.Add(session);
    session.Room = this;
}

session.Room에 자기 자신을 전달해준다.

유니티로 작업을 했으면 이정도에서 끝내주면 되겠지만,

4. GameRoom의 Enter와 Leave에 lock 걸어주기

굉장히 조심해야 하는게 모든 게 다 멀티 쓰레드 환경에서 다 돌아간다는 걸 염두해 둬야 한다.

왜냐하면 네트워크 패킷이 Recv이벤트가 발생을 해가지고 조립이 된 다음에 패킷 핸들러로 넘어 오는 것은 동시 다발적으로 여러 스레드에서 일어날 수 있는 거기 때문에 GameRoom에다가 EnterLeave를 하는 것도 동시 다발적으로 실행이 된다는 걸 반드시 가정을 하고 코딩을 해야 한다.

근데 애당초 GameRoom에 이런 ListDictionary나 대부분의 자료구조들은 멀티스레드 환경에서 돌아간다는게 보장이 되지 않는다.

그러기 때문에 _session.Add(session);을 이런식으로 풀어주면 문제가 일어나기 때문에 얘를 Lock으로 감싸 줄거야.

namespace Server
{
    class GameRoom
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        object _lock = new object(); 
        public void Enter(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Add(session);
                session.Room = this;
            }
        }

        public void Leave(ClientSession session)
        {
            lock(_lock)
            {
                _sessions.Remove(session);
            }
        }
    }
}

이렇게 상호 베타적으로 누군가가 Enter를 하고 있으면 조금 기다리도록 맞춰준 것이다.

5. GameRoom을 Server의 Program에 가서 static변수로 생성하기

GameRoom 같은 경우는 Server의 Program.cs쪽으로 가서 여기다가 static으로 하나 만들어 주도록 한다.

class Program
    {
        static Listener _listener = new Listener();
        public static GameRoom Room = new GameRoom(); 

        static void Main(string[] args)
        {
            PacketManager.Instance.Register();

            // 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 ClientSession(); });
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
            }
        }
    }

6. ClientSession을 SessionManager로 발급하기

나중에 가면 GameRoom이 이렇게 하나만 있는게 아니라 이 Room도 뭔가 매니저가 있어가지고 당연히 조종을 할 필요가 있을거야.

그리고 ClientSession같은 경우에는 여기서 그냥 이렇게 ClientSession을 return new ClientSession();를 덩그라니 하고 있었는데 사실 이것도 개선을 하자면 중간에 SessionManager를 둬가지고 걔가 Session을 발급 해주도록 만들어 주도록 해야지 조금 깔끔하게 관리가 될거야. 그래야지만 SessionId나 Session이 몇개 있는지 count 같은 거 관리 하기도 용이하겠고, 그러다 보니까 여기다 ClientSession Manager를 하나 만들어 줄건데 ServerCore쪽에 밀어 넣어줘도 되고, 윗단 컨텐츠 단에서 관리를 해도 괜찮다. 이건 큰 차이는 없고 선택의 영역인 거 같아.

어쨌든 Server에 새항목 추가로 SessoinManager라는 애를 만들어 준 다음에

namespace Server
{
    class SessionManager
    {
        static SessionManager _session = new SessionManager(); 
        public static SessionManager Instance { get { return _session; } }
    }
}

그러면 언제 어디서든 SessionManager의 Instance를 호출하면 static으로 만들어진 _session이 아이를 불러올거니까 편리하게 작업을 할 수 있다.

namespace Server
{
    class SessionManager
    {
        static SessionManager _session = new SessionManager(); 
        public static SessionManager Instance { get { return _session; } }

        int _sessionId = 0; // 발급을 하기 위 한 티켓 번호 0부터 늘어나게 될거야
        Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession>(); // ClientSession 관리 
        object _lock = new object(); // 락을 걸기 위한 객체 

        public ClientSession Generate()
        {
            lock(_lock)
            {
                int sessionId = ++_sessionId;

                ClientSession session = new ClientSession();
                // 혹시라도 나중에 Pooling 할 생각이 있어서 이미 만들어져 있는 ClientSession들을 뱉고 싶다고 하면
                // 초반에 이렇게 실컷 만들어준 애들을 Queue에다가 저장하고 있다가 Queue에서 뽑아서 건내주는 방법으로 할 수도 있을거야. 
                // 여기선 굳이 Session을 pooling 할 필요는 없는 거 같아. 지금은 동적으로 요청이 왔을 때 만들어주는 형식으로 해준다. 
                session.SessionId = sessionId;
                _sessions.Add(sessionId, session);

                Console.WriteLine($"Connected : {sessionId})");

                return session; // 만들어 준 애를 반환
            }
        }

				public ClientSession Find(int id)   //id를 이용해서 세션을 찾는 인터페이스 
        {
            lock(_lock)
            {
                ClientSession session = null;
                _sessions.TryGetValue(id, out session);
                return session; 
            }
        }

        public void Remove(ClientSession session)
        {
            lock(_lock)
            {
                _sessions.Remove(session.SessionId); 
            }
        }
    }
}

이렇게 코드를 추가해주고

Sever에 새 폴더를 추가해 준 다음에 이름을 Session이라고 하고

ClientSessoin과 SessionManager를 옮겨준다.

그리고 Server의 Program에 가서

Main에서 기존의

_listener.Init(endPoint, () => { return new ClientSession(); }

ClientSession을 만들어주고 있던 애를 이런식으로 직접 만드는게 아니라

_listener.Init(endPoint, () => { return SessionManager.Instance.Generate(); });

이렇게 SessionManger를 통해서 만들어 주도록 수정을 해줄거야.

그럼 이제 SessionManager가 발급을 관리를 하게 될 거. 동시에 id 발급도 잘 해주게 된다.

7. Session 제거해 주기

얘를 해주자마자 바로 떠올려야 하는게 어딘가에서 Remove 즉 Session을 제거해 주는 작업을 해줘야 하는데 대충 ClientSession에서 해주면 괜찮겠다라는 생각이 든다.

OnDisconnected를 해주는 동시에

public override void OnDisconnected(EndPoint endPoint)
{
    SessionManager.Instance.Remove(this); 

    Console.WriteLine($"OnDisconnected : {endPoint}");
}

자신을 SessionManager에서 해제해줘 요청을 해주면 된다.

8. GameRoom 들어오고 나가는 처리

여기까지 했으면 Session이 동적으로 잘 Manager를 통해서 잘 관리가 되고 있으니까 이어서 GameRoom을 이용해서 해주면 될텐데

지금 Server의 Program에 static으로 Room을 하나 만들어 놨어. 사실상 애도 전역으로 하나만 존재하는 개념이 될테니까 기존의 ClientSession에 가서

처음에 OnConnected를 했을 때

public override void OnConnected(EndPoint endPoint)
{
    Console.WriteLine($"OnConnected : {endPoint}");

    Program.Room.Enter(this); 
}

이렇게 접속했으면 일단 방에 넣어준다.

마찬가지로 OnDisconnected가 호출되가지고 방에서 만약 나와야 한다고 하면

public override void OnDisconnected(EndPoint endPoint)
{
    SessionManager.Instance.Remove(this); 

    **if(Room != null) // 두번 호출 방지
    {
        Room.Leave(this);
        Room = null; 
    }**

    Console.WriteLine($"OnDisconnected : {endPoint}");
}

Server의 Program.cs의 Main에서 Generate를 호출해 SessionManager의 _sessions에 ClientSession을 생성해 넣은 등록하고**,** OnDisconnected에서 자신을 넣어 _sessions에서 제거한다.

OnConnected할 때 내가 접속한 채팅방 GameRoom에도 접속을 한 상태가 된다.

9. 클라쪽에서 채팅 패킷 보냈을 때 Handler 처리

여기서 하고 싶은 건 OnRecvPacket, 즉 클라쪽에서 뭔가 채팅 패킷을 보냈다고 가정을 하면 걔가 PacketHandler쪽으로 오게 된다. 그 부분을 처리를 해준다.

class PacketHandler
{
    public static void C_PlayerInfoReqHandler(PacketSession session, IPacket packet)
    {
        C_PlayerInfoReq p = packet as C_PlayerInfoReq;

        Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

        foreach (C_PlayerInfoReq.Skill skill in p.skills)
        {
            Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
        }
    }
}

에서

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);       
    }
}

이렇게 수정한다. 얘가 하고 싶었던 메시지string chat으로 올테니까 얘를 모두한테 뿌려주는 역할을 한다.

이제 Broadcast 인터페이스를 만들러 가야한다.

10. Broadcast 인터페이스 만들기

GameRoom으로 다시 한번 들어 간다음에

public void Broadcast(ClientSession session, string chat)
{
    S_Chat packet = new S_Chat();
    packet.playerId = session.SessionId;
    packet.chat = chat;
    // 지금 사용하고 있는 인터페이스는 byte의 ArraySegment로 추출을 해야 되니까 
    ArraySegment<byte> segment = packet.Write();

    lock (_lock)
    {
				foreach (ClientSession s in _sessions)
	          s.Send(segment);
    }
}

이렇게 해주는데 lock 이전에는 멀티스레드의 영역이긴 하지만 동시 다발적으로 접근하는게 아니라 넘겨줄 인자들만 만들어주고 있었으니까 별 문제 없었지만 이제부터는 공유하고 있는 변수 즉 sessions를 다루게 될때는 무조건 lock을 걸어주고 접근을 해야 한다. 처음에는 헷갈릴 수 있어. 왜 얘는 lock을 안걸지 싶기도 한데, 결국에는 내가 다른 스레드랑 공유하고 있는 데이터인지 아닌지를 곰곰히 계속 생각을 해보는 습관을 들여야 한다. 나중에 가면 자연스럽게 나오게 된다. 지금은 크래쉬를 내보면서 배우면 된다.

결국 BroadCast를 쓰게 되면 방에 있는 모든 애들한테 lock을 건 상태로 하나씩 하나씩 순회를 하면서 채팅 메시지를 다 뿌려주는 상황이 되는 거.

여기까지 했으면 서버쪽은 어느정도 완성이 된 거.

다음 시간에는 클라이언트 쪽에서 생각을 해본다.

 

 

확인 테스트

Q1. 이번 시간에 한 작업들을 나열해 보세요

1.서버 코어에서 크래시 나는 부분 수정

    1_1. _socket 다루는 부분 Disconnect 할 때 대비

    1_2. Disconect할 떄 _sendQueue랑 _pendingList를 정리하는 작업

 

2.PDL에서 패킷 2개 설계\

 

3.클라이언트가 접속하면 강제로 채팅방에 입장하게 하기

 

4.GameRoom의 Enter와 Leave에 lock 걸어주기

 

5.GameRoom을 Server의 Program에 가서 static변수로 생성하기

 

6.ClientSession을 SessionManager로 발급하기

 

7.Session 제거해 주기

 

8.GameRoom 들어오고 나가는 처리

    ClientSession에서 OnConnected를 했을 때 Enter

    Disconnect 했을 때 Leave

 

9.클라쪽에서 채팅 패킷 보냈을 때 Handler 처리

 

10.Broadcast 인터페이스 만들기

 

반응형

댓글