SendBuffer가 필요한 이유

복사 비용 문제

  • 같은 패킷을 여러 세션에 보낼 때 세션마다 바이트 복사를 하면 비용이 큽니다.
  • 복사 비용은 대략 대상 세션 수 * 패킷 크기에 비례해 증가합니다.

해결 방향

SendBuffer = 불변 payload + 참조 카운트(shared ownership)

  • 한 번 만든 버퍼를 여러 세션 큐에서 공유합니다.
  • 마지막 참조가 사라질 때 자동으로 메모리가 해제됩니다.

브로드캐스트 효과

  • 브로드캐스트 시 "N개 세션마다 payload 복사"를 줄이고, 포인터 공유 중심으로 전송 파이프라인을 구성할 수 있습니다.

세션별 SendQueue와 단일 in-flight 정책

기본 구조

구성역할
sendQueue전송 대기 SendBufferRef 저장
sendRegistered (atomic bool)현재 WSASend in-flight 여부
sendEvent현재 전송 배치 컨텍스트(WSABUF 목록/owner refs)

왜 단일 in-flight가 흔한가

  • 세션당 동시에 여러 WSASend를 던지면 순서/경합 관리가 급격히 복잡해집니다.
  • "세션당 1개 WSASend in-flight" 정책은 순서 보장과 디버깅에 유리합니다.

Send 진입 패턴

void Session::Send(const SendBufferRef& sb)
{
    {
        WRITE_LOCK;
        _sendQueue.push(sb);
    }

    // 최초 1명만 등록 담당
    if (!_sendRegistered.exchange(true))
        RegisterSend();
}

RegisterSend와 Scatter/Gather

배치 구성

  • 큐에서 일부(또는 전부)를 꺼내 WSABUF 배열을 만듭니다.
  • 너무 많은 버퍼를 한 번에 보내지 않도록 배치 상한을 두는 것이 안전합니다.

예시 코드

bool Session::RegisterSend()
{
    std::vector<SendBufferRef> batch;
    {
        WRITE_LOCK;
        while (!_sendQueue.empty() && batch.size() < kMaxSendBatch)
        {
            batch.push_back(_sendQueue.front());
            _sendQueue.pop();
        }
    }

    _sendEvent.sendBuffers = std::move(batch); // 완료까지 owner 유지
    _sendEvent.wsaBufs.clear();
    for (auto& sb : _sendEvent.sendBuffers)
    {
        WSABUF wb;
        wb.buf = reinterpret_cast<char*>(sb->Buffer());
        wb.len = static_cast<ULONG>(sb->WriteSize());
        _sendEvent.wsaBufs.push_back(wb);
    }

    DWORD sent = 0;
    int32 ret = WSASend(_socket, _sendEvent.wsaBufs.data(),
                        static_cast<DWORD>(_sendEvent.wsaBufs.size()),
                        &sent, 0, &_sendEvent.overlapped, nullptr);

    if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
        return false;

    return true;
}

반환값 해석

결과의미대응
ret == 0즉시 완료 가능완료 처리 경로로 연결
WSA_IO_PENDING정상 비동기 진행완료 통지 대기
그 외 오류등록 실패플래그/배치 정리 후 종료/복구

ProcessSend: 부분 전송까지 정확히 처리

꼭 알아야 할 점

  • 완료 콜백의 numOfBytes는 배치 전체에서 "실제로 전송된 총 바이트"입니다.
  • 즉, 마지막 버퍼가 부분 전송될 수 있습니다.

처리 규칙

  1. numOfBytes만큼 배치 버퍼를 앞에서부터 소모
  2. 완전히 전송된 버퍼는 owner 참조 해제
  3. 부분 전송된 버퍼는 오프셋을 조정해 큐 앞에 재삽입
  4. 배치 정리 후 sendRegistered = false
  5. 큐에 남은 데이터가 있으면 다시 등록

흔한 실수

  • 부분 전송을 무시하고 배치를 통째로 성공 처리하면 데이터 유실이 발생합니다.

ProcessSend 후 drain 루프

안전한 재등록 패턴

void Session::OnSendComplete()
{
    // 배치 정리...
    _sendRegistered.store(false);

    bool needMore = false;
    {
        WRITE_LOCK;
        needMore = !_sendQueue.empty();
    }

    if (needMore && !_sendRegistered.exchange(true))
        RegisterSend();
}

왜 이렇게 하나

  • 락 안에서 RegisterSend()를 직접 부르면 재진입/중첩 락 위험이 커집니다.
  • "락 해제 후 등록 시도" 구조가 데드락 가능성을 줄입니다.

종료 경로 주의

  • Disconnecting 상태에서는 신규 등록을 막고 큐 정리 정책(드롭/flush)을 명확히 정해야 합니다.

락/원자 변수 설계 주의

항목권장
큐 보호mutex(또는 락 샤딩)로 짧게 보호
등록 단일화atomic<bool> sendRegistered
호출 순서큐 조작(락) -> 락 해제 -> WSASend 등록

정정 포인트

  • 표준 mutex의 같은 스레드 재잠금은 "크래시"보다 데드락이 일반적입니다.

팀 규칙 제안

  • "네트워크 API 호출은 가능한 락 밖에서"를 컨벤션으로 고정하세요.

운영 지표와 디버깅 포인트

필수 지표

  • 세션별 sendQueue 길이
  • 평균/최대 배치 크기(WSABUF 개수)
  • 부분 전송 발생률
  • WSASend 실패 코드 분포

장애 패턴

패턴증상원인
sendRegistered stuck true송신 영구 정지완료 후 플래그 해제 누락
부분 전송 무시패킷 일부 유실numOfBytes 소모 로직 누락
락 안에서 재등록간헐 데드락재진입 호출 구조

강의 시 유의사항

강조 포인트

  • ReceiveBuffer가 "수신 경계 보장"이라면 SendBuffer는 "송신 복사 비용 절감 + 파이프라인 안정화"입니다.
  • Part 8과 연결해, Send도 Register/Process 루프라는 점을 반복하세요.
  • 성능 최적화보다 먼저 데이터 유실 없는 부분 전송 처리부터 완성해야 합니다.

자주 하는 오해

오해바로잡기
WSASend 완료면 배치 전체가 전송됐다부분 전송 가능, numOfBytes로 정확히 소모해야 함
sendQueue만 있으면 동시 Send 문제 해결중복 등록 방지 플래그가 함께 필요
브로드캐스트는 어차피 N번 복사해야 한다SendBuffer 공유로 복사를 크게 줄일 수 있다

체크 질문 (스스로 답해보기)

  • sendRegistered를 해제하지 않으면 어떤 장애가 발생하는가?
  • 부분 전송이 발생했을 때 큐/버퍼를 어떤 규칙으로 복원해야 하는가?
  • 락 보유 상태에서 RegisterSend를 호출하면 왜 위험한가?

profile
李家네_공부방

0개의 댓글