9.12. UDP sockets¶
Lưu lượng UDP trong Python được gửi và nhận bằng hai phương thức trên một datagram socket: sendto() để gửi một datagram tới đích đã chọn, và recvfrom() để nhận một datagram và biết nó đến từ đâu. Mỗi lần gọi di chuyển một tin nhắn độc lập; không có trạng thái kết nối nào.
9.12.1. Gửi một datagram¶
Cách gửi UDP đơn giản nhất là một dòng Python trên socket constructor:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()
Lệnh đó gửi b"hello" tới cổng 9000 trên 192.168.1.20 rồi kết thúc. MicroPython tự chọn cổng nguồn tạm thời; tập lệnh không cần bind bất cứ thứ gì.
Gửi cùng một dữ liệu tới nhiều đích chỉ là một vòng lặp -- socket có thể tái sử dụng giữa các lần gửi, và không cần thiết lập kết nối:
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. Nhận một datagram¶
Để nhận datagram, socket phải yêu cầu một cổng đã biết mà các bên gửi sẽ dùng làm đích. Đó là lệnh gọi 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)
Địa chỉ "0.0.0.0" có nghĩa là "mọi giao diện IPv4 trên camera" -- dù gói tin đến qua giao diện Wi-Fi hay Ethernet nào, cổng 9000 đều thuộc về socket này.
Đối số 1024 cho recvfrom() là số byte tối đa để đọc vào bộ đệm trả về. Các datagram UDP vượt quá kích thước này sẽ bị cắt bớt; hãy chọn giá trị phù hợp với datagram lớn nhất mà ứng dụng dự kiến nhận.
recvfrom() trả về (data, src): các byte đã nhận, và địa chỉ của bên gửi. Địa chỉ của bên gửi chính là nơi để phản hồi, giúp dễ dàng viết một giao thức yêu cầu/phản hồi nhỏ:
while True:
request, src = s.recvfrom(1024)
if request == b"ping":
s.sendto(b"pong", src)
Theo mặc định, recvfrom() chặn cho đến khi có datagram đến. Các cách để không bị chặn -- timeout, socket non-blocking, asyncio -- được trình bày trên Sockets với asyncio.
9.12.3. Một yêu cầu và một phản hồi¶
Hai tập lệnh ngắn: một gửi yêu cầu, một nhận và phản hồi.
Bên nhận:
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)
Bên gửi:
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?")
Một vài điểm đáng chú ý trong bên gửi:
Không có
bind()và không cóconnect(). UDP client chỉ cần gửi.settimeout()đặt thời hạn cho lệnh gọi nhận. Nếu không có phản hồi nào đến trong hai giây, lệnh gọi sẽ ném raOSErrorthay vì chặn mãi mãi -- đây là cách tự nhiên để phát hiện gói tin bị mất.Khối
withtự động đóng socket.
9.12.4. Giới hạn kích thước datagram¶
Datagram UDP có thể lên tới khoảng 64 KB về mặt lý thuyết, nhưng giới hạn thực tế nhỏ hơn nhiều. Mỗi liên kết trên đường đi giữa bên gửi và bên nhận đều có Maximum Transmission Unit (MTU) -- khối byte đơn lớn nhất mà liên kết đó có thể mang trong một khung hình. Ethernet và Wi-Fi đều giới hạn ở khoảng 1500 byte, và hầu hết mọi đường đi internet đều bị ràng buộc bởi giới hạn đó ở đâu đó.
Khi một datagram vượt quá MTU của liên kết mà nó phải đi qua, tầng mạng sẽ chia nó thành các fragment nhỏ hơn và lắp ráp lại ở đích. Bản thân UDP không bao giờ thấy việc chia nhỏ này, nhưng các fragment có một số thuộc tính bất tiện:
Nếu bất kỳ fragment nào bị mất, toàn bộ datagram sẽ bị loại bỏ ở đầu nhận -- không có cơ chế truyền lại từng fragment. Xác suất mất dữ liệu tăng theo số lượng fragment.
Một số mạng và tường lửa loại bỏ hoàn toàn các gói tin bị phân mảnh, coi chúng là đáng ngờ.
Việc lắp ráp lại tiêu tốn bộ nhớ ở đầu nhận, mà trên vi điều khiển thì bộ nhớ rất hạn chế.
Quy tắc thực tế trên camera: giữ các tin nhắn UDP dưới 1500 byte. Khoảng 1400 byte để lại dư địa cho header IP và UDP, bất kỳ overhead tunneling nào trên đường đi, và các biến thể nhỏ về MTU giữa Ethernet, Wi-Fi và liên kết VPN. Các ứng dụng cần gửi nhiều hơn thế nên chia nhỏ dữ liệu ở tầng ứng dụng hoặc chuyển sang TCP, vốn xử lý việc chia nhỏ và lắp ráp lại một cách tự động.
9.12.5. Những lỗi thường gặp¶
Quên rằng UDP có thể mất gói tin. Code chạy hoàn hảo trên một mạng nội bộ yên tĩnh đôi khi thất bại theo những cách tinh vi trên một mạng bận hơn hoặc rộng hơn. Luôn thiết kế với khả năng tin nhắn có thể không đến nơi.
Bên nhận chưa bind trước khi bên gửi gửi. Một datagram gửi tới cổng mà không ai đang lắng nghe sẽ bị loại bỏ âm thầm. Khởi động bên nhận trước.
Gửi một datagram lớn hơn MTU của đường đi. Xem phần trước -- giữ tin nhắn dưới ~1400 byte.
Các mẫu trên bao gồm hầu hết mọi cách sử dụng UDP mà camera cần đến. Trang tiếp theo làm điều tương đương cho TCP.