설명
Server의 Main에서 Listener를 Init할때 ClientSession 객체를 바로 생성해주는 것이 아닌, SessionManager를 통해 Generate해서 모든 세션을 관리하는 방식을 채택했다.
세션마다 고유 번호를 할당하고 id를 키값으로 Dictionary에 ClientSession을 넣어서 생성, 찾기, 삭제를 할 때마다 lock을 걸어 동시 접근을 차단하는 식으로 구현했다.
서버에는 하나의 GameRoom이 존재하며, 하나의 GameRoom에는 List타입의 _sessions가 존재한다. 그리고 GameRoom에서 어떤 하나의 클라이언트가 메시지를 서버에게 전송하면 서버는 입장 중인 모든 세션 객체에 그 메시지를 Broadcast한다.
하나의 ClientSession이 서버와의 접속이 끊어질 경우, OnDisconnected함수가 호출되어 SessionManager.Remove와 해당 세션이 참여 중인 GameRoom이 있을 경우 GameRoom.Leave() 함수 호출을 통해 퇴장하도록 했다.
10명의 유저가 한 공간에 존재한다고 할때, 한 명의 유저가 메시지를 전송하면, 그 공간 안에 10명의 유저에게 뿌려줘야 한다. 그렇다면 10명의 유저가 동시에 메시지를 하나씩 전송한다면, 10*10=100번의 패킷을 전송해야한다는 말이다. 시간복잡도로 말하자면 O(n^2)가 된다. 그래서 n을 100, 1000으로 늘리면 늘릴수록 서버에 부담이 갈 수 있다.
코드를 그대로 실행해보면 Broadcast 부분에서 수많은 작업자 스레드가 lock에서 대기하고 있는 모습을 볼 수 있다. 하나의 쓰레드가 foreach문을 다 돌 때까지 다른 쓰레드가 대기를 할 수 밖에 없는 상황인데, 최악인 것은 쓰레드가 처리를 못하고 시간을 끌면 작업자 쓰레드를 새로 만들어버리는 악순환이 발생한다는 것이다.
따라서 모든 로직을 lock을 잡아 실행하는 것이 아니라 GameRoom에 Queue를 하나 만들고, 쓰레드들이 일감을 queue에 넣어두고 대기하지 말고 각자 할 일을 하러 가게끔 만들면 된다. 그런 큐를 JobQueue라고 한다.
코드
PDL.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>
Server
GameRoom.cs
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.SessionId;
packet.chat = $"{chat} 나는 {packet.playerId}";
ArraySegment<byte> segment = packet.Write();
lock(_lock)
{
foreach (ClientSession s in _sessions)
s.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);
}
}
}
SessionManager.cs
class SessionManager
{
static SessionManager _session = new SessionManager();
public static SessionManager Inst { 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.SessionId = sessionId;
_sessions.Add(sessionId, session);
Console.WriteLine($"Connected : {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.SessionId);
}
}
}
ClientSession.cs
class ClientSession : PacketSession
{
public int SessionId { get; set; }
public GameRoom Room { get; set; }
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Program.Room.Enter(this);
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
public override void OnDisconnected(EndPoint endPoint)
{
SessionManager.Inst.Remove(this);
if(Room != null)
{
Room.Leave(this);
Room = null;
}
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
PacketHandler.cs
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);
}
}
DummyClient
Program.cs
class Program
{
static void Main(string[] args)
{
// DNS (Domain Name System)
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.Inst.Generate(); }, 10); // 클라 숫자 10개 접속
while (true)
{
try
{
SessionManager.Inst.SendForEach();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(250);
}
}
}
SessionManager.cs
class SessionManager
{
static SessionManager _session = new SessionManager();
public static SessionManager Inst { get { return _session; } }
List<ServerSession> _sessions = new List<ServerSession>();
object _lock = new object();
public void SendForEach()
{
lock (_lock)
{
foreach (ServerSession session in _sessions)
{
C_Chat chatPacket = new C_Chat();
chatPacket.chat = $"Hello Server!!!";
ArraySegment<byte> segment = chatPacket.Write();
session.Send(segment);
}
}
}
public ServerSession Generate()
{
lock(_lock)
{
ServerSession session = new ServerSession();
_sessions.Add(session);
return session;
}
}
}
PacketHandler.cs
class PacketHandler
{
public static void S_ChatHandler(PacketSession session, IPacket packet)
{
S_Chat chatPacket = packet as S_Chat;
ServerSession serverSession = session as ServerSession;
Console.WriteLine(chatPacket.chat);
}
}
ref : https://velog.io/@fere1032/%EC%B1%84%ED%8C%85%ED%85%8C%EC%8A%A4%ED%8A%B8-1
'서버(Server) > Server' 카테고리의 다른 글
TCP : 3 way handshake(연결), 4 way handshake(종료) (0) | 2023.03.06 |
---|---|
채팅서버 JobQueue 방식으로 부하 줄이기(command 패턴) (0) | 2023.01.06 |
Protobuf 로 패킷 보내기 (0) | 2023.01.05 |
PacketSession (0) | 2022.12.29 |
TCP, UDP 차이 (0) | 2022.12.27 |