9.15. 이름과 DNS#

지금까지의 모든 페이지는 192.168.1.20 같은 숫자 IP 주소를 사용했습니다. 실제 애플리케이션은 거의 그렇게 하지 않습니다. 서버는 example.com 이나 api.example.com 으로 이름이 붙고, 애플리케이션은 패킷을 보낼 IP를 찾기 위해 런타임에 그 이름을 조회합니다. 그 조회가 바로 Domain Name System, 즉 DNS입니다.

9.15.1. 이름이 무엇으로 해석되는가#

이름은 그저 레이블일 뿐입니다. example.com 자체는 어떤 IP 정보도 담고 있지 않으며, 전화번호부에서 전화번호를 찾는 것과 같은 방식으로 조회되어야 합니다. DNS 인프라는 인터넷의 분산된 전화번호부이며, 조회 결과는 카메라가 연결할 수 있는 하나 이상의 IP 주소입니다.

example.com  ->  93.184.216.34

하나의 이름은 흔히 여러 주소로 해석됩니다(부하 분산, 지리적 이중화, 동일 서비스의 IPv4 IPv6 버전을 위해). 그 중 어느 것이든 동작합니다. 애플리케이션은 하나를 골라 시도하고, 실패하면 다음 것으로 넘어갑니다.

9.15.2. 조회는 어떻게 일어나는가#

카메라가 example.com 을 요청하면:

  1. 카메라는 작은 UDP 데이터그램(그렇습니다, UDP입니다 – UDP – 패킷을 보내고 잘 되기를 바라기 참조)을 자신이 설정한 DNS 서버로 보냅니다. DNS 서버의 주소는 카메라에 자신의 IP를 건네준 바로 그 DHCP 교환에서 받은 것입니다.

  2. DNS 서버는 이미 답을 캐시에 가지고 있을 수 있습니다(최근에 같은 질의를 받은 적이 있는 경우). 그렇다면 즉시 응답합니다.

  3. 그렇지 않다면 DNS 서버는 전역 DNS 계층 구조를 따라갑니다. 즉 root 서버에 .com 에 대해 묻고, 그 서버들에 example.com 에 대해 묻고, 서버들에 해당 이름에 대해 묻습니다. 이 전체 트리 탐색은 카메라에게는 숨겨집니다. 카메라는 하나의 질의와 하나의 응답만 봅니다.

  4. DNS 서버는 다음을 위해 결과를 캐시하고, 또 다른 UDP 데이터그램으로 응답을 카메라에 돌려보냅니다.

전체 교환은 일반적으로 캐시가 따뜻할 때는 몇 밀리초, 차가울 때는 최대 백 밀리초 안팎이 걸립니다.

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-튜플의 리스트 입니다(해석된 주소당 하나씩). [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")
    ...

원격 서버와 통신하는 대부분의 카메라 코드는 그 한 줄로 시작합니다. asyncio 헬퍼(asyncio.open_connection())는 숫자 IP 대신 이름이 전달되면 내부적으로 조회를 수행하므로, 비동기 코드는 보통 getaddrinfo() 를 직접 호출하지 않습니다.

9.15.4. 무엇이 잘못될 수 있는가#

  • 도달 가능한 DNS 서버 없음. Wi-Fi가 막 올라왔고 링크가 불안정하면 첫 getaddrinfo() 호출이 타임아웃될 수 있습니다. OSError 가 발생합니다. 링크가 안정되면 다시 시도하세요.

  • 이름이 존재하지 않음. 오타나 오래된 이름은 DNS 서버가 “그런 이름 없음”을 반환한 뒤 OSError 를 발생시킵니다. 오류 코드는 이를 “DNS 서버 도달 불가”와 구분해 주지만, 대부분의 카메라 애플리케이션에서는 재시도/포기 정책이 동일합니다.

  • 반환된 주소가 동작하지 않음. DNS는 더 이상 해당 서비스를 호스팅하지 않는 주소를 반환할 수 있습니다. 해결책은 getaddrinfo() 의 결과 리스트에서 다음 항목으로 넘어가거나, 나중에 이름을 다시 조회하는 것입니다(그때쯤이면 DNS 캐시가 갱신되어 있을 가능성이 큽니다).

  • 캡티브 포털(captive portal). 일부 Wi-Fi 네트워크는 DNS를 가로채 모든 것 에 대해 캡티브 포털 페이지의 IP를 반환합니다. 카메라는 연결된 것처럼 보이지만, 돌려받는 데이터는 실제 서비스가 보냈을 것과 일치하지 않습니다. 배포 환경에서는 흔하지 않지만, 컨퍼런스 Wi-Fi나 유사한 네트워크에서 일어나는 일입니다.

9.15.5. 카메라 자신의 이름#

지금까지의 페이지들은 다른 기기의 이름을 조회했습니다. 카메라 역시 주소를 요청할 때 로컬 네트워크에 알리는 자신만의 이름을 가지고 있습니다. 기본값은 보드마다 다른 일반적인 식별자입니다. network.hostname() 으로 네트워크의 나머지 부분이 인식할 만한 이름으로 재정의할 수 있습니다:

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

호스트네임은 인터페이스를 올리기 전에 설정하세요. 그래야 이름이 나중이 아니라 카메라의 초기 주소 요청의 일부로 나가게 됩니다.

카메라가 네트워크에 합류하면 이제 두 가지 일이 일어납니다. 첫째, 대부분의 가정용 라우터는 자신이 주소를 건네준 호스트네임을 자체 로컬 조회에 등록하므로, 다른 기기들이 라우터가 우연히 할당한 숫자 주소를 알 필요 없이 카메라에 kitchen-cam 으로 접근할 수 있습니다. (기업 네트워크는 이를 따를 수도 안 따를 수도 있습니다. 동작은 라우터에 달려 있습니다.) 둘째, 카메라 자체 가 별도 설정 없이 mDNS 응답기를 실행하므로, 클라이언트가 mDNS를 이해하는 모든 네트워크에서 같은 이름이 kitchen-cam.local 로도 접근 가능합니다. 대부분의 최신 데스크톱 운영체제가 이를 이해합니다.

참고

network.hostname() 에는 순수한 호스트네임을 전달하세요. 즉 .local 접미사 없이 "kitchen-cam" 만 전달합니다. .local 형식은 mDNS가 조회 시점에 덧붙이는 것입니다. 그것을 호스트네임에 박아 넣으면 카메라가 kitchen-cam.local 을 일반 호스트네임으로 알리게 되는데, 이는 조회의 양쪽 어느 쪽도 기대하지 않는 것입니다.

9.15.6. 이름만으로 충분하지 않을 때#

DNS가 도움이 되지 않는 몇 가지 상황이 있습니다:

  • 로컬 네트워크에서의 검색. 표준 DNS는 전역 디렉터리에 등록된 이름에 대한 질의에 응답하며, 로컬 세그먼트에 있는 장치에 대해서는 아무것도 알지 못합니다. Multicast DNS (mDNS)는 그 공백을 메우는 시스템입니다. 참여하는 모든 장치는 로컬 네트워크의 특수한 멀티캐스트 그룹에 가입하여 질의를 수신하며, 어떤 장치가 .local 로 끝나는 이름을 요청하면 그 이름을 소유한 장치가 직접 응답합니다. 중앙 서버도, DNS 구성도 필요 없습니다. Apple 장치의 Bonjour, Linux의 Avahi, 그리고 Windows 10 이상은 모두 동일한 프로토콜을 사용하는데, 바로 이 때문에 이전 섹션에서 설정한 kitchen-cam.local 이름이 추가 구성 없이 가정용 네트워크에서 해석됩니다.

    그 교환에서 카메라 쪽은 응답기(responder) 이며, 이미 실행 중입니다. 카메라에 없는 것은 리졸버(resolver), 즉 스크립트가 네트워크에 “printer.local 은 어디 있나?”라고 묻고 답을 받게 해주는 나머지 절반입니다. 함께 제공되는 mDNS 코드는 응답기 전용입니다(임베디드 기기는 일반적으로 찾아지는 대상 이지 찾는 주체가 아닙니다). 디스커버리가 반대 방향으로 흘러야 할 때, 로컬 세그먼트의 경우에는 UDP 브로드캐스트(UDP – 패킷을 보내고 잘 되기를 바라기 참조)가 더 간단한 답이며, 또는 cbrand/micropython-mdns 같은 순수 Python 모듈이 완전한 리졸버를 추가해 줍니다.

  • IPv6 이름. getaddrinfo() 는 IPv4와 IPv6가 모두 사용 가능하면 두 결과를 모두 반환합니다. 애플리케이션의 소켓이 사용할 수 있는 패밀리를 고르세요.

대부분의 카메라 측 코드에서 getaddrinfo 는 네트워크 연결을 여는 모든 함수의 맨 위에 오는 한 줄입니다. 이 섹션의 다른 곳에서 "192.168.1.20" 에 대해 실행했던 예제들은 "api.example.com" 같은 공개 이름에 대해서도 모두 동일하게 동작합니다. 다만 먼저 해석하면 됩니다.