개발 서적 리뷰/게임서버 프로그래머 책

[게임서버프로그래밍#6] 클라이언트-서버 구현을 위한 소켓 기초 정리

거북이의 기술블로그 2025. 4. 29. 00:56

이 글에서는 소켓 통신의 기초부터, Blocking/Non-Blocking 구조, Winsock2 함수(ioctlsocket, getsockopt, select) 사용법, 클라이언트-서버 프로그램 기본 흐름까지 실제 코드 예제와 함께 설명한다.

 

 

 


 

소켓(Socket)이란 무엇인가

  • 소켓은 네트워크 연결을 제어하는 핸들이다.
  • 파일 핸들처럼 읽고 쓰기를 담당하지만 대상이 네트워크다.

 


 

 

 

Blocking 통신 구조

Blocking 통신은 소켓 함수 호출 시 완료될 때까지 대기하는 구조를 의미한다.

* 클라이언트 Blocking 흐름

  1. socket() : 소켓 핸들 생성
  2. bind() : 클라이언트 포트 설정
  3. connect() : 서버 연결 (연결 완료될 때까지 대기)
  4. send() : 송신 버퍼에 데이터 저장 (버퍼가 꽉 차면 Blocking)
  5. close() : 소켓 닫기

* 서버 Blocking 흐름

  1. socket() : 소켓 핸들 생성
  2. bind() : 서버 포트 설정
  3. listen() : 연결 요청 대기
  4. accept() : 클라이언트 연결 수락 (연결 전까지 Blocking)
  5. recv() : 데이터 수신 (버퍼에 데이터 올 때까지 대기)
  6. close() : 소켓 닫기

 

 


 

 

비동기(Non-Blocking) 통신 구조

Non-Blocking 통신은 소켓 함수 호출이 즉시 반환되어 다른 작업을 할 수 있도록 한다.

  • ioctlsocket()을 사용하여 소켓을 비동기 모드로 설정한다.
  • select()를 통해 소켓의 I/O 가능 여부를 확인한다.

* Non-Blocking 클라이언트 흐름

  1. socket() : 소켓 핸들 생성
  2. ioctlsocket(sock, FIONBIO, &mode) : 비동기 모드 설정
  3. connect() 호출 → Would Block 오류 무시
  4. select()로 연결 완료 여부 확인
  5. send()/recv() 전에 select()로 I/O 가능 상태를 확인

* Non-Blocking 서버 흐름

  1. socket() : 소켓 핸들 생성
  2. ioctlsocket(sock, FIONBIO, &mode) : 비동기 모드 설정
  3. listen()으로 연결 요청 대기
  4. select()를 통해 accept() 가능 여부 탐지
  5. 연결된 소켓에 대해 recv(), send() 진행

 

 

 


 

주요 함수 정리

 

1. ioctlsocket()

소켓 핸들에 명령을 내려 속성을 설정한다.

  • FIONBIO : 동기/비동기 설정
  • FIONREAD : 읽을 수 있는 데이터 양 확인
  • SIO_KEEPALIVE_VALS : TCP Keep-Alive 시간 변경
  • SIO_GET_EXTENSION_FUNCTION_POINTER : 확장 함수 포인터 획득

2. getsockopt()

소켓의 현재 상태나 설정값을 조회할 수 있다.

  • SO_REUSEADDR : 주소 재사용 허용
  • SO_KEEPALIVE : 유휴 연결 상태 유지
  • SO_LINGER : 소켓 종료 시 데이터 전송 여부 설정
  • SO_SNDBUF / SO_RCVBUF : 송수신 버퍼 크기 설정
  • SO_ERROR : 소켓 오류 상태 확인

3. select()

복수 소켓의 상태를 감시할 수 있다.
FD_ZERO(), FD_SET() 매크로와 함께 사용한다.

fd_set 종류 감시 내용
readfds 읽기(read) 가능 여부 확인
writefds 쓰기(write) 가능 여부 확인
exceptfds 오류 발생 여부 확인
FD_ZERO(&readSet);
FD_SET(sock, &readSet);
timeval timeout = {1, 0}; // 1초 대기
int result = select(0, &readSet, nullptr, nullptr, &timeout);
if (result > 0 && FD_ISSET(sock, &readSet)) {
    recv(sock, buffer, size, 0);
}

 

 

 


 

Blocking vs Non-Blocking 비교

항목 Blocking 구조 Non-Blocking 구조
대기 여부 함수 호출 시 완료될 때까지 대기 함수 호출 즉시 반환
구현 난이도 비교적 간단 select/poll 로직 필요
성능 적은 클라이언트 처리에 적합 대규모 동시 접속 처리에 적합

 

 


 

Non-Blocking에서 주의할 점

 

  • Would Block 에러는 정상:
    아직 소켓이 준비되지 않았다는 의미이며, select()로 대기 후 재시도해야 한다.
  • 0바이트 송신법:
    Non-Blocking connect() 후, 연결 성공 여부를 확인할 때 0바이트를 송신해본다.
  • CPU 낭비 주의:
    select()에 timeout을 설정하여 무한 루프에서 CPU를 소모하지 않도록 한다.
  • UDP 주의사항:
    송신버퍼는 남아있더라도 패킷 크기가 맞지 않으면 보내지 못할 수 있다.

 

 


 

실제 코드 흐름 요약

* 클라이언트

Winsock winsock;
ClientSocket client("localhost", "27015");
client.send("Hello");
auto response = client.receive(512);

 

* 서버

Winsock winsock;
ServerSocket server;
server.accept_connection();
server.socket_recv("Hello from server");