소켓 API는 어디서 왔나: 버클리 소켓
오늘날 프로그램이 네트워크를 쓰는 표준적인 방식인 "소켓 API"는 1983년 공개된 4.2BSD 유닉스에서 시작되어 버클리 소켓(Berkeley sockets)이라고 불립니다(위키백과 기준). 이후 이 인터페이스는 POSIX 표준의 일부가 되어 "POSIX 소켓"과 사실상 같은 말로 쓰이고, 마이크로소프트 윈도우의 Winsock도 이 인터페이스를 거의 그대로 따릅니다. 운영체제가 달라도 소켓 호출의 이름과 순서가 비슷한 이유가 여기에 있습니다.
프로그램이 연결을 맺는 순서
아래는 POSIX 문서와 위키백과에서 확인되는 TCP 연결의 기본 호출 순서입니다.
| 호출 | 쓰는 쪽 | 하는 일 |
|---|---|---|
| socket() | 서버·클라이언트 | 통신 끝점을 만들고 파일 디스크립터를 돌려줍니다(실패 시 -1). |
| bind() | 주로 서버 | 소켓에 로컬 주소와 포트를 붙입니다. |
| listen() | 서버(TCP) | 연결을 받을 수 있는 상태로 만들고, backlog로 대기열 길이를 힌트로 지정합니다. 구현체가 값을 더 낮게 조정할 수 있습니다(POSIX). |
| accept() | 서버(TCP) | 대기열에서 연결 하나를 꺼내 그 연결 전용의 새 소켓을 만듭니다. 리스닝 소켓은 계속 열려 있습니다. |
| connect() | 클라이언트 | TCP에서는 서버에 연결을 맺고, UDP에서는 기본 상대 주소를 정합니다. |
| send()/recv(), sendto()/recvfrom() | 양쪽 | 데이터를 보내고 받습니다. |
아래는 이 순서를 보여주는 짧은 일반 예시(파이썬, 같은 컴퓨터 안에서만 동작하는 에코 서버)입니다. 서버를 먼저 실행한 뒤 다른 창에서 클라이언트를 실행합니다.
# 서버: socket -> bind -> listen -> accept
import socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("127.0.0.1", 5000)) # 로컬 PC에서만 접속 가능
srv.listen(5)
conn, addr = srv.accept() # addr = 상대의 (IP, 임시 포트)
conn.sendall(conn.recv(1024)) # 받은 내용을 그대로 돌려줌
conn.close(); srv.close()
# 클라이언트: socket -> connect
import socket
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(("127.0.0.1", 5000))
c.sendall(b"hello")
print(c.recv(1024))
c.close()여기서 서버의 accept()가 돌려준 addr에 찍히는 상대 포트가 바로 클라이언트에 배정된 임시 포트입니다. 127.0.0.1은 자기 자신을 가리키는 주소이므로 외부에서 접속할 수 없고, 실습용으로 안전합니다. 허락받지 않은 다른 시스템에 접속을 시도하는 행위는 법적 문제가 될 수 있으므로, 실습은 반드시 자신의 컴퓨터 안에서만 하세요.
TCP 소켓과 UDP 소켓 비교
| 구분 | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) |
|---|---|---|
| 연결 | 연결 지향: 먼저 연결을 맺음 | 비연결: 연결 없이 데이터그램 전송 |
| 서버 호출 | bind → listen → accept | bind 후 바로 recvfrom (accept 불필요) |
| 신뢰성 | 바이트 스트림을 오류 정정과 함께 전달 | 순서 뒤바뀜, 중복, 유실이 있을 수 있음(위키백과) |
| 부하 | 상대적으로 큼 | 상대적으로 작음 |
실제 사례로 보는 소켓
사례 1. 운영체제마다 달랐던 임시 포트 범위
클라이언트가 서버에 접속할 때 쓰는 임시 포트는 IANA가 49152~65535를 권고합니다(RFC 6335). 그러나 위키백과에 정리된 바에 따르면 윈도우 XP 이하는 기본값이 1024~5000이었고, 윈도우 비스타 이후는 IANA 범위를 기본으로 씁니다. 윈도우 서버 2003은 2008년 보안 업데이트 MS08-037 이후 IANA 범위를 쓰게 되었습니다. 리눅스는 다수 커널에서 32768~60999가 기본값이고, /proc/sys/net/ipv4/ip_local_port_range에서 실제 범위를 확인할 수 있습니다. 배울 점은 "임시 포트 49152~65535"를 모든 환경에 적용되는 상수로 외우면 안 된다는 것입니다. 방화벽 규칙이나 동시 접속 설계를 할 때는 대상 OS의 실제 범위를 확인해야 합니다.
사례 2. 하나의 80번 포트에서 수많은 연결을 처리하는 서버의 구조
웹서버는 보통 80이나 443번 포트에 리스닝 소켓 하나를 열어 두고, 새 접속이 들어올 때마다 accept()가 연결 전용의 새 소켓을 만들어 줍니다. 이 새 소켓들은 서버 쪽 IP:포트는 같지만 상대(클라이언트) IP:포트가 다르므로 서로 다른 소켓 쌍으로 구별됩니다. 그래서 포트 번호가 하나여도 연결이 섞이지 않습니다. 이것은 버클리 소켓 API의 기본 설계에서 확인되는 동작이며, 동시 접속자를 처리하는 방식(프로세스, 스레드, 이벤트 기반)은 서버 소프트웨어마다 다릅니다.
직접 확인해 보기
- 윈도우 명령 프롬프트에서
netstat -an을 실행해 LISTENING과 ESTABLISHED 상태의 소켓 주소("IP:포트")를 구분해 봅니다. 읽기만 하는 명령이라 안전합니다. - 브라우저로 사이트에 접속한 직후 같은 명령을 실행하면 내 컴퓨터의 임시 포트와 서버의 443번 포트로 이어진 연결을 볼 수 있습니다.
- 위 파이썬 예시를 로컬(127.0.0.1)에서만 실행해 accept()가 돌려준 상대 포트와 netstat 출력이 일치하는지 비교해 봅니다.
흔한 오해와 정정
- 오해: 소켓과 포트는 같은 말이다. 실제: 포트는 숫자 하나이고, 소켓은 IP·포트·프로토콜이 결합된 통신 끝점입니다.
- 오해: 한 포트에는 연결을 하나만 받을 수 있다. 실제: 리스닝 소켓 하나에서 연결마다 새 소켓이 만들어지고 소켓 쌍이 다르면 서로 구분됩니다.
- 오해: UDP 소켓은 위험하고 TCP 소켓만 안전하다. 실제: UDP는 신뢰성 기능이 없을 뿐 용도가 다른 프로토콜이며, 필요하면 응용 프로그램이 직접 재전송·순서 처리를 구현합니다.
- 오해: 웹소켓은 소켓의 다른 이름이다. 실제: 웹소켓은 응용 계층의 별도 프로토콜입니다.
한계와 더 알아볼 점
이 글의 호출 순서는 TCP/UDP 기반 인터넷 소켓의 일반적인 형태입니다. 같은 컴퓨터 안의 프로세스끼리 쓰는 유닉스 도메인 소켓처럼 IP·포트를 쓰지 않는 소켓도 있고, 언어와 프레임워크마다 소켓 위에 더 높은 추상화를 얹어 직접 호출할 일이 거의 없는 경우도 많습니다. listen()의 backlog 값이 실제로 어떻게 해석되는지는 구현체에 따라 달라질 수 있습니다(POSIX 문서는 힌트라고 규정).
포트 번호 자체가 궁금하다면
소켓을 이루는 요소 중 하나인 포트 번호 자체를 더 자세히 알고 싶다면포트 번호 완전정복도 함께 확인해보세요.
실시간 양방향 통신에 쓰이는 웹소켓과는 다릅니다
이름이 비슷한 "웹소켓(WebSocket)"은 이 페이지에서 다룬 운영체제 수준의 일반적인 소켓 개념이 아니라, 브라우저와 서버가 지속적인 양방향 통신을 하기 위해 만들어진 별도의 응용 계층 프로토콜입니다. 웹소켓 자체가 궁금하다면 웹소켓(WebSocket)이란? 기초 개념 정리를 확인해보세요.
함께 읽으면 좋은 가이드
소켓 개념을 이해하면 방화벽(Firewall)이란? 기초 개념 정리과포트포워딩 설정하는 방법이 왜 필요한지 더 쉽게 이해할 수 있습니다. TCP와 UDP의 차이는 TCP와 UDP 차이 가이드에서 더 자세히 볼 수 있습니다.
자주 묻는 질문
소켓 번호는 사용자가 직접 정하나요?
서버가 사용하는 잘 알려진 포트(80, 443 등)는 프로그램이나 관리자가 지정하지만, 클라이언트가 접속할 때 쓰는 임시 포트는 운영체제가 자동으로 배정하므로 사용자가 신경 쓸 필요가 없습니다.
소켓이 부족해지는 경우도 있나요?
네, 매우 많은 연결을 동시에 짧은 시간에 맺고 끊는 서버에서는 임시 포트가 일시적으로 부족해지는 "소켓 고갈" 현상이 나타날 수 있습니다. 이런 경우 서버 설정에서 연결 재사용(Keep-Alive) 설정을 조정해 완화하기도 합니다.
bind()는 클라이언트에서도 꼭 호출해야 하나요?
일반적으로 클라이언트는 bind()를 생략하고 connect()를 호출하면 운영체제가 임시 포트를 자동으로 배정합니다. 특정 로컬 포트를 쓰고 싶을 때만 bind()를 먼저 호출합니다.
listen()의 backlog를 크게 주면 동시 접속자를 그만큼 받을 수 있나요?
그렇지 않을 수 있습니다. POSIX 문서에 따르면 backlog는 대기열 길이에 대한 힌트이고 구현체가 최대값으로 낮춰 적용할 수 있습니다. 또 대기열은 아직 accept()되지 않은 연결의 수를 말하므로 전체 동시 접속자 수와는 다른 개념입니다.
127.0.0.1로 연 서버에는 왜 다른 컴퓨터에서 접속이 안 되나요?
127.0.0.1은 자기 자신(루프백)을 가리키는 주소라서 같은 컴퓨터 안에서만 닿습니다. 외부에서 접속하게 하려면 다른 주소에 바인드해야 하지만, 그 경우 방화벽과 접근 통제를 먼저 점검해야 합니다.