9.12. UDP socket¶
在 Python 中,UDP 流量是透過資料包(datagram)socket 上的兩個方法來傳送與接收的:sendto() 用來向選定的目的地發送一個資料包,recvfrom() 用來接收一個資料包並得知它來自何處。每次呼叫都會搬移一則自成一體的訊息;不存在任何連線狀態。
9.12.1. 傳送資料包¶
最簡單的 UDP 傳送,是在 socket 建構式之上加一行 Python 程式碼:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()
這會將 b"hello" 送往 192.168.1.20 上的連接埠 9000,然後不再過問。MicroPython 會挑選一個臨時來源連接埠;指令碼不需要繫結任何東西。
將同一份酬載傳送到多個目的地,只是一個迴圈而已,因為 socket 在多次傳送之間是可重複使用的,而且沒有連線需要建立:
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. 接收資料包¶
要接收資料包,socket 必須佔用一個已知的連接埠,讓傳送方將其作為目的地使用。這就是 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 都屬於這個 socket。
recvfrom() 的 1024 引數,是要讀入所回傳緩衝區的最大位元組數。超過此大小的 UDP 資料包會被 截斷;請挑選一個能配合應用程式所預期最大資料包的數值。
recvfrom() 回傳 (data, src):接收到的位元組,以及傳送方的位址。傳送方的位址正是要回覆的對象,這讓撰寫一個小型的請求/回應協定變得容易:
while True:
request, src = s.recvfrom(1024)
if request == b"ping":
s.sendto(b"pong", src)
預設情況下,recvfrom() 會 封鎖,直到有資料包抵達。讓它不要封鎖的各種模式(逾時、非封鎖 socket、asyncio)都在 使用 asyncio 的 Socket 中介紹。
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?")
傳送端中有幾點值得留意:
settimeout()為接收呼叫設定了一個期限。如果兩秒內沒有回覆抵達,該呼叫會拋出OSError,而非永遠封鎖,這是偵測封包遺失的一種自然方式。with區塊會自動關閉 socket。
9.12.4. 資料包大小限制¶
理論上 UDP 資料包最大可達約 64 KB,但實務上的限制小得多。傳送方與接收方之間路徑上的每一段連結,都有一個 最大傳輸單元(Maximum Transmission Unit,MTU),也就是該連結在單一影格中能承載的最大位元組區塊。乙太網路與 Wi-Fi 都將此值限制在約 1500 位元組,而幾乎每一條網際網路路徑在某處都會回溯到這個限制。
當一個資料包超過它必須跨越之連結的 MTU 時,網路層會將它切割成較小的 分段(fragment),並在目的地重新組合。UDP 本身永遠看不到這種切割,但分段具有幾項不便的特性:
只要其中任何一個分段遺失,整個資料包在接收方都會被丟棄,因為沒有逐分段的重傳機制。遺失機率會隨著分段數量增加而升高。
某些網路與防火牆會將分段封包視為可疑而直接丟棄。
重新組合會在接收方耗用記憶體,而在微控制器上記憶體是相當稀缺的。
在相機上的實務準則是:讓 UDP 訊息遠低於 1500 位元組。約 1400 位元組可為 IP 與 UDP 標頭、路徑所增加的任何隧道(tunneling)負擔,以及乙太網路、Wi-Fi 與 VPN 連結之間 MTU 的小幅變動預留空間。需要傳送超過此大小的應用程式,應在應用層將資料切塊,或改用 TCP,因為 TCP 會自動處理切割與重新組合。
9.12.5. 常見陷阱¶
忘了 UDP 可能遺失封包。 在安靜的本地網路上運作完美的程式碼,有時會在較繁忙或範圍較廣的網路上以難以察覺的方式失敗。請始終為訊息可能沒有抵達的情況做好設計。
接收方在傳送方傳送前尚未繫結。 送往無人監聽之連接埠的資料包會被悄悄丟棄。請先啟動接收方。
傳送大於路徑 MTU 的資料包。 請參閱前一節,讓訊息保持在約 1400 位元組以下。
上述模式涵蓋了相機所會用到的幾乎每一種 UDP 用途。下一頁將針對 TCP 做相同的說明。