socket 모듈은 저수준 네트워크 인터페이스를 제공합니다. 이 치트시트는 TCP/UDP 클라이언트/서버의 최소 패턴, 타임아웃 처리, 주의점을 정리합니다.
언제 이 치트시트를 보나?
- 저수준 네트워크 통신이 필요할 때
- 커스텀 프로토콜 구현이 필요할 때
- HTTP 라이브러리 없이 간단한 요청을 보내고 싶을 때
핵심 패턴
- TCP:
SOCK_STREAM - 연결 지향, 신뢰성 보장 - UDP:
SOCK_DGRAM - 비연결, 빠르지만 손실 가능 - 서버:
bind() → listen() → accept() → recv()/send() - 클라이언트:
connect() → send()/recv()
TCP 클라이언트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import socket
# TCP 클라이언트 기본
def tcp_client():
# AF_INET: IPv4, SOCK_STREAM: TCP
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(5.0) # 5초 타임아웃
sock.connect(('localhost', 8080))
# 데이터 전송
sock.sendall(b'Hello, Server!')
# 데이터 수신
data = sock.recv(1024) # 최대 1024바이트
print(f'Received: {data.decode()}')
tcp_client()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # 간단한 HTTP GET 요청
import socket
def simple_http_get(host: str, path: str = '/') -> str:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(10.0)
sock.connect((host, 80))
# HTTP 요청 전송
request = f'GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n'
sock.sendall(request.encode())
# 응답 수신
response = b''
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
return response.decode()
# 실제로는 requests나 urllib 사용 권장
# print(simple_http_get('example.com', '/'))
|
TCP 서버
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import socket
def tcp_server():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
# 주소 재사용 허용 (빠른 재시작)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 8080))
server.listen(5) # 대기 큐 크기
print('Server listening on port 8080...')
while True:
# 클라이언트 연결 대기
client, addr = server.accept()
with client:
print(f'Connected by {addr}')
# 데이터 수신
data = client.recv(1024)
if data:
print(f'Received: {data.decode()}')
# 응답 전송
client.sendall(b'Hello, Client!')
# tcp_server()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # 다중 클라이언트 처리 (스레드)
import socket
import threading
def handle_client(client: socket.socket, addr: tuple):
with client:
print(f'Connected: {addr}')
while True:
data = client.recv(1024)
if not data:
break
client.sendall(data) # 에코 서버
print(f'Disconnected: {addr}')
def threaded_server():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 8080))
server.listen(5)
while True:
client, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(client, addr))
thread.start()
|
UDP 클라이언트/서버
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import socket
# UDP 클라이언트
def udp_client():
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.settimeout(5.0)
# connect 없이 바로 전송 (비연결)
sock.sendto(b'Hello, UDP Server!', ('localhost', 9999))
# 응답 수신
data, addr = sock.recvfrom(1024)
print(f'Received from {addr}: {data.decode()}')
# UDP 서버
def udp_server():
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind(('localhost', 9999))
print('UDP Server listening on port 9999...')
while True:
data, addr = sock.recvfrom(1024)
print(f'Received from {addr}: {data.decode()}')
sock.sendto(b'ACK', addr)
|
타임아웃 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# 전체 타임아웃 설정
sock.settimeout(5.0)
try:
sock.connect(('localhost', 8080))
sock.sendall(b'test')
data = sock.recv(1024)
except socket.timeout:
print('Connection timed out')
except socket.error as e:
print(f'Socket error: {e}')
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 논블로킹 모드
import socket
import select
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
try:
sock.connect(('localhost', 8080))
except BlockingIOError:
pass # 논블로킹에서는 바로 반환
# select로 대기
ready = select.select([sock], [sock], [], 5.0)
if ready[1]: # 쓰기 가능 = 연결됨
sock.sendall(b'test')
|
유용한 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import socket
# 호스트 이름 → IP
ip = socket.gethostbyname('example.com')
print(ip) # 93.184.216.34
# 서비스 이름 → 포트
port = socket.getservbyname('http')
print(port) # 80
# 현재 호스트 이름
hostname = socket.gethostname()
print(hostname)
# 주소 정보 조회
info = socket.getaddrinfo('example.com', 80)
for family, socktype, proto, canonname, sockaddr in info:
print(sockaddr) # ('93.184.216.34', 80)
|
소켓 옵션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 주소 재사용 (서버 재시작 시 유용)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# TCP Keepalive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# 버퍼 크기
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8192)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192)
# TCP_NODELAY (Nagle 알고리즘 비활성화)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
자주 하는 실수/주의점
- recv()는 요청한 만큼 안 올 수 있음: 루프로 전체 수신 필요
1
2
3
4
5
6
7
8
| def recv_all(sock, length):
data = b''
while len(data) < length:
chunk = sock.recv(length - len(data))
if not chunk:
raise ConnectionError('Connection closed')
data += chunk
return data
|
- sendall() vs send():
send()는 일부만 보낼 수 있음, sendall() 권장 - 리소스 정리:
with 문 또는 try/finally로 소켓 닫기 - 인코딩: 문자열은
encode()/decode() 필요 - 포트 번호: 1024 미만은 root 권한 필요
- IPv6:
AF_INET6 사용, 듀얼 스택은 별도 처리 필요
고수준 대안
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 일반 HTTP 요청 → requests 라이브러리
import requests
response = requests.get('https://example.com')
# 비동기 서버 → asyncio
import asyncio
async def handle_client(reader, writer):
data = await reader.read(100)
writer.write(data)
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(handle_client, 'localhost', 8080)
async with server:
await server.serve_forever()
# 웹 서버 → Flask, FastAPI 등
|
관련 링크(공식 문서)