학습 목표
TCP echo 서버·클라이언트를 소켓 API로 구현할 수 있다.
서버 bind → listen → accept 와 클라이언트 connect 흐름을 설명할 수 있다.
listen 소켓과 연결 소켓(connected) 의 차이를 설명할 수 있다.
소켓 호출이 시스템 콜을 통해 커널 TCP 스택과 연결됨을 설명할 수 있다.
문제 상황
curl은 되는데 직접 TCP 서버를 짜면Address already in use가 난다bind·SO_REUSEADDR·프로세스 재시작 타이밍
- 서버 코드에
accept가 있는데 클라이언트는connect만 한다 — 역할이 다르다 recv가 0바이트를 반환하면 무한 루프에 빠진다- 상대가 연결을 닫음 —
close/FIN처리 필요
- 상대가 연결을 닫음 —
connect·sendall을 호출했는데 커널·와이어에서 실제로 무슨 일이 일어나는지 감이 없다- 애플리케이션 계층 개요에서 소켓 API를 봤다 — 이번엔 직접 호출한다
1. Echo 서버가 하는 일
Echo는 받은 바이트를 그대로 돌려보낸다. HTTP·프로토콜 파싱 없이 TCP 바이트 스트림만 다루므로 소켓 API 연습에 적합하다.

| 역할 | 동작 |
|---|---|
| 서버 | 포트에 고정·대기·연결 수락·데이터 반사 |
| 클라이언트 | 서버 주소로 연결·전송·응답 수신 |
| 전송 계층 | TCP 신뢰·순서 — 앱은 send/recv만 호출 |
2. 서버 vs 클라이언트 API 순서

서버 (TCP) — echo_server.py
| 순서 | 호출 | 의미 |
|---|---|---|
| 1 | socket() |
listen_sock — IPv4 TCP 소켓 |
| 2 | setsockopt(SO_REUSEADDR) |
재시작 시 포트 재사용 |
| 3 | bind((HOST, PORT)) |
127.0.0.1:9000 고정 |
| 4 | listen(BACKLOG) |
연결 대기 큐 |
| 5 | accept() |
클라이언트 연결 수락 → conn |
| 6 | conn.recv / conn.sendall |
conn으로 echo |
| 7 | not data → break |
상대 FIN — with conn 종료 |
| 8 | 외부 while True |
listen_sock 은 다음 accept 대기 |
클라이언트 (TCP) — echo_client.py
| 순서 | 호출 | 의미 |
|---|---|---|
| 1 | socket() |
sock 생성 |
| 2 | connect((HOST, PORT)) |
3-way handshake |
| 3 | sendall(message.encode()) |
UTF-8 bytes 전송 |
| 4 | recv(1024) → print |
echo 응답 수신·출력 |
| 5 | with 블록 종료 |
close() — FIN |
listen소켓 —accept만 한다. 데이터 recv 안 함conn소켓 —accept가 돌려준 연결별 fd — echo는 여기서recv/send- 클라이언트는
bind생략 가능 — OS가 임시 포트(ephemeral) 할당
3. 코드 — echo 서버
code/echo_server.py:
#!/usr/bin/env python3
import socket
HOST = "127.0.0.1"
PORT = 9000
BACKLOG = 5
def main() -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listen_sock:
listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_sock.bind((HOST, PORT))
listen_sock.listen(BACKLOG)
print(f"listening on {HOST}:{PORT}")
while True:
conn, addr = listen_sock.accept()
with conn:
print(f"connected from {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
print(f"closed {addr}")
if __name__ == "__main__":
main()
| 줄 | 설명 |
|---|---|
BACKLOG = 5 |
listen() 인자 — handshake 대기 큐 상한(개념) |
SO_REUSEADDR |
재시작 시 TIME_WAIT 포트 재사용 |
with conn |
연결별 fd 자동 close |
recv(1024) |
TCP 스트림 — 한 번에 다 안 올 수 있음 |
not data |
빈 bytes = 상대 FIN — 루프 종료 |
closed {addr} |
conn 종료 로그 — listen_sock 은 유지 |
4. 코드 — echo 클라이언트
code/echo_client.py:
#!/usr/bin/env python3
import socket
import sys
HOST = "127.0.0.1"
PORT = 9000
def main() -> None:
message = sys.argv[1] if len(sys.argv) > 1 else "hello echo"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((HOST, PORT))
sock.sendall(message.encode())
data = sock.recv(1024)
print(data.decode())
if __name__ == "__main__":
main()
# 터미널 1
python3 code/echo_server.py
# 터미널 2
python3 code/echo_client.py "ping from client"
# ping from client
5. 유저 모드와 커널
소켓 함수는 라이브러리에 있지만, 실제로는 시스템 콜로 커널에 진입한다.

echo_server.py (listen_sock + conn):
listen_sock Kernel
socket() ──syscall──> create listen_sock fd
setsockopt ──syscall──> SO_REUSEADDR
bind(:9000) ──syscall──> local port assigned
listen(BACKLOG) ──syscall──> TCP_LISTEN
accept() ──syscall──> return conn fd · ESTABLISHED
conn (per client)
recv(1024) ──syscall──> copy from socket rx buffer
sendall(data) ──syscall──> enqueue socket tx buffer
close (with) ──syscall──> FIN · release conn fd
echo_client.py (sock):
sock Kernel
socket() ──syscall──> create sock fd
connect(:9000) ──syscall──> SYN · 3-way handshake
sendall(bytes) ──syscall──> enqueue socket tx buffer
recv(1024) ──syscall──> copy from socket rx buffer
close (with) ──syscall──> FIN · release sock fd
| 개념 | 설명 |
|---|---|
| 소켓 버퍼 | 커널이 유지 — recv는 버퍼에서 복사, send는 버퍼에 적재 |
| 블로킹 | 기본 recv/accept — 데이터 올 때까지 프로세스 대기 (OS 스케줄링) |
| 파일 디스크립터 | 소켓도 fd — select/poll·이벤트 루프의 대상 |
| 멀티 프로세스 | 요청마다 fork/스레드 vs 비동기 이벤트 (고급) |
- OS 시리즈 시스템 콜·유저/커널 모드와 동일 경계
strace -e trace=network python3 code/echo_client.py— 실제 syscall 이름 확인 (Linux)
6. 자주 나는 오류
| 증상 | 원인 | 대응 |
|---|---|---|
Address already in use |
포트 이미 bind | 서버 종료·SO_REUSEADDR·다른 PORT |
Connection refused |
listen 없음·방화벽·잘못된 PORT | 서버 기동·ss -tlnp |
recv hang |
서버가 응답 안 함·블로킹 | 프로토콜·버그·타임아웃 settimeout |
recv → b"" |
정상 종료 | 루프 break |
| 한글 깨짐 | bytes vs str | encode()/decode()·UTF-8 |
# 리스닝 확인
ss -tlnp | grep 9000
# macOS
lsof -iTCP:9000 -sTCP:LISTEN
7. 정리
- TCP echo —
bind/listen/acceptvsconnect— 역할 분리 accept소켓 ≠ listen 소켓 — 연결마다 새 fdrecv빈 bytes — peer close 신호- 소켓 = 시스템 콜 → 커널 TCP·버퍼
- HTTP·TLS는 이 위에 프로토콜 레이어
다음에 다룰 것
- 네트워크 트러블슈팅
- 연결 실패 체크리스트, DNS·TLS·방화벽
해당 내용은 Computer Networking: A Top-Down Approach, 8/E (James Kurose, Keith Ross) 의 내용을 기반으로 합니다.
'네트워크 기초' 카테고리의 다른 글
| Network Series를 마치며 (0) | 2026.07.08 |
|---|---|
| 네트워크 트러블슈팅 (0) | 2026.07.07 |
| Wireshark로 HTTP/TCP 보기 (0) | 2026.07.05 |
| 물리 계층과 매체 (0) | 2026.07.04 |
| 이더넷과 MAC (0) | 2026.07.03 |