서버의 한 포트는 어떻게 여러 연결을 받을까: 소켓
소켓은 응용 프로그램이 네트워크 통신을 요청하고 데이터를 주고받는 운영체제 인터페이스다. 프로그램은 소켓을 통해 전송 프로토콜과 주소를 선택하고, 연결하거나 수신을 기다리며, 데이터를 읽고 쓴다. 소켓은 통신 경로 전체도 아니고 물리적인 랜선도 아니다.
서버의 한 포트에서 연결을 기다리되 클라이언트와의 통신 상태는 연결별로 관리한다.
기다리는 소켓과 대화하는 소켓
전형적인 TCP 서버는 socket으로 소켓을 만들고, bind로 로컬 주소와 포트를 정한 뒤 listen으로 연결을 기다린다. 클라이언트가 연결하면 accept는 그 클라이언트와 통신하는 별도의 연결 소켓을 돌려준다. 기존의 수신 대기 소켓은 계속 다른 연결을 받을 수 있다.
따라서 웹 서버가 하나의 포트에서 수천 명의 클라이언트를 받는다고 모든 데이터가 같은 연결 소켓 하나에 뒤섞이는 것은 아니다. 연결은 로컬과 원격의 주소·포트 조합 같은 정보로 구별된다. 같은 서버 포트를 사용해도 원격 끝점이 다르면 별도의 TCP 연결로 관리할 수 있다. 실제 식별과 네임스페이스에는 운영체제의 구체적인 조건도 관여한다.
클라이언트는 socket을 만들고 connect로 서버에 연결한 뒤 데이터를 주고받는다. 많은 환경에서 로컬 포트는 운영체제가 임시 포트 범위에서 선택한다. 서버의 공개된 서비스 포트와 클라이언트의 로컬 임시 포트는 역할이 다르다. 연결 소켓이 만들어졌다는 사실과 서버가 응용 요청을 처리할 준비가 끝났다는 사실 역시 같지는 않다.
읽기와 쓰기는 기다림을 포함한다
블로킹 소켓의 read 또는 recv는 받을 데이터가 없으면 기다릴 수 있다. 논블로킹 방식에서는 즉시 진행할 수 없다는 결과를 돌려주고, 프로그램이 이벤트 통지나 준비 상태를 활용한다. 논블로킹은 데이터가 무조건 더 빨리 도착한다는 뜻이 아니라, 한 작업의 기다림 때문에 실행 흐름 전체를 묶지 않는 방법이다.
TCP 소켓은 바이트 스트림이므로 한 번의 쓰기가 한 번의 읽기와 대응하지 않는다. 부분 전송과 부분 읽기를 처리하고, 길이와 구분 규칙으로 응용 메시지를 복원해야 한다. 연결 종료와 일시적으로 데이터가 없는 경우를 API 계약에 맞게 구별하는 것도 중요하다. UDP 소켓은 데이터그램을 다루며 listen과 accept가 필요한 TCP 서버와 동일한 순서로 사용하지 않는다.
소켓 버퍼는 응용과 커널 사이의 속도 차이를 흡수한다. 하지만 무한하지 않으며, 쓰기가 받아들여졌어도 원격 응용이 해당 데이터를 처리한 것은 아닐 수 있다. 큰 버퍼로 응용의 지연이나 잘못된 처리 구조를 모두 해결할 수 없다.
동시에 많은 연결을 다루는 서버는 연결별 상태와 파일 디스크립터, 메모리 사용을 관리해야 한다. 연결마다 스레드를 두는 방식, 이벤트 루프, 비동기 I/O 등은 기다림과 작업을 조직하는 서로 다른 선택이다. 한 포트라는 주소 인터페이스 뒤에 실제로 얼마나 많은 자원이 필요한지는 구현에 따라 달라진다.
소켓 문제를 진단할 때는 어느 호출이 실패했는지부터 확인한다. bind 실패는 주소나 포트 사용 문제일 수 있고, connect 실패는 도달성이나 서버 대기 상태와 관련될 수 있다. 읽기 중 정체는 메시지 길이 처리나 상대의 전송 동작을 확인해야 한다. API 단계와 네트워크 단계를 연결하면 포트 하나를 열었다는 말이 가리는 실제 동작을 구체적으로 설명할 수 있다.