AI로 MSA 서버 만들어 보기 #1 : Core

2025. 12. 3. 18:02·Study/C++ & C#

백엔드란 어렵다.

특히 그게 IOCP라면 더욱더.

네트워크에 대한 지식뿐만 아니라, IOCP에 대해서도 이해가 필요하다.

이전에 ``Boost.Asio``를 통한 서버 공부를 진행했지만, 마음처럼 되지 않았다.

 

하지만 지금이 어떤 시대인가?
대 AI 시대가 열렸고, 엄청난 성능의 모델이 등장했다.

지금이라면 내가 원하는 형태의 코드를 AI가 작성해 줄 수 있고, 실제로 동작하는 그러한 코드들을 보며 공부할 수 있다.

때는 ``GPT-5``가 갓 나왔을 때. "AI로 ``Boost.Asio``를 기반으로 한 Windows와 Linux 양쪽에서 사용할 수 있는 멀티 플랫폼 서버를 만들어 보자"라고 생각했다.

 

시작은 ``GPT-5``였지만, ``GPT-5.1``, ``Claude Sonnet 4.5``, ``Gemini 2.5 Pro``, ``Gemini 3.0 Pro``의 다양한 모델을 사용했다.

대부분의 형태는 ``Codex-CLI``를 통해 ``GPT-5``를 사용하며 잡았고, 나머지 모델은 자잘한 작업에 사용했다.

특히 Antigravity가 출시된 이후로는 ``Gemini 3.0 Pro``를 주로 사용하고, 일부 작업은 ``Sonnet 4.5``로 진행했다.

거의 대부분을 과정을 ``구두지시``로 진행했고, AI가 갈피를 못 잡을 땐 직접 손을 댔다.

 

쓰면서 느끼는 건...

"이야 이거 참 인간 개발자가 많이 필요 없겠는데?"였다.

생성형 AI로 패러다임이 전환된 지 얼마 되지도 않았다.

그런데도 벌써 이런 엄청난 성능이다.

 

``Gemini 3.0``은 그야말로 게임 체인저다.

나중엔 극소수의 천재 개발자가 이끌어가는 형태를 자연스럽게 상상하게 된다.

그래도 그 시대가 오기 전까진 열심히 공부해 보자.


먼저 프로젝트 전체의 아키텍처부터 확인해 보자.

1. 전체 아키텍처

이런 식으로 구성돼 있다.

MSA를 위한 구색만 갖춘 수준이다.

현재는 Dokcer Compose를 통해 리눅스에 올려서 테스트하고 있다.

나중에 k8s로 넘겨볼지도 모르겠다.

 

어떻게든 코어 라이브러리를 전반적으로 사용하게 해 꼬임을 방지하고자 했다.

일종의 간단한 서버엔진을 흉내 내 본 것이기도 하다.

일단 여기선 ``Core``에 대해 얘기해 보자.

 

2. Core

2-1. 아키텍처

Core의 아키텍처를 간략히 표현한 것이다.

서버 코어로써의 최소한의 기능만 하고 있다고 볼 수 있다.

네트워크 / 메모리 / 동시성 관리를 모두 코어의 기능을 통해 수행한다.

서버는 이 코어에 전적으로 의존하고 있다.

 

3. Network

Network에서 모든 비동기 I/O 처리와 통신을 처리한다.

쌩으로 IOCP를 구현한 것이 아닌, Boost.Asio를 통해 구현되었으므로 ``io_context``에 기반한다.

3-1. Hive

``Hive`` 클래스는 ``io_context``의 수명 주기를 관리하는 핵심 클래스이다

``io_context``는 등록된 작업이 없으면 ``run()``이 리턴되어 종료돼 버린다.

따라서 이를 ``executor_work_guard``를 사용해 종료를 방지한다.

// core/src/net/hive.cpp

Hive::Hive(io_context& io)
    : io_(io)
    , guard_(boost::asio::make_work_guard(io_)) { 
    // make_work_guard를 통해 io_context에 "가상의 작업"을 등록하여,
    // 실제 I/O 작업이 없더라도 run()이 리턴하지 않고 대기하도록 합니다.
}

void Hive::run() {
    // 이 함수를 호출한 스레드는 I/O 이벤트 루프의 워커가 됩니다.
    // 여러 스레드에서 동시에 호출하면 Thread-Pool 기반의 Proactor 패턴이 됩니다.
    stopped_.store(false, std::memory_order_relaxed);
    io_.run();
}

void Hive::stop() {
    // 명시적으로 stop()을 호출해야만 guard가 해제되고 io_context가 종료됩니다.
    // 이는 서버의 Graceful Shutdown을 구현하는 기초가 됩니다.
    guard_.reset();
    io_.stop();
}

``Hive``는 FF14의 서버 에뮬레이팅 프로젝트인 Sapphire에 있는 구조를 가져온 것이다.

https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h

 

Sapphire/src/common/Network/Hive.h at master · SapphireServer/Sapphire

A Final Fantasy XIV 4.0+ Server Emulator written in C++ - SapphireServer/Sapphire

github.com

3-2. Session

``Session`` 클래스는 단일 TCP 연결의 생명 주기화 통신 처리를 담당한다.

멀티 스레드 환경에서 단일 소켓에 대한 동시 접근은 레이스 컨디션을 유발하고 데이터 정합성을 위협한다.

이를 방지하기 위해 ``mutex``를 사용하게 되지만, 이 또한 레이스 컨디션을 유발한다.

따라서 ``strand``를 사용해 ``Lock-Free``를 구현한다.

``strand``는 해당 세션 바인딩 된 모든 것들이 순차 실행 됨을 보장하기 때문이다.

따라서 명시적인 Lock 없이도 작업을 처리할 수 있게 된다.

// server/core/net/session.cpp

void Session::async_send(BufferManager::PooledBuffer data, size_t packet_size) {
    // asio::dispatch를 사용하여 작업을 strand로 전달합니다.
    // 현재 스레드가 이미 strand 안에 있다면 즉시 실행되고, 아니면 큐에 넣습니다.
    asio::dispatch(strand_, [self = shared_from_this(), data = std::move(data), packet_size]() mutable {
        if (self->stopped_) return;

        // 여기는 strand에 의해 보호되는 영역이므로, Mutex 없이 안전하게 큐에 접근합니다.
        bool kick_write = self->send_queue_.empty();
        self->send_queue_.push({std::move(data), packet_size});

        if (kick_write) {
            self->do_write();
        }
    });
}

그리고 성능을 위해 메모리 처리에도 신경 써야 하는데, 이때 특히 복사 비용에 대한 고민이 필요하다.

``Session``은 별도 버퍼 관리 클래스와의 연계를 통해 ``Zero-Copy``에 가깝도록 했다.

  1. Read
    • ``BufferManager``에서 할당받은 버퍼를 ``async_read``에 직접 제공하여, 커널에서 유저로의 복사 외의 추가 복사를 방지한다.
  2. Write
    • 직렬화된 데이터가 담긴 ``PooledBuffer``의 소유권을 큐로 이동시켜 복사를 방지한다.
// server/core/net/session.cpp

void Session::do_read_header() {
    // 메모리 풀에서 버퍼를 하나 가져옵니다. (O(1))
    read_buf_ = buffer_manager_.Acquire(); 

    // 소켓이 이 버퍼에 직접 데이터를 쓰도록 합니다.
    asio::async_read(socket_, asio::buffer(read_buf_.get(), server::core::protocol::k_header_bytes),
        asio::bind_executor(strand_, [this, self](const error_code& ec, std::size_t n) {
            if (!ec) {
                // ... 헤더 디코딩 ...
                do_read_body(header_.length);
            }
        }));
}

3-3. Protocol & Packets

서버와 클라이언트 간의 통신은 명확하게 정의된 패킷(Packet) 단위로 이루어진다.
TCP는 스트림 기반 프로토콜이므로 데이터의 경계가 없지만, 애플리케이션 레벨에서는 메시지 경계가 필요하다.
이를 위해 Length-Prefixed 방식을 변형한 헤더 구조를 사용한다.

1) Packet Structure

모든 패킷은 14바이트의 고정 길이 헤더와 가변 길이 바디로 구성된다.
네트워크 전송 시에는 표준인 Big-Endian 엔디안을 따른다.

Offset Field Type Description
0 Length uint16 바디 데이터의 길이 (헤더 제외)
2 MsgID uint16 메시지 식별자 (Opcode)
4 Flags uint16 압축, 암호화 여부 등의 플래그
6 Sequence uint32 패킷 순서 보장 및 중복 방지
10 Timestamp uint32 전송 시간 (Latency 측정용)

이러한 구조는 다음과 같은 이점을 제공한다.

  1. 명확한 경계: Length 필드를 통해 다음 패킷의 시작 위치를 정확히 알 수 있다.
  2. 보안성: 바디를 읽기 전에 헤더를 먼저 검증하여, 비정상적으로 큰 패킷이나 잘못된 프로토콜을 사전에 차단할 수 있다.
  3. 관측성: Sequence와 Timestamp를 통해 패킷 유실이나 지연 시간을 추적할 수 있다.

2) Packet Processing Flow

패킷 처리는 Double-Read 전략을 취한다.
즉, 헤더를 먼저 읽고(14바이트), 그 결과에 따라 바디를 읽는다.

// core/src/net/session.cpp

void Session::do_read_header() {
    // 1. 14바이트 고정 길이 헤더 읽기 요청
    asio::async_read(socket_, asio::buffer(read_buf_.get(), 14),
        [this](const error_code& ec, size_t n) {
            if (!ec) {
                // 2. 헤더 디코딩
                server::core::protocol::decode_header(read_buf_.get(), header_);

                // 3. 유효성 검사 (너무 큰 패킷 차단)
                if (header_.length > MAX_PAYLOAD) {
                    stop(); 
                    return;
                }

                // 4. 바디 읽기 시작
                do_read_body(header_.length);
            }
        });
}

void Session::do_read_body(size_t body_len) {
    if (body_len == 0) {
        // 바디가 없는 패킷(예: Ping)도 처리 가능
        dispatch(header_.msg_id, {});
        do_read_header();
        return;
    }

    // 5. 바디 길이만큼 읽기 요청
    asio::async_read(socket_, asio::buffer(read_buf_.get(), body_len),
        [this](const error_code& ec, size_t n) {
            if (!ec) {
                // 6. 핸들러로 전달
                dispatch(header_.msg_id, read_buf_);

                // 7. 다음 패킷 대기
                do_read_header();
            }
        });
}

3) Dispatcher

수신된 패킷은 Dispatcher를 통해 적절한 핸들러로 전달된다.
std::unordered_map을 사용하여 O(1) 속도로 핸들러를 찾으며, 예외가 발생하더라도 해당 세션만 종료되도록 격리하여 서버 전체의 안정성을 보장한다.

// core/src/net/dispatcher.cpp

bool Dispatcher::dispatch(uint16_t msg_id, Session& s, span<const uint8_t> payload) {
    if (auto it = table_.find(msg_id); it != table_.end()) {
        try {
            it->second(s, payload); // 핸들러 실행
            return true;
        } catch (...) {
            // 핸들러 오류가 서버 전체를 죽이지 않도록 방어
        }
    }
    return false;
}

3-4. Flow Control (Backpressure)

비동기 서버에서 가장 흔히 발생하는 문제는 Producer-Consumer 속도 불균형이다.
클라이언트의 인터넷 상태가 좋지 않아 데이터를 빨리 받지 못하는데, 서버가 계속해서 데이터를 보낸다면 어떻게 될까?
Session의 송신 큐(Send Queue)에 데이터가 무한히 쌓이다가 결국 OOM(Out Of Memory)으로 서버가 죽게 된다.

이를 방지하기 위해 송신 큐에 한계점(High Watermark)을 설정하고, 이를 초과하면 가차 없이 세션을 끊어버리는 Backpressure 메커니즘을 적용했다.

// core/src/net/session.cpp

void Session::async_send(...) {
    // ...
    // 현재 큐에 쌓인 데이터 크기가 설정된 한계(예: 10MB)를 초과하면
    if (queued_bytes_ + packet_size > options_->send_queue_max) {
        log::warn("Send queue limit exceeded; stopping session");
        stop(); // 세션 강제 종료 (보호 차원)
        return;
    }
    // ...
}

3-5. Connection Setup

연결이 맺어지자마자 바로 비즈니스 로직을 수행하지는 않는다.
가장 먼저 MSG_HELLO 패킷을 교환하여 서로의 버전을 확인하고 기능을 협상한다.

  1. Handshake: 서버는 연결 직후 MSG_HELLO를 보내 프로토콜 버전, 지원 기능(압축 등), Heartbeat 주기를 알린다.
  2. Heartbeat: 아무런 통신이 없으면 연결이 죽었는지 알 수 없으므로, 주기적으로 MSG_PING을 보내 연결 생존을 확인한다.
// server/core/net/session.cpp

void Session::start() {
    send_hello();       // 1. 헬로 패킷 전송
    do_read_header();   // 2. 수신 대기 시작
    arm_read_timeout(); // 3. 읽기 타임아웃 타이머 가동 (Zombie Connection 방지)
    arm_heartbeat();    // 4. 하트비트 타이머 가동
}

3-6. Security & Optimization

서버 코어는 강력한 보안과 최적화 기능을 기본적으로 내장하고 있다.

  1. Cipher (AES-256-GCM): OpenSSL 기반의 Cipher 클래스를 제공하여, 데이터의 기밀성과 무결성을 동시에 보장한다. 단순 암호화뿐만 아니라 인증(Authentication)까지 수행하므로 패킷 변조를 원천 차단한다.
// core/src/security/cipher.cpp

std::vector<uint8_t> Cipher::encrypt(std::span<const uint8_t> plaintext, ...) {
    // OpenSSL EVP Interface 사용
    ScopedCtx ctx(EVP_CIPHER_CTX_new());

    // AES-256-GCM 초기화
    EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data());

    // 암호화 수행
    EVP_EncryptUpdate(ctx.get(), ciphertext.data(), &out_len, plaintext.data(), ...);
    EVP_EncryptFinal_ex(ctx.get(), ciphertext.data() + out_len, &final_len);

    // Authentication Tag 추출 (변조 방지 핵심)
    EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, TAG_SIZE, ...);

    return ciphertext;
}
  1. Compressor (LZ4): LZ4 기반의 Compressor 클래스를 제공한다. 압축 효율이 매우 높으며, 이를 통해 대역폭 비용을 획기적으로 절감할 수 있다.
// core/src/compression/compressor.cpp

std::vector<uint8_t> Compressor::compress(std::span<const uint8_t> data) {
    // 압축 후 최대 크기 계산
    int max_dst_size = LZ4_compressBound(static_cast<int>(data.size()));
    std::vector<uint8_t> compressed(max_dst_size);

    // LZ4 기본 압축 수행
    int compressed_size = LZ4_compress_default(
        reinterpret_cast<const char*>(data.data()),
        reinterpret_cast<char*>(compressed.data()),
        static_cast<int>(data.size()),
        max_dst_size
    );

    compressed.resize(compressed_size);
    return compressed;
}

 

4. Concurrency

``Concurrency`` 클래스는 스레드 관리와 스케줄링을 담당한다.

단순하게 말하자면 병렬 처리를 가능케 하는 클래스이다.

4-1. ThreadManager

``ThreadManager``는 워커 스레드의 생명주기를 관리한다.

실제 로직에서 직접 ``std::thread``를 다루는 일이 없어야 하고, 스레드가 모든 작업을 제대로 처리할 수 있게 관리해 데이터 유실도 방지한다.

// server/core/concurrent/thread_manager.cpp

void ThreadManager::Start(int num_threads) {
    stopped_.store(false, std::memory_order_relaxed);
    threads_.reserve(num_threads);
    for (int i = 0; i < num_threads; ++i) {
        threads_.emplace_back([this] { WorkerLoop(); });
    }
}

void ThreadManager::Stop() {
    bool expected = false;
    if (!stopped_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
        return;
    }

    job_queue_.Stop(); // Pop()에서 대기 중인 워커들을 모두 깨워 종료 신호를 전달합니다.

    for (auto& t : threads_) {
        if (t.joinable()) {
            t.join();
        }
    }
    threads_.clear();
}

// 각 워커 스레드는 JobQueue::Pop()이 nullptr를 반환할 때까지 반복 실행됩니다.
void ThreadManager::WorkerLoop() {
    while (!stopped_.load(std::memory_order_acquire)) {
        Job job = job_queue_.Pop();
        if (!job) { // nullptr 작업이 오면 종료합니다.
            break;
        }
        job();
    }
}

4-2. JobQueue

``JobQueue``는 순차적 실행을 보장하는 FIFO 큐이다.

이는 ``std::mutex``와 ``std::condition_variable``을 사용하는 전형적 Producer - Consumer 패턴이다.

특정 단위마다 ``JobQueue``를 사용해 순서를 보장하고 Lock 관리에서 어느 정도 해방시켜 준다.

// server/core/concurrent/job_queue.cpp

void JobQueue::Push(Job job) {
    {
        std::lock_guard<std::mutex> lock(mutex_);
        jobs_.push(std::move(job));
        // 메트릭 기록: 현재 큐 깊이
        runtime_metrics::record_job_queue_depth(jobs_.size());
    }
    // 대기 중인 워커 스레드 하나를 깨웁니다.
    cv_.notify_one();
}

Job JobQueue::Pop() {
    std::unique_lock<std::mutex> lock(mutex_);
    // 큐에 작업이 들어오거나 종료 신호가 올 때까지 대기합니다.
    cv_.wait(lock, [this] { return !jobs_.empty() || stopping_; });

    if (stopping_ && jobs_.empty()) {
        runtime_metrics::record_job_queue_depth(jobs_.size());
        return nullptr; // nullptr이면 종료 신호로 간주합니다.
    }

    Job job = std::move(jobs_.front());
    jobs_.pop();
    runtime_metrics::record_job_queue_depth(jobs_.size());
    return job;
}

4-3. TaskScheduler

``TaskScheduler`` 지연이나 주기적 실행이 필요한 작업들을 관리한다.

수만 또는 수십만 이상의 타이머가 필요한 서버에서 각 작업마다 ``asio::steady_timer``를 생성해 사용하는 것은 엄청나게 큰 오버헤드가 될 것임이 자명하다.

따라서 단일 ``std::priority_queue``를 사용해 모든 작업들을 관리한다.

// server/core/concurrent/task_scheduler.cpp

void TaskScheduler::schedule(Task task, Clock::duration delay) {
    std::lock_guard<std::mutex> lock(mutex_);
    // 만료 시간(due)을 기준으로 정렬되는 우선순위 큐에 삽입합니다.
    delayed_.push(DelayedTask{Clock::now() + delay, std::move(task)});
}

std::size_t TaskScheduler::poll(std::size_t max_tasks) {
    // ...
    auto now = Clock::now();
    // 현재 시간보다 이전에 만료된 작업들을 모두 꺼내서 실행 대기열(ready_)로 옮깁니다.
    while (!delayed_.empty() && delayed_.top().due <= now) {
        ready_.push(std::move(delayed_.top().task));
        delayed_.pop();
    }
    // ...
}

5. Memory & Utils

5-1. MemoryPool

``MemoryPool``은 고정 크기 메모리 블록의 할당 및 해제를 담당한다.

빈번한 동적 할당은 메모리 파편화를 유발할 가능성이 크다.

``MemoryPool``을 통해 사전에 큰 청크를 할당하고, 그 청크를 쪼개 관리하여 파편화를 방지한다.

그리고 가용 메모리 블록의 포인터들을 스택으로 관리해 O(1)의 접근속도를 보장한다.

// core/src/memory/memory_pool.cpp

class MemoryPool {
    std::vector<std::byte> memoryChunk_; // 실제 메모리가 할당된 공간
    std::stack<void*> freeList_;         // 사용 가능한 블록들의 포인터 스택

    void* Acquire() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (freeList_.empty()) return nullptr; // 풀 고갈
        void* ptr = freeList_.top();
        freeList_.pop();
        return ptr;
    }

    void Release(void* ptr) {
        std::lock_guard<std::mutex> lock(mutex_);
        freeList_.push(ptr);
    }
};

5-2. Dispatcher

``Dispatcher``는 수신된 패킷의 Opcode에 따라 그에 맞는 핸들러 함수를 호출한다.

``std::unordered_map``을 사용하여 O(1)의 Opcode 조회 성능을 보장한다.

``std::span``을 사용해 버퍼 길이를 초과하는 접근을 막는다.

// core/src/net/dispatcher.cpp

class Dispatcher {
    using handler_t = std::function<void(Session&, std::span<const std::uint8_t>)>;
    std::unordered_map<std::uint16_t, handler_t> table_;

    bool dispatch(std::uint16_t msg_id, Session& s, std::span<const std::uint8_t> payload) const {
        if (auto it = table_.find(msg_id); it != table_.end()) {
            it->second(s, payload); // 핸들러 호출
            return true;
        }
        return false; // 알 수 없는 메시지 ID
    }
};

6. Observability

서비스 환경에서의 디버깅과 모니터링을 위한 유틸리티를 제공한다.

6-1. Logging

``server::core::log``를 통해 Thread-Safe 로깅을 제공한다.

싱글톤 패턴을 통해 전체에서 단 하나의 로깅 인스턴스만 사용하도록 한다.

// core/src/util/log.cpp

class AsyncLogger {
public:
    // 로그 메시지를 큐에 넣고 워커 스레드를 깨웁니다.
    // 이 함수는 메인 로직 스레드에서 호출되므로 최대한 빨리 리턴해야 합니다.
    void push(const std::string& msg) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            queue_.push(msg);
        }
        cv_.notify_one();
    }

private:
    // 생성자는 private으로 선언하여 싱글톤 패턴을 강제합니다.
    AsyncLogger() : stop_(false) {
        // 백그라운드 워커 스레드를 시작합니다.
        worker_ = std::thread([this] { worker_loop(); });
    }

    // 소멸자에서 워커 스레드가 안전하게 종료되도록 대기합니다.
    ~AsyncLogger() {
        // ...
    }

    // 워커 스레드에서 실행될 메인 루프입니다.
    void worker_loop() {
        while (true) {
            std::string msg;
            // ...
        }
    }

    std::mutex mutex_; // 큐 접근을 위한 뮤텍스
    std::condition_variable cv_; // 큐 상태 변경을 알리는 조건 변수
    std::queue<std::string> queue_; // 로그 메시지를 저장하는 큐
    std::thread worker_; // 로그 처리를 담당하는 백그라운드 스레드
    bool stop_; // 스레드 종료를 위한 플래그

    // AsyncLogger 인스턴스를 얻기 위한 friend 함수 선언
    friend AsyncLogger& get_logger();
};

6-2. Metrics

Prometheus에 제공하기 위한 메트릭 수집 인터페이스를 제공한다.

``Counter``, ``Gauge``, ``Histogram`` 인터페이스를 제공한다.

// core/include/server/core/metrics/metrics.cpp

struct NoopCounter final : Counter { void inc(double, Labels) override {} };
struct NoopGauge final : Gauge { void set(double, Labels) override {} void inc(double, Labels) override {} void dec(double, Labels) override {} };
struct NoopHistogram final : Histogram { void observe(double, Labels) override {} };

std::mutex& mu() { static std::mutex m; return m; }
std::unordered_map<std::string, NoopCounter>& counters() { static std::unordered_map<std::string, NoopCounter> m; return m; }
std::unordered_map<std::string, NoopGauge>& gauges() { static std::unordered_map<std::string, NoopGauge> m; return m; }
std::unordered_map<std::string, NoopHistogram>& histos() { static std::unordered_map<std::string, NoopHistogram> m; return m; }
}

// Prometheus exporter가 붙지 않은 초기 단계에서도 공통 metrics API를 호출할 수 있도록
// Noop 객체를 미리 준비해두고, 실제 exporter가 연결되면 즉시 교체되는 구조다.
// 이를 통해 비즈니스 로직은 메트릭 시스템의 유무와 상관없이 항상 동일하게 동작한다.
Counter& get_counter(const std::string& name) {
    std::lock_guard<std::mutex> lk(mu());
    return counters().try_emplace(name).first->second;
}

// ...


// 실제 로직에서 사용할 때
auto& req_count = server::core::metrics::get_counter("requests_total");
req_count.inc(1.0, {{"method", "login"}});

7. 결론

``Boost.Asio``는 정말 엄청난 도구다.

이전에 쌩으로 IOCP 서버를 개발하는 공부를 해 본 입장에서, 개발에 엄청난 편의성을 제공한다.

사실상 패킷 처리, 메모리 관리, 스케줄링, 로깅, 텔레메트리에만 신경을 쓰면 되는 수준이다.

아니 이 정도면 많은 부분인가?

 

로우레벨에 대해 신경 쓸 부분이 줄었다는 것은 실수를 많이 줄일 수 있다는 뜻이기도 하니 여하튼 좋다.

다음엔 서버에 대한 내용이다.

'Study > C++ & C#' 카테고리의 다른 글

AI로 MSA 서버 만들어 보기 #3 : Gateway  (0) 2025.12.09
AI로 MSA 서버 만들어 보기 #2 : Server  (0) 2025.12.07
[C++] 채팅서버에 DB 실장  (0) 2024.05.14
[C#] ###Clicker 동작 개선  (0) 2024.05.05
[C++] ChatRoom 구현  (0) 2024.04.09
'Study/C++ & C#' 카테고리의 다른 글
  • AI로 MSA 서버 만들어 보기 #3 : Gateway
  • AI로 MSA 서버 만들어 보기 #2 : Server
  • [C++] 채팅서버에 DB 실장
  • [C#] ###Clicker 동작 개선
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (173)
      • Thoughts (14)
      • Study (75)
        • Japanese (3)
        • C++ & C# (50)
        • Javascript (3)
        • Python (14)
        • Others (5)
      • Play (1)
        • Battlefield (1)
      • Others (10)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    Network
    7계층
    로깅
    Dalamud
    프로그래머스
    OSI
    boost
    Python
    Asio
    Selenium
    클라우드
    db
    Server
    네트워크
    C++
    FF14
    bot
    서버
    discord py
    c#
    암호화
    포인터
    docker
    discord
    cloudtype
    스타필드
    네트워크 프로그래밍
    IOCP
    JS
    베데스다
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
AI로 MSA 서버 만들어 보기 #1 : Core
상단으로

티스토리툴바