전체 코드

1️⃣ 기존 패킷 송신 방식과 문제점

⚠ 기존 구조: 개별 패킷 송신

현재 서버의 GameRoom에서는 유저가 채팅을 입력할 때마다 즉시 Broadcast()를 호출하여 개별 패킷을 클라이언트들에게 전송하는 방식으로 동작하고 있었다.

public void Broadcast(ClientSession session, string chat)
{
    S_Chat packet = new S_Chat();
    packet.playerId = session.SessionId;
    packet.chat = $"{chat} I am {packet.playerId}";
    ArraySegment<byte> segment = packet.Write();

    foreach (ClientSession s in _session)
        s.Send(segment);
}

즉시 전송 방식

  • 클라이언트가 패킷을 보낼 때마다 foreach를 통해 모든 클라이언트에게 패킷을 즉시 전송
  • 서버 부하 증가
  • 네트워크 트래픽 비효율적

⚠ 문제점

N² 복잡도 문제

  • Broadcast()가 클라이언트 수만큼 반복 실행되므로 O(N²)의 성능 저하 발생

네트워크 오버헤드 증가

  • 매번 개별 패킷을 전송하므로 패킷 단위 오버헤드(헤더, 핸드셰이크 등) 발생

CPU & 메모리 낭비

  • 멀티스레드 환경에서 Send() 실행이 많아질수록 성능 저하

2️⃣ 해결책: 패킷 모아 보내기 (Batch Packet Sending)

✅ 핵심 아이디어

  • 개별 패킷을 즉시 보내는 것이 아니라 _pendingList에 저장
  • 0.25초마다 Flush()를 호출하여 모아둔 패킷을 한 번에 전송
  • 네트워크 부하 감소 및 성능 최적화

3️⃣ GameRoom을 이용한 패킷 모아 보내기 구현

GameRoom 개선 (패킷 저장 & Flush 기능 추가)

using System;
using System.Collections.Generic;
using System.Text;
using ServerCore;

namespace Server
{
    class GameRoom
    {
        List<ClientSession> _session = new List<ClientSession>();
        JobQueue _jobQueue = new JobQueue();
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();

        public void Push(Action job)
        {
            _jobQueue.Push(job);
        }

        // 일정 시간마다 모아둔 패킷을 전송하는 Flush 함수
        public void Flush()
        {
            foreach (ClientSession s in _session)
            {
                s.Send(_pendingList);
            }
            
            Console.WriteLine($"Flushed {_pendingList.Count} packets");
            _pendingList.Clear();
        }

        // 패킷을 즉시 전송하지 않고 대기열(_pendingList)에 저장
        public void Broadcast(ClientSession session, string chat)
        {
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} I am {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            _pendingList.Add(segment);
        }

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

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

변경 사항
1. _pendingList(List<ArraySegment>): 패킷을 저장하는 리스트 추가
2. Broadcast():

  • 즉시 전송(X)
  • _pendingList에 추가하여 모아두었다가 나중에 전송(O)
  1. Flush():
    • 0.25초마다 _pendingList에 저장된 패킷을 클라이언트들에게 전송
    • _pendingList.Clear(); 를 통해 리스트 초기화

4️⃣ ServerProgram.cs에서 Flush() 실행 주기적으로 호출

using System.Net;
using System.Threading;
using ServerCore;

namespace Server
{
    class ServerProgram
    {
        static Listener _listener = new Listener();
        public static GameRoom Room = new GameRoom();

        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 1111);

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

            while (true)
            {
                // 일정 주기마다 모아둔 패킷을 전송 (0.25초마다)
                Room.Push(() => Room.Flush());
                Thread.Sleep(250);
            }
        }
    }
}

주요 변경 사항

  • 메인 스레드에서 Flush() 실행
    • 0.25초(250ms)마다 _pendingList에 저장된 패킷들을 모아 전송
    • Thread.Sleep(250); 를 사용하여 주기적으로 실행

5️⃣ 클라이언트 패킷 수신 최적화

서버에서 패킷을 모아서 보내는 구조로 변경되었으므로, 클라이언트에서 수신 버퍼 크기 증가 필요
→ 그렇지 않으면 일부 패킷이 손실될 가능성이 있음

RecvBuffer 버퍼 크기 증가

public class RecvBuffer
{
    byte[] _buffer;
    int _readPos;
    int _writePos;

    public RecvBuffer(int size)
    {
        _buffer = new byte[65535]; // 최대 크기로 설정
    }
}

SendBuffer 버퍼 크기 증가

public class SendBufferHelper
{
    public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

    // 기존보다 큰 버퍼로 확장
    public static int ChunkSize { get; set; } = 65535 * 100;
}

수정 이유

  • RecvBufferSendBuffer 크기를 늘려 대량의 패킷 수신 가능
  • 클라이언트가 100개의 패킷을 동시에 받을 수 있도록 설정

6️⃣ 클라이언트 패킷 수신 개수 로그 출력

public sealed override int OnRecv(ArraySegment<byte> buffer)
{
    int processLen = 0;
    int packetCount = 0;

    while (true)
    {
        if (buffer.Count < HeaderSize)
            break;

        ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
        if (buffer.Count < dataSize)
            break;

        OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
        packetCount++;

        processLen += dataSize;
        buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
    }

    if (packetCount > 1)
        Console.WriteLine($"패킷 모아보내기: {packetCount}");

    return processLen;
}

패킷 모아 보내기 정상 동작 확인

  • packetCount 값을 통해 한 번에 처리된 패킷 수를 로그 출력

profile
李家네_공부방

0개의 댓글