소켓은 애플리케이션이 운영체제의 네트워크 기능을 사용하는 endpoint다. Python의 socket 모듈로 TCP 서버를 만들 때는 socket → bind → listen → accept → recv/send, 클라이언트는 socket → connect → send/recv 흐름을 따른다. UDP는 연결을 수립하지 않고 sendto와 recvfrom으로 datagram과 상대 주소를 함께 다룬다.
아래 코드는 흐름을 이해하기 위한 localhost 최소 예제다. 동시 접속, TLS, 인증, 메시지 크기 제한과 과부하 제어를 갖춘 운영 서버 코드는 아니다.
TCP 서버와 클라이언트의 역할
TCP 서버에는 두 종류의 소켓이 등장한다.
listening socket
└─ accept()
└─ connected socket 1
└─ connected socket 2
└─ ...
listening socket은 새 연결 요청을 받는다. accept()가 반환한 connected socket으로 특정 클라이언트와 데이터를 주고받는다. listening socket 자체에서 클라이언트 데이터를 읽는 것은 아니다.
| 서버 호출 | 의미 |
|---|---|
socket() |
주소 체계와 전송 방식을 선택해 소켓을 만든다. |
bind() |
로컬 IP 주소와 포트에 소켓을 연결한다. |
listen() |
연결 요청을 받을 listening socket으로 전환한다. |
accept() |
대기 중인 연결 하나를 받아 새 connected socket을 반환한다. |
recv()·sendall() |
연결된 byte stream을 읽고 쓴다. |
줄바꿈으로 메시지 경계를 만든 TCP 예제
TCP는 메시지 목록이 아니라 순서가 보장되는 byte stream을 제공한다. 한 번의 sendall()이 상대의 한 번의 recv()로 돌아온다고 가정하면 안 된다. 이 예제는 줄바꿈(\n)을 하나의 메시지 끝으로 정한다.
서버 코드는 다음과 같다.
# tcp_server.py
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f"listening on {HOST}:{PORT}")
conn, address = server.accept()
with conn:
conn.settimeout(5)
with conn.makefile("rwb") as stream:
line = stream.readline(4097)
if not line:
raise ConnectionError("client closed before sending a line")
if len(line) > 4096 or not line.endswith(b"\n"):
raise ValueError("message must end with newline within 4096 bytes")
print(f"received from {address}: {line!r}")
stream.write(b"echo: " + line)
stream.flush()
클라이언트는 같은 framing 규칙을 사용한다.
# tcp_client.py
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.create_connection((HOST, PORT), timeout=5) as client:
with client.makefile("rwb") as stream:
stream.write("안녕하세요\n".encode("utf-8"))
stream.flush()
reply = stream.readline(4097)
if len(reply) > 4096 or not reply.endswith(b"\n"):
raise ValueError("invalid response framing")
print(reply.decode("utf-8").rstrip("\n"))
먼저 서버를 실행하고 다른 터미널에서 클라이언트를 실행한다.
python3 tcp_server.py
python3 tcp_client.py
makefile()은 소켓을 buffered binary stream으로 감싸 readline()을 쓸 수 있게 한다. raw recv()를 사용할 때는 받은 byte 수가 요청한 크기보다 작을 수 있으므로 반복해서 읽고 애플리케이션 framing을 직접 처리해야 한다.
TCP에서 연결 종료도 프로토콜의 일부다
recv()가 빈 bytes인 b''를 반환하면 상대가 정상적인 byte stream 종료를 알린 상태다. 아직 메시지가 끝나지 않았는데 연결이 닫혔다면 완전한 요청으로 처리하면 안 된다.
길이 prefix를 쓰는 프로토콜이라면 다음과 같은 구조를 만들 수 있다.
[4-byte payload length][payload bytes]
먼저 4바이트를 정확히 읽고, 해석한 길이만큼 다시 반복해 읽는다. 이때 최대 허용 길이를 검사하지 않으면 공격자가 지나치게 큰 메모리 할당을 유도할 수 있다. JSON이라도 TCP가 객체 경계를 보존해 주지 않으므로 newline, length prefix 또는 표준 상위 프로토콜이 필요하다.
UDP 서버와 클라이언트 예제
UDP는 datagram 경계를 유지한다. 서버는 datagram과 보낸 주소를 함께 받고 그 주소로 답한다.
# udp_server.py
import socket
HOST = "127.0.0.1"
PORT = 5001
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
server.bind((HOST, PORT))
data, address = server.recvfrom(4096)
print(f"received from {address}: {data!r}")
server.sendto(b"echo: " + data, address)
# udp_client.py
import socket
SERVER = ("127.0.0.1", 5001)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
client.settimeout(3)
client.sendto("안녕하세요".encode("utf-8"), SERVER)
reply, address = client.recvfrom(4096)
print(address, reply.decode("utf-8"))
UDP는 연결 설정과 순서 보장, 재전송을 기본 제공하지 않는다. datagram이 사라지거나 중복되고 순서가 바뀔 수 있다. 애플리케이션이 신뢰성, request ID, 재시도와 중복 제거를 필요로 한다면 직접 설계하거나 이미 검증된 상위 프로토콜을 사용한다.
recvfrom(4096)보다 큰 datagram은 운영체제와 API 동작에 따라 잘릴 수 있다. 큰 payload를 임의로 IP fragmentation에 맡기기보다 프로토콜의 크기 제한과 분할 방식을 정의한다.
TCP와 UDP를 고르는 기준
| 질문 | TCP | UDP |
|---|---|---|
| 데이터 모델 | 순서 있는 byte stream | 독립된 datagram |
| 전달·순서 | 재전송과 순서 복구 제공 | 애플리케이션이 필요 여부를 결정 |
| 혼잡 제어 | TCP가 제공 | 애플리케이션·상위 프로토콜 책임 |
| 메시지 경계 | 보존하지 않음 | datagram 경계 보존 |
| 대표 사용 | HTTP/1.1·2, SSH, SMTP | DNS 질의, 실시간 미디어의 일부, QUIC의 기반 |
“빠르니 UDP”가 선택 기준은 아니다. 손실 복구와 순서가 필요하면 그 비용이 애플리케이션으로 이동한다. 반대로 오래된 데이터보다 최신 데이터가 중요하고 자체 시퀀스·손실 처리 모델이 있다면 datagram이 적합할 수 있다.
실제 서버로 확장할 때 빠진 것
- 여러 연결을 처리할 thread, process, selector 또는 async event loop
- idle·read·write timeout과 연결 수 제한
- TLS 인증서 검증과 애플리케이션 인증·권한 검사
- 최대 메시지 길이, 입력 검증과 rate limit
- partial read/write, 취소, backpressure와 graceful shutdown
- request ID, 구조화 로그와 민감 정보 마스킹
- IPv4·IPv6, DNS 실패와 다중 주소 연결 전략
포트가 여러 연결을 어떤 소켓으로 나누는지는 전송 계층 멀티플렉싱·역다중화, TCP 연결의 혼잡 제어는 TCP 혼잡 제어 정리에서 이어서 볼 수 있다.
참고 자료
'배움과 성장 > 네트워크·보안' 카테고리의 다른 글
| 신뢰적 데이터 전송 원리: 체크섬·순서 번호·ACK·재전송 (0) | 2023.04.01 |
|---|---|
| 전송 계층 멀티플렉싱·역다중화: 포트와 소켓으로 이해하기 (0) | 2023.03.31 |
| P2P 파일 배포 원리: 클라이언트-서버와 BitTorrent 비교 (0) | 2023.03.31 |
| DNS Record 종류와 조회법: A·AAAA·CNAME·MX·NS·TXT·PTR (0) | 2023.03.31 |
| DNS 루트 서버 운영 비용은 누가 내나: 독립 운영기관의 재원 구조 (0) | 2023.03.31 |
댓글