9.13. TCP 소켓¶
TCP 소켓은 겉보기에는 달라 보이지만 동일한 기반 타입을 공유하는 두 가지 형태로 존재합니다. 원격 서버에 connect() 하는 클라이언트 소켓과, bind(), listen() 한 뒤 들어오는 연결을 accept() 하는 서버 소켓입니다. 두 역할 모두 소켓 객체 에서 소개한 동일한 socket 클래스를 사용하며, 다만 호출하는 메서드만 다를 뿐입니다.
9.13.1. TCP 클라이언트¶
가장 단순한 클라이언트는 연결을 열고, 요청을 보내고, 응답을 읽은 뒤 연결을 닫습니다:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)
s.close()
connect() 는 TCP – 신뢰성 있는 바이트 스트림 에서 다룬 3-way 핸드셰이크를 수행하고 연결이 열리면 반환합니다. send() 는 바이트를 연결에 씁니다. recv() 는 연결에서 지정한 바이트 수까지 읽습니다. 애플리케이션 작업이 끝나면 close() 가 연결을 종료합니다.
소켓 객체 에서 소개한 with 문 관용구로 감싼 동일한 스크립트로, 무언가 예외를 발생시키더라도 소켓이 닫힙니다:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
print(s.recv(1024))
9.13.1.1. 끝까지 읽기¶
한 번의 recv() 는 요청한 바이트 수 까지만 반환합니다. TCP는 메시지의 시퀀스가 아니라 스트림이기 때문에 더 적은 양을 반환할 수도 있습니다. 애플리케이션은 전체 응답을 받을 때까지 계속 읽어야 합니다:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
recv() 가 빈 bytes 를 반환하면 루프가 끝납니다. 이는 상대편이 자신의 연결 절반을 정상적으로 닫았을 때 발생합니다. 이런 방식의 프로토콜에서 애플리케이션은 “스트림의 끝”을 “메시지의 끝”과 동일하게 읽습니다.
9.13.1.2. 끝까지 보내기¶
반대 주의 사항이 send() 에도 적용됩니다. 요청한 것보다 더 적은 바이트를 보낼 수 있으며, 실제로 쓰여진 바이트 수를 반환합니다. 큰 페이로드의 경우 보내지 못한 나머지를 다시 시도하세요:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() 은 내부적으로 루프를 수행하므로, 대부분의 코드는 그냥 이를 호출하여 수동 재시도를 피할 수 있습니다:
s.sendall(some_big_bytes)
9.13.2. TCP 서버¶
서버 측은 네 단계입니다. 포트를 점유하고, 소켓을 리스닝 모드로 전환하고, 연결을 하나씩 수락하고, 수락된 각 소켓에서 통신합니다. 최소한의 에코 서버는 다음과 같습니다:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")
while True:
conn, addr = server.accept()
print("connection from", addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data) # echo back
conn.close()
단계별로:
bind()는 카메라의 호스트와 포트를 점유합니다."0.0.0.0"는 모든 인터페이스에서 수락합니다. 이를 특정 IP로 바꾸면 리스너가 해당 인터페이스로 제한됩니다.listen()은 소켓을 일반 소켓에서 리스닝 소켓으로 전환합니다. 인자는 백로그 로, 애플리케이션이 바쁜 동안 MicroPython이 대기열에 넣을 수 있는 보류 중인 연결의 수입니다. 작은 숫자를 고르세요. 대부분의 경우1이면 충분합니다.accept()는 클라이언트가 연결할 때까지 블록되었다가(conn, addr)을 반환합니다. 이 하나의 연결을 나타내는 새 소켓과 클라이언트의 주소입니다. 리스닝 소켓 자체는 더 많은 연결을 수락하기 위해 열린 상태로 유지됩니다.이 통신의 모든 바이트는 새 소켓인
conn을 통해 흐릅니다. 읽기와 쓰기는 클라이언트 측과 동일한recv()/send()호출을 사용합니다.클라이언트가 닫으면
recv()가b""를 반환합니다. 내부 루프가 끝나고 서버는close()로 자신의 쪽을 닫습니다.
바깥쪽 while True 는 다음 클라이언트를 기다리기 위해 accept() 로 돌아갑니다. 이 형태에서 서버는 한 번에 하나의 클라이언트를 처리합니다. 여러 클라이언트를 병렬로 실행하려면 스레드나 asyncio 가 필요합니다. 후자는 다음 페이지의 주제입니다.
9.13.3. 흔한 함정¶
recv() 를 메시지 형태로 취급하기. 그렇지 않습니다. 두 번의
send(b"hi")호출이b"hihi"의 한 번의recv(4)로 도착할 수도 있고, 두 번의recv(2)로 도착할 수도 있습니다. 메시지 경계가 중요하다면 애플리케이션은 프레이밍을 추가해야 합니다. 개행 문자든, 길이 접두사든, 무엇이든 말이죠.짧은 전송 시 재시도를 잊는 것. 수백 바이트를 넘는 것에는
sendall()을 사용하세요.수락된 소켓을 닫는 것을 잊는 것. 각
conn은 별도의 소켓입니다. 리스닝 소켓을 닫아도 수락된 소켓은 닫히지 않습니다. 양쪽 모두에with블록을 사용하면 실수하기 어렵게 만들 수 있습니다:while True: with server.accept()[0] as conn: # ... talk on conn ...
아직 TIME_WAIT 상태인 포트에 다시 바인딩하기. 서버가 닫은 후 몇 초 이내에 재시작하면, MicroPython이 이전 연결을 위해 여전히 포트를 잡고 있기 때문에
bind()가 “address in use” 오류로 실패할 수 있습니다.bind()이전에server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)를 호출하면 이를 해소합니다.
9.13.4. 다음 단계¶
accept() 에서 블록된다는 것은 서버가 한 번에 하나의 클라이언트만 처리할 수 있다는 의미입니다. recv() 에서 블록된다는 것은 느린 클라이언트 하나가 전체 루프를 멈추게 한다는 의미입니다. 카메라에서의 표준적인 해답은 asyncio 입니다. 각 연결을 자체 태스크로 실행하고, 이벤트 루프가 그 사이를 디스패치하게 하는 것입니다. 다음 페이지는 이 페이지의 모든 것에 대한 asyncio 버전을 다룹니다.