お前等の名前を一片に解決してやるわフハハハハ

大量の正引きをしたいときってありますよね? え、無いの? 大人なの? 何なの? バカなの? 死ぬの??

DNS に名前聞くとネットワークを経由するので幾許かの時間は掛かるので、それが勿体ないって場合に対応するように非同期で名前解決して処理する」ってのは良くあるみたいなんですが、あたしがいきなりそんな難しいことして上手くできるはずがありません。

で、とりあえず、沢山の FQDNDNS サーバに聞いてみる、ってのをやってみることに。

id:Voluntas さんに「UDP のソケットってのは使い回せる」ってのを聞いてたのもあって、

  • socket を 1 つ張って持っとく (Resolver)
  • 解決すべき候補が来たら登録して query 投げとく (set)
  • ある程度 query 溜まったら一括処理しとく (sweep)
  • 直ぐ解決結果が欲しいと言われたら返す (get)

って感じでやってみることに。

sweep するとき timeout したいんで、select 使ってみることにしました。select で timeout 指定して、全体である程度経っても返事得られなかったら失敗したことにしましょう。

昨日の DNS のレスポンスの parse も何だか悲壮感が漂うコードだったのでちょっと直したりしつつこんな感じになりました。

# coding=utf8
import select
import socket
import struct
import time

random.seed()

# Google 使っちゃうよ
DNSSERVER = '8.8.8.8'
PORT = 53


class Resolver(object):
  queue = set()  # 未処理のもの入れ
  results = {}   # 結果入れ
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  fd = sock.fileno()

  def set(self, fqdn):
    """登録して、溜まってたら処理する"""

    if fqdn not in self.results:
      qdata = self._query(fqdn)
      self.sock.sendto(qdata, (DNSSERVER, PORT))
      self.results[fqdn] = None
      self.queue.add(fqdn)
      if len(self.queue) > 30:
        self.sweep()

  def get(self, fqdn, timeout=10):
    """溜まってるの処理して質問に答える"""

    if self.queue:
      self.sweep(timeout)
    return self.results.get(fqdn)

  def sweep(self, timeout=10):
    """溜まってるの処理する"""

    start = time.time()
    while self.queue:
      if len(select.select([self.fd], [], [], 0.001)[0]) > 0:
        # 受けとれるデータがあったら処理して結果に入れる
        rdata = self.sock.recvfrom(8192)[0]
        # qr は response なら 1
        qr = ord(rdata[3]) >> 7
        # rcode は成功なら 0
        rcode = ord(rdata[4]) & 31

        # response で成功なら parse する
        if (qr, rcode) == (1, 0):
          rfqdn, response = DNSResponse(rdata).parse()
          self.queue.remove(rfqdn)
          self.results[rfqdn] = response
      if time.time() - start > timeout:
        # 時間切れは終了
        break
    for q in self.queue:
      del self.results[q]
    self.queue.clear()

  def _query(self, fqdn):
    """fqdn の IP address を下さいという query を作る"""

    fqdn = fqdn.encode('utf-8')
    qid = struct.pack('>H', random.randint(0, 2 ** 16 - 1))
    header = qid + b'\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'

    # d.hatena.ne.jp -> \x01d\x06hatena\x02ne\x02jp\x00
    qname = b''.join([chr(len(x)) + x for x in fqdn.split(b'.')]) + b'\x00'
    # 1 が "a host address"
    qtype = struct.pack('>H', 1)
    # 1 が "the Internet"
    qclass = struct.pack('>H', 1)

    return header + qname + qtype + qclass


class DNSResponse(object):
  def __init__(self, data):
    self.data = data
    self.pos = 0

  def parse(self):
    """DNS response をパースして list of tuples で返す

    data -> [(name, type, class ttl, response_data)]
    """
    result = []
    self.pos = 12  # これ hard coding でいいの?

    # response には query が含まれてるのね
    qname = self._pick_name()
    qtype = self._uint16()
    qclass = self._uint16()

    while True:
      # response data を喰いつくす、全部読んだら終わり
      if len(self.data) <= self.pos:
        break

      rec_name = self._pick_name()
      rec_type = self._uint16()
      rec_class = self._uint16()
      rec_ttl = self._uint32()
      rec_dlength = self._uint16()

      if rec_type == 1:
        # A レコードだったら
        rec_data = self._parse_ipv4addr()
      elif rec_type == 5:
        # CNAME レコードだったら
        rec_data = self._pick_name()
      elif rec_type == 6:
        # SOA レコードだったら
        name = self._pick_name()
        rname = self._pick_name()
        serial = self._uint32()
        refresh = self._uint32()
        retry = self._uint32()
        expire = self._uint32()
        minimum = self._uint32()
        rec_data = (name, rname, serial, refresh, retry, expire)
      else:
        # それ以外だったら parse しないで bytes として
        rec_data = data[self.pos:self.pos + rec_dlength]
        self.pos += rec_dlength
      result.append((rec_name, rec_type, rec_class, rec_ttl, rec_data))

    return qname, result

  def _pick_name(self):
    """self.data の self.pos bytes 目から始まる名前を拾う"""

    name_list = []
    while True:
      length = self._uint8()
      if length == 0:
        break
      elif length >= 192:
        # length (2 octets) の頭が 11 だと compressed なので別処理
        name_pos = struct.unpack(
            '>H', self.data[self.pos - 1:self.pos + 1])[0] % (3 << 14)
        pos, self.pos = self.pos, name_pos
        name = self._pick_name()
        self.pos = pos + 1
        name_list.append(name)
        break
      else:
        name_list.append(self.data[self.pos:self.pos + length])
        self.pos += length

    return b'.'.join(name_list)

  def _uint8(self):
    """self.data の self.pos bytes 目から 1 byte 読んで self.pos をずらす"""
    result = struct.unpack('>B', self.data[self.pos:self.pos + 1])[0]
    self.pos += 1
    return result

  def _uint16(self):
    """self.data の self.pos bytes 目から 2 byte 読んで self.pos をずらす"""
    result = struct.unpack('>H', self.data[self.pos:self.pos + 2])[0]
    self.pos += 2
    return result

  def _uint32(self):
    """self.data の self.pos bytes 目から 4 byte 読んで self.pos をずらす"""
    result = struct.unpack('>I', self.data[self.pos:self.pos + 4])[0]
    self.pos += 4
    return result

  def _parse_ipv4addr(self):
    """self.data の self.pos bytes 目から 4 byte 読んで self.pos をずらす

    返り値は IP address としての str
    """
    result = '.'.join([str(ord(x)) for x in self.data[self.pos:self.pos + 4]])
    self.pos += 4
    return result


if __name__ == '__main__':
  import sys

  resolver = Resolver()
  for fqdn in sys.argv[1:]:
    resolver.set(fqdn)
  for fqdn in sys.argv[1:]:
    print(resolver.get(fqdn))

でね、なんか大量に FQDN 欲しかったんですけど、はてなブックマークのトップからもらうことにしました。こんなの書いてみました。え? timeit? なにそれ??

import re
import time
import urllib

import resolve


resolver = resolve.Resolver()
hatebu = urllib.urlopen('http://b.hatena.ne.jp/')
fqdns = set()
for line in hatebu:
  m = re.search(r'https?://([-a-zA-Z0-9\.]+)/', line)
  if m:
    fqdns.add(m.group(1))

print len(fqdns)
print "start"
start = time.time()

for fqdn in fqdns:
  resolver.set(fqdn)
#for fqdn in fqdns:
  resolver.get(fqdn)

print time.time() - start

はてぶから HTML 取って正規表現で URL っぽいの抜いて set に入れて、そいつら Resolver に登録して結果取得してます。

1 つ for がコメントアウトしてあるんで、このままだと 1 つ 1 つ登録しては問い合わせていて、結果こんな感じに

$ python test.py
82
start
7.79730296135

うちからだと 8.8.8.8 へのリクエストがだいたい 0.09 秒くらいで返ってくるので、まぁそんなもんかと。

で、この for 生かして、全部 set してから get するとこんな感じ

$ python test.py
82
start
0.363145112991

なかなか速くなりました。待ち時間を有効に使えてる感じが体感できます。

問題は、何の役に立つんだこれ。バカなの? バカなの?