TCP 서버 4단계 개요
핵심 흐름
socket() -> bind() -> listen() -> accept() -> (recv/send)
| 단계 | 함수 | 목적 |
|---|
| 1 | socket() | 리스너 소켓 생성 |
| 2 | bind() | IP/포트에 소켓 매핑 |
| 3 | listen() | 접속 대기 상태 진입 |
| 4 | accept() | 실제 클라이언트 연결 소켓 획득 |
리스너와 연결 소켓의 분리
- 리스너 소켓은 "접수 창구" 역할만 합니다.
accept()가 반환한 연결 소켓이 실제 송수신을 담당합니다.
- 즉, 동접 N명에서는 "리스너 1개 + 연결 소켓 N개" 구조가 됩니다.
최소 서버 스켈레톤
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKET clientSock = accept(listenSock, ...);
1단계: socket() - 리스너 생성
코드
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
int err = WSAGetLastError();
return false;
}
인자 의미
| 인자 | 값 | 의미 |
|---|
| Address Family | AF_INET | IPv4 |
| Type | SOCK_STREAM | TCP 스트림 소켓 |
| Protocol | IPPROTO_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 실패 처리 시도 후 루프 지속/종료 정책 명확화
종료 순서
- 연결 소켓 정리
- 리스너 소켓 정리
- Winsock cleanup
로그 필수 항목
- 함수명, 에러코드, 리스너 포트, 클라이언트 주소(가능하면), 재시도 여부
강의 시 유의사항
강조 포인트
accept()가 새 소켓을 만든다는 점을 반드시 반복하세요.
bind의 포트 충돌(WSAEADDRINUSE)은 실제 개발에서 가장 자주 만나는 문제입니다.
- "동접 N명 = 연결 소켓 N개" 감각을 수치로 설명하세요.
시연 추천
- 서버 실행 후
accept() 라인에서 대기하는 모습
- 더미 클라이언트 접속 순간
accept()가 반환되는 타이밍
- 같은 포트로 서버 2개 실행 시
bind 실패 재현
체크 질문 (스스로 답해보기)
- 왜 리스너 소켓으로 직접 게임 데이터를 송수신하지 않는가?
bind 실패 시 가장 먼저 확인할 환경 요인은 무엇인가?
accept() 블로킹 특성이 서버 구조에 어떤 제약을 만드는가?