最近做网络课的实验,涉及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 污染,请参考此文。