TCP 서버 기본 흐름

Jaemyeong Lee·2025년 1월 8일

게임 서버1

목록 보기
122/220

TCP 서버 4단계 개요

핵심 흐름

socket() -> bind() -> listen() -> accept() -> (recv/send)
단계함수목적
1socket()리스너 소켓 생성
2bind()IP/포트에 소켓 매핑
3listen()접속 대기 상태 진입
4accept()실제 클라이언트 연결 소켓 획득

리스너와 연결 소켓의 분리

  • 리스너 소켓은 "접수 창구" 역할만 합니다.
  • accept()가 반환한 연결 소켓이 실제 송수신을 담당합니다.
  • 즉, 동접 N명에서는 "리스너 1개 + 연결 소켓 N개" 구조가 됩니다.

최소 서버 스켈레톤

SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// bind -> listen
SOCKET clientSock = accept(listenSock, ...);
// clientSock으로 recv/send

1단계: socket() - 리스너 생성

코드

SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
    int err = WSAGetLastError();
    return false;
}

인자 의미

인자의미
Address FamilyAF_INETIPv4
TypeSOCK_STREAMTCP 스트림 소켓
ProtocolIPPROTO_TCP(또는 0)TCP 선택

실무 주의점

  • 실패 시 원인은 자원 부족, 초기화 누락, 잘못된 환경 설정일 수 있습니다.
  • 이후 단계가 실패해도 이 소켓은 반드시 closesocket으로 정리해야 합니다.

2단계: bind() - 주소/포트 매핑

코드

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(7777);

if (bind(listenSock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
    int err = WSAGetLastError();
    closesocket(listenSock);
    return false;
}

핵심 포인트

  • INADDR_ANY: 모든 NIC 주소에서 들어오는 연결 허용
  • htons: 포트는 네트워크 바이트 오더(빅엔디언)로 변환 필요
  • 포트는 프로세스 당 독점 사용이 기본(같은 포트 동시 bind 불가)

자주 보는 오류

에러 코드의미대응
WSAEADDRINUSE포트 이미 사용 중포트 변경, 기존 프로세스 종료, 재시작 정책 점검
WSAEACCES권한/보안 정책 문제관리자 권한, 방화벽/보안 정책 확인
WSAEINVAL잘못된 소켓 상태/인자socket 생성 및 주소 구조체 설정 재검토

3단계: listen() - 접속 대기 큐 활성화

코드

if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) {
    int err = WSAGetLastError();
    closesocket(listenSock);
    return false;
}

backlog 이해

  • backlog는 "완전히 처리되지 않은 연결 요청"의 대기량 한도를 의미합니다.
  • SOMAXCONN은 시스템이 허용하는 최대값을 사용하겠다는 의도입니다.
  • backlog가 작으면 급격한 접속 피크에서 연결 거절/지연이 늘 수 있습니다.

오해 금지

  • backlog를 크게 잡는다고 무한 동접이 되는 것은 아닙니다.
  • 실제 한계는 CPU, 메모리, 소켓 처리 루프, I/O 모델이 함께 결정합니다.

4단계: accept() - 클라이언트 수락

코드

sockaddr_in clientAddr{};
int addrLen = sizeof(clientAddr);
SOCKET clientSock = accept(listenSock, reinterpret_cast<sockaddr*>(&clientAddr), &addrLen);
if (clientSock == INVALID_SOCKET) {
    int err = WSAGetLastError();
    return false;
}

반환값 의미

항목의미
clientSock특정 클라이언트와 통신할 새 연결 소켓
clientAddr접속한 클라이언트의 주소/포트 정보

블로킹 특성

  • 기본(블로킹) 소켓에서 accept()는 연결이 올 때까지 대기합니다.
  • 따라서 단일 스레드 구조에서는 accept() 다음 코드가 한동안 실행되지 않을 수 있습니다.
  • 이 특성 때문에 이후 Part에서 non-blocking, 이벤트 모델, IOCP가 필요해집니다.

전체 코드에서 놓치기 쉬운 정리 규칙

실패 지점마다 정리

  • socket 성공 후 bind 실패 -> closesocket(listenSock)
  • listen 실패 -> closesocket(listenSock)
  • accept 실패 처리 시도 후 루프 지속/종료 정책 명확화

종료 순서

  1. 연결 소켓 정리
  2. 리스너 소켓 정리
  3. Winsock cleanup

로그 필수 항목

  • 함수명, 에러코드, 리스너 포트, 클라이언트 주소(가능하면), 재시도 여부

강의 시 유의사항

강조 포인트

  • accept()가 새 소켓을 만든다는 점을 반드시 반복하세요.
  • bind의 포트 충돌(WSAEADDRINUSE)은 실제 개발에서 가장 자주 만나는 문제입니다.
  • "동접 N명 = 연결 소켓 N개" 감각을 수치로 설명하세요.

시연 추천

  • 서버 실행 후 accept() 라인에서 대기하는 모습
  • 더미 클라이언트 접속 순간 accept()가 반환되는 타이밍
  • 같은 포트로 서버 2개 실행 시 bind 실패 재현

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

  • 왜 리스너 소켓으로 직접 게임 데이터를 송수신하지 않는가?
  • bind 실패 시 가장 먼저 확인할 환경 요인은 무엇인가?
  • accept() 블로킹 특성이 서버 구조에 어떤 제약을 만드는가?

profile
李家네_공부방

0개의 댓글