本文来自依云's Blog,转载请注明。
最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:
q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01' s.sendto(q, ('8.8.8.8', 53))
\x07twitter\x03com\x00
就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:]) '46.82.174.68' >>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:]) '128.121.146.100'
再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01
),是收不到回答的。
有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:
#!/usr/bin/env python3 # vim:fileencoding=utf-8 from socketserver import * from socket import * import sys, os ''' 来源 http://code.google.com/p/pydnsproxy/ ''' DEF_LOCAL_HOST = '127.0.0.1' DEF_REMOTE_SERVER = '8.8.8.8' DEF_PORT = 5350 DEF_CONF_FILE = 'dnsserver.conf' DEF_TIMEOUT = 0.4 gl_remote_server = None class LocalDNSHandler(BaseRequestHandler): def setup(self): global gl_remote_server if not gl_remote_server: remote_server = DEF_REMOTE_SERVER else: remote_server = gl_remote_server self.dnsserver = (remote_server, 53) def handle(self): data, socket = self.request rspdata = self._getResponse(data) socket.sendto(rspdata, self.client_address) def _getResponse(self, data): "Send client's DNS request (data) to remote DNS server, and return its response." sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server sock.sendto(data, self.dnsserver) sock.settimeout(5) while True: try: rspdata = sock.recv(4096) break except error as e: if e.errno != 11: raise else: print("Try again") # "delicious food" for GFW: while True: sock.settimeout(DEF_TIMEOUT) try: rspdata = sock.recv(4096) print("GFWed?") except timeout: break except error as e: if e.errno != 11: raise else: print("Trying again") return rspdata class LocalDNSServer(ThreadingUDPServer): pass def main(): global gl_remote_server try: if hasattr(sys, 'frozen'): dir = os.path.dirname(sys.executable) else: dir = os.path.dirname(__file__) confFile = os.path.join(dir, DEF_CONF_FILE) f = open(confFile, 'r') dns = f.read().split('=') f.close() if len(dns) == 2: if dns[0].strip().lower() == 'dns': gl_remote_server = dns[1].strip() else: pass except: pass dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler) dnsserver.serve_forever() if __name__ == '__main__': main()
注意到和原程序不同的是,我捕获了错误号为11的socket.error
异常。这个是EAGAIN
,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:
EAGAIN or EWOULDBLOCK The socket is marked nonblocking and the receive operation would block, or a receive timeout had been set and the timeout expired before data was received. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.
不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain
这样的语句了。
2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文。
Dec 17, 2010 10:59:01 PM
感觉没什么必要的说……直接你的方法(while True)其实也挺好的……
Jan 13, 2011 08:47:25 AM
刚刚看 Ruby 教程,发现有 retry 指令!而且,循环中还有 redo/retry。
Jan 13, 2011 09:36:14 PM
啊……我晕!我想起来了,ruby继承自perl,自然perl的redo, last, next 那都是有的= =
Jan 07, 2012 08:00:49 AM
n久以前我用twitter dig测试了下,发现居然每次返回的twitter ip居然是随机的。。。什么德国的,俄罗斯的。。。。天朝就是杯具啊,还好手动修改hosts搞定