9.12. UDP 소켓

Python에서 UDP 트래픽은 데이터그램 소켓의 두 메서드로 송수신합니다. 선택한 목적지로 데이터그램을 발사하는 sendto() 와, 데이터그램을 받고 그것이 어디서 왔는지 알아내는 recvfrom() 입니다. 각 호출은 하나의 자체 완결적인 메시지를 옮기며, 연결 상태는 없습니다.

9.12.1. 데이터그램 보내기

가장 단순한 UDP 전송은 소켓 생성자 위에 한 줄의 Python으로 이루어집니다:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()

이 코드는 192.168.1.20 의 포트 9000 으로 b"hello" 를 보내고 그냥 떠납니다. MicroPython이 임시(ephemeral) 소스 포트를 고르므로, 스크립트는 아무것도 바인딩할 필요가 없습니다.

동일한 페이로드를 여러 목적지로 보내는 것은 그저 루프일 뿐입니다. 소켓은 전송 사이에 재사용 가능하며, 설정할 연결이 없습니다:

targets = [
    ("192.168.1.20", 9000),
    ("192.168.1.21", 9000),
    ("192.168.1.22", 9000),
]

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    for addr in targets:
        s.sendto(b"hello", addr)

9.12.2. 데이터그램 받기

데이터그램을 받으려면 소켓이 송신자가 목적지로 사용할 알려진 포트를 점유해야 합니다. 그것이 bind() 호출입니다:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    data, src = s.recvfrom(1024)
    print("from", src, "got", data)

"0.0.0.0" 주소는 “카메라의 모든 IPv4 인터페이스”를 의미합니다. 어떤 Wi-Fi 또는 이더넷 인터페이스가 패킷을 들여오든, 포트 9000 은 이 소켓에 속합니다.

recvfrom()1024 인자는 반환되는 버퍼로 읽어 들일 최대 바이트 수입니다. 이 크기를 초과하는 UDP 데이터그램은 잘립니다. 애플리케이션이 예상하는 가장 큰 데이터그램에 맞춰 값을 고르세요.

recvfrom()(data, src) 를 반환합니다. 받은 바이트와 송신자의 주소입니다. 송신자의 주소는 응답할 대상이므로, 작은 요청/응답 프로토콜을 작성하기 쉽습니다:

while True:
    request, src = s.recvfrom(1024)
    if request == b"ping":
        s.sendto(b"pong", src)

기본적으로 recvfrom() 은 데이터그램이 도착할 때까지 블록 됩니다. 블록되지 않도록 만드는 패턴들(타임아웃, 논블로킹 소켓, asyncio)은 asyncio를 이용한 소켓 에 있습니다.

9.12.3. 요청과 응답

두 개의 짧은 스크립트: 하나는 요청을 보내고, 하나는 받아서 응답합니다.

수신자:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    req, src = s.recvfrom(64)
    print("got", req, "from", src)
    s.sendto(b"ack: " + req, src)

송신자:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.settimeout(2.0)                              # 2 s reply window
    s.sendto(b"ping", ("192.168.1.20", 9000))
    try:
        reply, _ = s.recvfrom(64)
        print("reply:", reply)
    except OSError:
        print("no reply in 2 s -- packet lost?")

송신자에서 주목할 만한 몇 가지:

  • bind() 도 없고 connect() 도 없습니다. UDP 클라이언트는 그냥 보냅니다.

  • settimeout() 은 수신 호출에 마감 시한을 둡니다. 2초 안에 응답이 도착하지 않으면, 호출은 영원히 블록되는 대신 OSError 를 발생시킵니다. 손실된 패킷을 감지하는 자연스러운 방법입니다.

  • with 블록은 소켓을 자동으로 닫습니다.

9.12.4. 데이터그램 크기 한계

UDP 데이터그램은 이론적으로 최대 약 64 KB까지 가능하지만, 실제 한계는 훨씬 작습니다. 송신자와 수신자 사이 경로의 모든 링크에는 Maximum Transmission Unit (MTU), 즉 해당 링크가 하나의 프레임에 담아 전달할 수 있는 가장 큰 단일 바이트 블록이 있습니다. 이더넷과 Wi-Fi는 모두 이 값을 약 1500바이트로 제한하며, 거의 모든 인터넷 경로는 어딘가에서 이 한계로 귀결됩니다.

데이터그램이 통과해야 하는 링크의 MTU를 초과하면, 네트워크 계층이 이를 더 작은 프래그먼트 로 분할하고 목적지에서 재조립합니다. UDP 자체는 분할을 결코 보지 못하지만, 프래그먼트에는 몇 가지 불편한 특성이 있습니다:

  • 어느 하나의 프래그먼트라도 손실되면, 전체 데이터그램이 수신자에서 폐기됩니다. 프래그먼트별 재전송은 없습니다. 손실 확률은 프래그먼트 수와 함께 커집니다.

  • 일부 네트워크와 방화벽은 단편화된 패킷을 의심스러운 것으로 취급하여 완전히 폐기합니다.

  • 재조립은 수신자에서 메모리를 소비하는데, 마이크로컨트롤러에서는 메모리가 부족합니다.

카메라에서의 실용적인 규칙: UDP 메시지를 1500바이트보다 충분히 작게 유지하세요. 약 1400바이트로 하면 IP 및 UDP 헤더, 경로가 추가하는 터널링 오버헤드, 이더넷·Wi-Fi·VPN 링크 간 MTU의 작은 변동을 위한 여지가 남습니다. 그보다 많이 보내야 하는 애플리케이션은 애플리케이션 계층에서 데이터를 청크로 나누거나, 분할과 재조립을 자동으로 처리하는 TCP로 전환해야 합니다.

9.12.5. 흔한 함정

  • UDP가 패킷을 잃을 수 있다는 것을 잊는 것. 조용한 로컬 네트워크에서 완벽하게 작동하는 코드가 더 바쁘거나 더 넓은 네트워크에서 미묘한 방식으로 실패하기도 합니다. 메시지가 도착하지 않았을 가능성을 항상 염두에 두고 설계하세요.

  • 송신자가 보내기 전에 수신자가 바인딩되지 않은 것. 아무도 리스닝하지 않는 포트로 보낸 데이터그램은 조용히 폐기됩니다. 수신자를 먼저 시작하세요.

  • 경로의 MTU보다 큰 데이터그램을 보내는 것. 앞 절을 참고하세요. 메시지를 약 1400바이트 미만으로 유지하세요.

위의 패턴들은 카메라가 사용하는 거의 모든 UDP 용도를 다룹니다. 다음 페이지는 TCP에 대해 동등한 내용을 다룹니다.