9.12. UDP sockets

ทราฟฟิก UDP ใน Python ส่งและรับด้วยสองเมธอดบน datagram socket ได้แก่ sendto() สำหรับส่ง datagram ไปยังปลายทางที่เลือก และ recvfrom() สำหรับรับ datagram และทราบแหล่งที่มา การเรียกแต่ละครั้งเคลื่อนย้ายข้อความที่สมบูรณ์ในตัวเอง ไม่มีสถานะการเชื่อมต่อ

9.12.1. การส่ง datagram

การส่ง UDP ที่ง่ายที่สุดคือโค้ด Python หนึ่งบรรทัดบน socket constructor:

import socket

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

นั่นส่ง b"hello" ไปยังพอร์ต 9000 บน 192.168.1.20 แล้วออกไป MicroPython เลือก source port ชั่วคราว สคริปต์ไม่ต้อง bind อะไรทั้งนั้น

การส่ง payload เดียวกันไปยังหลายปลายทางเป็นแค่ลูป -- 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. การรับ datagram

เพื่อรับ datagrams 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 บน camera" -- ไม่ว่าจะเป็นอินเทอร์เฟซ Wi-Fi หรือ Ethernet ที่นำแพ็กเก็ตเข้ามา พอร์ต 9000 เป็นของ socket นี้

อาร์กิวเมนต์ 1024 ของ recvfrom() คือจำนวนไบต์สูงสุดที่อ่านลงในบัฟเฟอร์ที่คืนค่า UDP datagrams ที่มีขนาดเกินกว่านี้จะถูก ตัดทอน เลือกค่าให้ตรงกับขนาด datagram ที่ใหญ่ที่สุดที่แอปพลิเคชันคาดไว้

recvfrom() คืนค่า (data, src) ได้แก่ ไบต์ที่รับมาและที่อยู่ของผู้ส่ง ที่อยู่ของผู้ส่งคือสิ่งที่จะตอบกลับ ทำให้ง่ายต่อการเขียนโปรโตคอล request/response ขนาดเล็ก:

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

โดยค่าเริ่มต้น recvfrom() จะ บล็อก จนกว่า datagram จะมาถึง รูปแบบสำหรับทำให้ไม่บล็อก ได้แก่ timeouts, non-blocking sockets, asyncio อธิบายไว้ที่ Sockets กับ 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 clients แค่ส่ง

  • settimeout() กำหนดเวลาหมดอายุสำหรับการเรียก receive หากไม่มีการตอบกลับภายในสองวินาที การเรียกจะ raise OSError แทนที่จะบล็อกตลอดไป -- เป็นวิธีธรรมชาติในการตรวจจับแพ็กเก็ตที่สูญหาย

  • บล็อก with ปิด socket โดยอัตโนมัติ

9.12.4. ขีดจำกัดขนาด datagram

UDP datagrams สามารถมีขนาดได้ถึงประมาณ 64 KB ในทางทฤษฎี แต่ขีดจำกัดในทางปฏิบัติมีขนาดเล็กกว่ามาก ทุกลิงก์ในเส้นทางระหว่างผู้ส่งและผู้รับมี Maximum Transmission Unit (MTU) -- บล็อกไบต์เดียวที่ใหญ่ที่สุดที่ลิงก์นั้นสามารถส่งได้ในหนึ่งเฟรม Ethernet และ Wi-Fi จำกัดอยู่ที่ประมาณ 1500 ไบต์ และเส้นทางอินเทอร์เน็ตแทบทุกเส้นทางสืบย้อนไปยังขีดจำกัดนั้น

เมื่อ datagram เกิน MTU ของลิงก์ที่ต้องข้าม ชั้น network จะแบ่งมันออกเป็น fragments ขนาดเล็กและรวมกลับที่ปลายทาง UDP เองไม่เคยเห็นการแบ่ง แต่ fragments มีคุณสมบัติที่ไม่สะดวกหลายประการ

  • หาก fragment ใดหายไป datagram ทั้งหมดจะถูกทิ้งที่ผู้รับ -- ไม่มีการส่งซ้ำต่อ fragment ความน่าจะเป็นของการสูญเสียเพิ่มขึ้นตามจำนวน fragment

  • บางเครือข่ายและไฟร์วอลล์ทิ้งแพ็กเก็ตที่ถูก fragment ทั้งหมด โดยถือว่าน่าสงสัย

  • การ re-assembly ใช้หน่วยความจำที่ผู้รับ ซึ่งบน microcontroller มีจำกัด

กฎในทางปฏิบัติบน camera คือ ให้ข้อความ UDP อยู่ต่ำกว่า 1500 ไบต์อย่างดี ประมาณ 1400 ไบต์เหลือพื้นที่สำหรับส่วนหัว IP และ UDP, overhead ของ tunneling ที่เส้นทางเพิ่ม และความแตกต่างเล็กน้อยใน MTU ระหว่าง Ethernet, Wi-Fi และลิงก์ VPN แอปพลิเคชันที่ต้องส่งข้อมูลมากกว่านั้นควรแบ่งข้อมูลที่ชั้นแอปพลิเคชัน หรือเปลี่ยนไปใช้ TCP ซึ่งจัดการการแบ่งและ re-assembly โดยอัตโนมัติ

9.12.5. ปัญหาที่พบบ่อย

  • ลืมว่า UDP สามารถสูญเสียแพ็กเก็ต โค้ดที่ทำงานได้อย่างสมบูรณ์บนเครือข่ายท้องถิ่นที่เงียบสงบ บางครั้งล้มเหลวในรูปแบบละเอียดบนเครือข่ายที่มีทราฟฟิกมากขึ้นหรือกว้างขึ้น ออกแบบสำหรับความเป็นไปได้ที่ข้อความไม่ถึงเสมอ

  • ฝั่งรับไม่ได้ bind ก่อนที่ฝั่งส่งจะส่ง datagram ที่ส่งไปยังพอร์ตที่ไม่มีใครฟังอยู่จะถูกทิ้งอย่างเงียบๆ เริ่ม receiver ก่อน

  • ส่ง datagram ที่ใหญ่กว่า MTU ของเส้นทาง ดูหัวข้อก่อนหน้า -- ให้ข้อความอยู่ต่ำกว่า ~1400 ไบต์

รูปแบบข้างต้นครอบคลุมการใช้งาน UDP เกือบทุกกรณีที่ camera ต้องการ หน้าถัดไปทำสิ่งเทียบเท่าสำหรับ TCP