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);
}
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);
_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는 배치 전체에서 "실제로 전송된 총 바이트"입니다.
- 즉, 마지막 버퍼가 부분 전송될 수 있습니다.
처리 규칙
numOfBytes만큼 배치 버퍼를 앞에서부터 소모
- 완전히 전송된 버퍼는 owner 참조 해제
- 부분 전송된 버퍼는 오프셋을 조정해 큐 앞에 재삽입
- 배치 정리 후
sendRegistered = false
- 큐에 남은 데이터가 있으면 다시 등록
흔한 실수
- 부분 전송을 무시하고 배치를 통째로 성공 처리하면 데이터 유실이 발생합니다.
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를 호출하면 왜 위험한가?