9.15. Tên và DNS

Mọi trang cho đến nay đều sử dụng địa chỉ IP số -- 192.168.1.20 và tương tự. Các ứng dụng thực tế hầu như không bao giờ làm vậy. Các máy chủ được đặt tên là example.com hoặc api.example.com, và ứng dụng tra cứu tên đó lúc chạy để tìm IP để gửi gói tin. Quá trình tra cứu đó là Domain Name System, hay DNS.

9.15.1. Tên phân giải thành gì

Một tên chỉ là một nhãn. example.com bản thân không mang thông tin IP nào -- nó phải được tra cứu, giống như cách một số điện thoại được tra cứu trong danh bạ. Cơ sở hạ tầng DNS là danh bạ điện thoại phân tán của internet, và kết quả của một lần tra cứu là một hay nhiều địa chỉ IP mà camera có thể kết nối đến.

example.com  ->  93.184.216.34

Một tên thường phân giải thành nhiều địa chỉ (để cân bằng tải, dự phòng địa lý, phiên bản IPv4 IPv6 của cùng một dịch vụ). Bất kỳ địa chỉ nào cũng hoạt động; ứng dụng chọn một cái và thử, nếu thất bại thì dùng cái tiếp theo.

9.15.2. Quá trình tra cứu diễn ra như thế nào

Khi camera hỏi về example.com:

  1. Camera gửi một UDP datagram nhỏ (đúng vậy, UDP -- xem UDP -- gửi gói tin, hy vọng điều tốt nhất) đến máy chủ DNS đã cấu hình. Địa chỉ của máy chủ DNS đến từ cùng quá trình DHCP đã cấp cho camera địa chỉ IP của nó.

  2. Máy chủ DNS có thể đã có câu trả lời trong cache (đã được hỏi gần đây). Nếu vậy, nó trả lời ngay.

  3. Nếu không, máy chủ DNS đi qua cây phân cấp DNS toàn cầu: hỏi các máy chủ root về .com, hỏi các máy chủ đó về example.com, hỏi các máy chủ đó về tên. Toàn bộ quá trình duyệt cây được ẩn khỏi camera; camera chỉ thấy một truy vấn và một trả lời.

  4. Máy chủ DNS lưu kết quả vào cache cho lần sau và gửi trả lời về cho camera dưới dạng một UDP datagram khác.

Toàn bộ quá trình trao đổi thường mất vài mili giây trên cache ấm, lên đến khoảng một trăm mili giây trên cache lạnh.

9.15.3. Giao diện Python

Hàm getaddrinfo() thực hiện tra cứu và trả về một địa chỉ sẵn sàng để truyền vào hàm tạo socket:

import socket

addr = socket.getaddrinfo("example.com", 80)[0][-1]
print(addr)
# ('93.184.216.34', 80)

Chữ ký là getaddrinfo(host, port). Giá trị trả về là một danh sách 5-tuple (một cho mỗi địa chỉ được phân giải); [0] chọn cái đầu tiên và [-1] chọn trường cuối cùng, là tuple (ip, port) mà socket có thể nhận trực tiếp:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(socket.getaddrinfo("example.com", 80)[0][-1])
    s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
    ...

Hầu hết code camera giao tiếp với máy chủ từ xa đều bắt đầu bằng một dòng đó. Các helper của asyncio (asyncio.open_connection()) thực hiện tra cứu nội bộ nếu tên được truyền thay vì IP số, vì vậy code async thường không gọi getaddrinfo() trực tiếp.

9.15.4. Những gì có thể xảy ra sai

  • Không có máy chủ DNS nào có thể truy cập. Nếu Wi-Fi vừa bật và liên kết không ổn định, lần gọi getaddrinfo() đầu tiên có thể hết thời gian. OSError được báo ra; thử lại khi liên kết ổn định.

  • Tên không tồn tại. Lỗi đánh máy hoặc tên cũ sẽ báo OSError sau khi máy chủ DNS trả về "no such name". Mã lỗi phân biệt điều này với "DNS server unreachable", nhưng đối với hầu hết ứng dụng camera, chính sách retry/give-up là giống nhau.

  • Địa chỉ trả về không hoạt động. DNS có thể trả về một địa chỉ không còn lưu trữ dịch vụ. Cách khắc phục là chuyển sang mục nhập tiếp theo trong danh sách kết quả của getaddrinfo(), hoặc tra cứu lại tên đó sau (cache DNS có thể đã được cập nhật vào lúc đó).

  • Captive portal. Một số mạng Wi-Fi chặn DNS và trả về IP của trang captive-portal cho mọi thứ. Camera có vẻ kết nối nhưng dữ liệu nó nhận lại sẽ không khớp với những gì dịch vụ thực sự sẽ gửi. Không phổ biến trong môi trường triển khai, nhưng là điều xảy ra trên Wi-Fi hội nghị và các mạng tương tự.

9.15.5. Tên riêng của camera

Các trang trước đã tra cứu tên của các thiết bị khác. Camera cũng có tên riêng của nó mà nó quảng bá đến mạng cục bộ khi xin địa chỉ. Mặc định là một định danh chung khác nhau theo từng board; network.hostname() ghi đè nó bằng thứ gì đó mà phần còn lại của mạng sẽ nhận ra:

import network
network.hostname("kitchen-cam")
# ... then bring the link up as usual ...

Đặt hostname trước khi khởi động giao diện mạng, để tên được gửi đi như một phần của yêu cầu địa chỉ ban đầu của camera thay vì sau đó.

Hai điều xảy ra sau khi camera đã tham gia mạng. Thứ nhất, hầu hết các router gia đình đăng ký hostname của các thiết bị được cấp địa chỉ vào bảng tra cứu cục bộ của chúng, vì vậy các thiết bị khác có thể truy cập camera với tên kitchen-cam -- mà không cần biết địa chỉ số mà router đã gán. (Mạng doanh nghiệp có thể hoặc không hỗ trợ điều này; hành vi phụ thuộc vào router.) Thứ hai, bản thân camera chạy một mDNS responder sẵn có, vì vậy cùng tên đó cũng có thể truy cập dưới dạng kitchen-cam.local trên bất kỳ mạng nào có client hiểu mDNS -- mà hầu hết các hệ điều hành desktop hiện đại đều hiểu.

Ghi chú

Chỉ truyền hostname thuần túy vào network.hostname() -- chỉ "kitchen-cam", không có hậu tố .local. Dạng .local là thứ mDNS thêm vào lúc tra cứu; đưa nó vào hostname làm cho camera quảng bá kitchen-cam.local như một hostname thông thường, điều mà cả hai phía của quá trình tra cứu không mong đợi.

9.15.6. Khi tên không đủ

Một vài tình huống DNS không giúp ích được:

  • Khám phá trên mạng cục bộ. DNS tiêu chuẩn trả lời các câu hỏi về tên được đăng ký trong thư mục toàn cầu; nó không biết gì về các thiết bị trên phân đoạn cục bộ. Multicast DNS (mDNS) là hệ thống lấp đầy khoảng trống đó. Mỗi thiết bị tham gia gia nhập một nhóm multicast đặc biệt trên mạng cục bộ và lắng nghe các truy vấn; khi một thiết bị hỏi về một tên kết thúc bằng .local, thiết bị nào sở hữu tên đó sẽ trả lời trực tiếp. Không có máy chủ trung tâm, không cần cấu hình DNS. Bonjour trên thiết bị Apple, Avahi trên Linux, và Windows 10+ đều sử dụng cùng giao thức -- đó là lý do tại sao tên kitchen-cam.local được thiết lập trong phần trước phân giải được trên mạng gia đình mà không cần cấu hình gì thêm.

    Phía của camera trong trao đổi đó là responder, và nó đã chạy sẵn. Điều camera không có là resolver -- nửa kia, sẽ cho phép một script hỏi mạng "printer.local ở đâu?" và nhận được câu trả lời. Code mDNS đi kèm chỉ là responder (các thiết bị nhúng thường là thứ được tìm thấy, không phải thứ đang tìm kiếm). Khi việc khám phá phải diễn ra theo chiều ngược lại, UDP broadcast (xem UDP -- gửi gói tin, hy vọng điều tốt nhất) là câu trả lời đơn giản hơn cho trường hợp phân đoạn cục bộ, hoặc một module thuần Python như cbrand/micropython-mdns thêm một resolver đầy đủ.

  • Tên IPv6. getaddrinfo() trả về cả kết quả IPv4 và IPv6 nếu cả hai đều có sẵn. Chọn họ địa chỉ mà socket của ứng dụng có thể sử dụng.

Đối với hầu hết code phía camera, getaddrinfo là một dòng lệnh ở đầu bất kỳ hàm nào mở kết nối mạng. Các ví dụ khác trong phần này đã chạy với "192.168.1.20" đều sẽ hoạt động tương tự với một tên công khai như "api.example.com" -- chỉ cần phân giải trước.