お前等の名前を一片に解決してやるわフハハハハ
大量の正引きをしたいときってありますよね? え、無いの? 大人なの? 何なの? バカなの? 死ぬの??
「DNS に名前聞くとネットワークを経由するので幾許かの時間は掛かるので、それが勿体ないって場合に対応するように非同期で名前解決して処理する」ってのは良くあるみたいなんですが、あたしがいきなりそんな難しいことして上手くできるはずがありません。
で、とりあえず、沢山の FQDN を DNS サーバに聞いてみる、ってのをやってみることに。
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
なかなか速くなりました。待ち時間を有効に使えてる感じが体感できます。
問題は、何の役に立つんだこれ。バカなの? バカなの?