실시간 서버 만들기 전에 알아 둘 네트워크 기본: TIME_WAIT, select와 epoll, WebSocket·SSE, 채팅 서버 구조

이 글의 핵심

채팅이나 알림처럼 실시간 서버를 만들 때 먼저 부딪히는 문제 세 가지(소켓 상태, I/O 모델, 전송 방식)를 우분투 컨테이너에서 실행해 확인했습니다. 2,000번 연결·종료 뒤 TIME_WAIT가 2,000개 남았고, select는 fd 1104에서 실패했지만 epoll은 등록에 성공했습니다.

채팅이나 알림 서버를 만들 때는 세 가지가 차례로 문제가 된다. 소켓이 어떤 상태에 쌓이는지, 많은 연결을 어떤 방식으로 기다릴지, 서버와 클라이언트가 어떤 전송 방식으로 말할지다. 이 글은 앞의 두 가지를 우분투 24.04 컨테이너에서 직접 실행해 확인하고, 세 번째와 구조 설계는 자세한 글로 연결한다. 컨테이너는 aarch64, 파이썬은 3.12다.

실행으로 확인한 것

실험결과
로컬에서 서버가 먼저 close하는 구조로 2,000번 연결·종료ss -tan state time-wait로 본 TIME-WAIT 소켓 2,000개
남은 소켓의 로컬 주소127.0.0.1:9100(서버 포트) 쪽
파일 디스크립터 번호가 1,104인 소켓을 select.select에 넣기ValueError: filedescriptor out of range in select()
같은 소켓을 select.epoll에 등록성공

1. 연결 상태: 누가 먼저 끊느냐

TCP 연결을 먼저 끊은 쪽에 TIME_WAIT 상태가 남는다. 위 실험은 서버가 응답 뒤 close()를 먼저 부르는 구조여서, 2,000개가 서버 포트 쪽(:9100)에 쌓였다. 이 상태는 오류가 아니라 늦게 도착한 패킷으로부터 연결을 보호하는 정상 동작이다. 다만 짧은 연결을 많이 만드는 서버에서는 개수가 많아질 수 있으므로 개수 자체보다 포트 고갈이나 연결 실패가 생기는지를 본다. TIME_WAIT와 CLOSE_WAIT가 쌓이는 이유와 ss 디버깅은 TCP 연결 상태 11가지에 정리했다.

2. I/O 멀티플렉싱: select는 번호에 한계가 있다

연결이 수천 개가 되면 스레드 하나가 연결 하나를 맡는 방식은 감당하기 어렵고, 한 스레드가 여러 소켓을 기다리는 방식을 쓴다. 이 실험에서 select는 파일 디스크립터 번호가 1,024 이상이면 오류를 냈고(번호 1104), epoll은 같은 소켓을 등록할 수 있었다. 이 오류는 파이썬의 select가 FD_SETSIZE 한계를 검사해서 나는 것이라고 알려져 있다. 리눅스의 epoll, BSD·맥의 kqueue, 윈도우의 IOCP 차이와 C10K 문제는 I/O 멀티플렉싱 비교에서 다룬다.

3. 전송 방식: WebSocket, SSE, 롱 폴링

서버가 클라이언트에게 먼저 보내야 하는 기능이 필요하면 폴링, 롱 폴링, SSE, WebSocket 중에서 고른다. 한 방향 알림이면 SSE가, 양방향이면 WebSocket이 맞는 경우가 많다는 것이 일반적인 판단이다. 선택 기준과 프록시·재접속 같은 운영 문제는 WebSocket vs SSE vs 롱 폴링에 있다.

4. 구조: 방 관리와 연결 수 제한

위 세 가지를 정한 다음에는 연결을 받는 스레드와 일하는 스레드를 나누고, 방(room)과 메시지 라우팅을 설계한다. 이 구조를 C++와 Asio로 구현한 예가 채팅 서버 아키텍처다. 소켓 API 자체가 낯설다면 소켓 프로그래밍 기초와 네트워크 에러 처리를 먼저 읽는 편이 좋다.

읽는 순서 제안

  1. TCP 연결 상태 → 2. I/O 멀티플렉싱 → 3. 전송 방식 → 4. 서버 구조.

확인하지 못한 것

  • 실제 네트워크(지연이 있는 환경)에서의 TIME_WAIT 개수와 tcp_tw_reuse 같은 커널 설정의 영향.
  • select와 epoll의 성능 차이. 이 글은 번호 한계만 확인했다.
  • WebSocket·SSE의 실제 연결 유지 시간과 프록시 동작.