9.15. 名前と DNS

これまでのすべてのページでは、192.168.1.20 のような数値の IP アドレスを使ってきました。実際のアプリケーションがそうすることはほとんどありません。サーバーには example.comapi.example.com といった名前が付けられており、アプリケーションは実行時にその名前を引いて、パケットを送る先の IP を見つけます。その名前解決が Domain Name System(DNS)です。

9.15.1. 名前が解決される先

名前は単なるラベルにすぎません。example.com 自体は IP 情報を一切持っておらず、電話番号を電話帳で調べるのと同じように、引かれる必要があります。DNS インフラはインターネットの分散型電話帳であり、名前解決の結果はカメラが接続できる 1 つ以上の IP アドレスです。

example.com  ->  93.184.216.34

1 つの名前は、しばしば複数のアドレスに解決されます(負荷分散、地理的冗長性、同じサービスの IPv4 および IPv6 バージョンのためです)。そのどれを使っても動作します。アプリケーションは 1 つを選んで試し、失敗すれば次へフォールバックします。

9.15.2. 名前解決の仕組み

カメラが example.com を要求すると次のことが起こります。

  1. カメラは小さな UDP データグラム(はい、UDP です。UDP -- パケットを送って最善を祈る を参照)を、設定された DNS サーバーに送ります。DNS サーバーのアドレスは、カメラに自身の IP を渡したのと同じ DHCP のやり取りから得られたものです。

  2. DNS サーバーはすでに答えをキャッシュしているかもしれません(最近問い合わせがあった場合)。その場合はすぐに応答します。

  3. そうでなければ、DNS サーバーはグローバルな DNS 階層をたどります。ルート サーバーに .com について尋ね、それらのサーバーに example.com について尋ね、さらにそれらの サーバーにその名前について尋ねます。このツリー全体のたどりはカメラからは隠されています。カメラには 1 つのクエリと 1 つの応答だけが見えます。

  4. DNS サーバーは次回のために結果をキャッシュし、別の UDP データグラムとして応答をカメラに送り返します。

このやり取り全体は、ウォームキャッシュなら通常数ミリ秒、コールドキャッシュなら最大で 100 ミリ秒程度かかります。

9.15.3. Python インターフェース

getaddrinfo() 関数は名前解決を行い、ソケットコンストラクタにそのまま渡せるアドレスを返します:

import socket

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

シグネチャは getaddrinfo(host, port) です。戻り値は 5 要素タプルの リスト(解決されたアドレスごとに 1 つ)です。[0] で最初のものを選び、[-1] で最後のフィールドを選びます。これがソケットに直接渡せる (ip, port) タプルです:

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")
    ...

リモートサーバーと通信するほとんどのカメラコードは、その 1 行から始まります。asyncio のヘルパー(asyncio.open_connection())は、数値の IP ではなく名前が渡された場合に内部で名前解決を行うため、非同期コードでは通常 getaddrinfo() を直接呼び出すことはありません。

9.15.4. うまくいかないケース

  • 到達可能な DNS サーバーがない。 Wi-Fi が立ち上がったばかりでリンクが不安定な場合、最初の getaddrinfo() 呼び出しがタイムアウトすることがあります。OSError が発生します。リンクが安定したら再試行してください。

  • 名前が存在しない。 タイプミスや古い名前は、DNS サーバーが「そのような名前はない」と返した後に OSError を発生させます。エラーコードによってこれを「DNS サーバーに到達できない」と区別できますが、ほとんどのカメラアプリケーションでは再試行/断念のポリシーは同じです。

  • 返されたアドレスが機能しない。 DNS が、もはやそのサービスをホストしていないアドレスを返すことがあります。対処法は、getaddrinfo() の結果リストの次のエントリにフォールバックするか、後でもう一度名前を引くことです(その頃には DNS キャッシュが更新されている可能性が高いでしょう)。

  • キャプティブポータル。 一部の Wi-Fi ネットワークは DNS を傍受し、すべて に対してキャプティブポータルページの IP を返します。カメラは接続できたように見えますが、返ってくるデータは実際のサービスが送るものと一致しません。運用環境では一般的ではありませんが、会議場の Wi-Fi や類似のネットワークでは起こり得ることです。

9.15.5. カメラ自身の名前

これまでのページでは 他の デバイスの名前を引いてきました。カメラ自身も名前を持っており、アドレスを要求するときにそれをローカルネットワークに広告します。デフォルトはボードによって異なる汎用的な識別子です。network.hostname() を使えば、ネットワークの他の機器が認識できる名前で上書きできます:

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

ホストネームはインターフェースを立ち上げる 前に 設定してください。そうすれば、名前はカメラの最初のアドレス要求の一部として送出され、後からではなくなります。

カメラがネットワークに参加すると、今や 2 つのことが起こります。第 1 に、ほとんどの家庭用ルーターは、アドレスを渡した相手のホストネームを自身のローカルルックアップに登録するため、他のデバイスは、ルーターがたまたま割り当てた数値アドレスを知らなくても、カメラに kitchen-cam として到達できます。(企業ネットワークはこれを尊重する場合もしない場合もあります。挙動はルーター次第です。)第 2 に、カメラ 自身 が初期状態で mDNS レスポンダーを実行しているため、クライアントが mDNS を理解するあらゆるネットワーク上で、同じ名前が kitchen-cam.local としても到達可能になります。最近のほとんどのデスクトップ OS は mDNS を理解します。

注釈

network.hostname() には 素の ホストネーム、つまり "kitchen-cam" だけを渡してください。.local サフィックスは付けません。.local 形式は mDNS が名前解決時に付加するものです。これをホストネームに焼き込むと、カメラは kitchen-cam.local をプレーンなホストネームとして広告してしまい、名前解決のどちら側も期待しない結果になります。

9.15.6. 名前だけでは足りないとき

DNS が役に立たないいくつかの状況があります。

  • ローカルネットワーク上のディスカバリ。 標準の DNS は、グローバルなディレクトリに登録された名前についての問いに答えますが、ローカルセグメント上のデバイスについては何も知りません。Multicast DNS(mDNS)がそのギャップを埋めるシステムです。参加する各デバイスはローカルネットワーク上の特別なマルチキャストグループに参加してクエリを待ち受けます。あるデバイスが .local で終わる名前を要求すると、その名前を所有するデバイスが直接応答します。中央サーバーも DNS 設定も不要です。Apple デバイスの Bonjour、Linux の Avahi、そして Windows 10 以降はいずれも同じプロトコルを話します。だからこそ、前のセクションで設定した kitchen-cam.local という名前は、何も追加設定しなくても家庭のネットワークで解決されるのです。

    そのやり取りにおけるカメラ側は レスポンダー であり、すでに動作しています。カメラが持っていないのは リゾルバー、つまりもう一方の半分で、スクリプトがネットワークに「printer.local はどこか?」と尋ねて答えを得られるようにするものです。付属の mDNS コードはレスポンダー専用です(組み込みデバイスは通常 発見される側 であって、発見する側ではありません)。ディスカバリを逆方向に流す必要がある場合、ローカルセグメントのケースでは UDP ブロードキャスト(UDP -- パケットを送って最善を祈る を参照)の方が単純な答えですし、cbrand/micropython-mdns のようなピュア Python モジュールが完全なリゾルバーを追加してくれます。

  • IPv6 の名前。 getaddrinfo() は、両方が利用可能なら IPv4 と IPv6 の両方の結果を返します。アプリケーションのソケットが使えるファミリーを選んでください。

ほとんどのカメラ側のコードでは、getaddrinfo はネットワーク接続を開くあらゆる関数の先頭に置く 1 行です。このセクションの他の箇所で "192.168.1.20" に対して動かしていた例は、"api.example.com" のような公開名に対してもすべて同じように動作します。まず名前解決するだけです。