强制python mechanize/urllib2仅使用A请求?

11 投票
4 回答
6211 浏览
提问于 2025-04-15 17:42

这里有一个相关的问题,但我不知道怎么把答案应用到 mechanize/urllib2 上:如何强制 Python 的 httplib 库只使用 A 请求

基本上,给出这段简单的代码:

#!/usr/bin/python
import urllib2
print urllib2.urlopen('http://python.org/').read(100)

这导致 Wireshark 显示以下内容:

  0.000000  10.102.0.79 -> 8.8.8.8      DNS Standard query A python.org
  0.000023  10.102.0.79 -> 8.8.8.8      DNS Standard query AAAA python.org
  0.005369      8.8.8.8 -> 10.102.0.79  DNS Standard query response A 82.94.164.162
  5.004494  10.102.0.79 -> 8.8.8.8      DNS Standard query A python.org
  5.010540      8.8.8.8 -> 10.102.0.79  DNS Standard query response A 82.94.164.162
  5.010599  10.102.0.79 -> 8.8.8.8      DNS Standard query AAAA python.org
  5.015832      8.8.8.8 -> 10.102.0.79  DNS Standard query response AAAA 2001:888:2000:d::a2

这可是5秒的延迟

我在系统中没有启用 IPv6(gentoo 编译时使用了 USE=-ipv6),所以我觉得 Python 没有理由去尝试 IPv6 的查找。

上面提到的问题建议明确设置套接字类型为 AF_INET,这听起来不错。不过,我不知道怎么强制 urllib 或 mechanize 使用我创建的任何套接字。

编辑:我知道 AAAA 查询是问题所在,因为其他应用程序也有延迟,一旦我重新编译并禁用 IPv6,问题就解决了……除了 Python 仍然执行 AAAA 请求。

4 个回答

2

当你询问关于 python.org 的 AAAA 记录时,DNS 服务器 8.8.8.8(谷歌的 DNS)会立刻给出回复。所以,如果在你提供的追踪记录中没有看到这个回复,可能是因为这个数据包没有返回(这在使用 UDP 时是可能发生的)。如果这种丢失是随机的,那就很正常;但如果是系统性的,那就说明你的网络设置可能有问题,比如防火墙坏了,导致第一个 AAAA 回复没有回来。

5 秒的延迟是因为你的 stub resolver(一个小程序,用来处理 DNS 查询)。如果这个延迟是随机的,那可能只是运气不好,但和 IPv6 没关系,A 记录的回复也可能失败。

禁用 IPv6 听起来很奇怪,因为距离最后一个 IPv4 地址分配只有两年时间!

% dig @8.8.8.8  AAAA python.org

; <<>> DiG 9.5.1-P3 <<>> @8.8.8.8 AAAA python.org
; (1 server found)
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50323
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;python.org.                    IN      AAAA

;; ANSWER SECTION:
python.org.             69917   IN      AAAA    2001:888:2000:d::a2

;; Query time: 36 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sat Jan  9 21:51:14 2010
;; MSG SIZE  rcvd: 67
4

没有答案,但有一些数据点。DNS解析似乎是从 httplib.py 中的 HTTPConnection.connect() 开始的(在我的 Python 2.5.4 标准库中是第670行)。

代码的流程大致如下:

for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM):
    af, socktype, proto, canonname, sa = res
    self.sock = socket.socket(af, socktype, proto)
    try:
        self.sock.connect(sa)
    except socket.error, msg: 
        continue
    break

关于发生了什么,几点说明:

  • socket.getaddrinfo() 的第三个参数限制了套接字的类型——也就是区分 IPv4 和 IPv6。传入零会返回所有类型。这个零是写死在标准库里的。

  • 将主机名传入 getaddrinfo() 会触发名称解析——在我的 OS X 机器上,启用了 IPv6 后,A 和 AAAA 记录都会请求,两个答案都会立刻返回,并且都会被返回。

  • 连接的循环会尝试每个返回的地址,直到其中一个成功。

例如:

>>> socket.getaddrinfo("python.org", 80, 0, socket.SOCK_STREAM)
[
 (30, 1, 6, '', ('2001:888:2000:d::a2', 80, 0, 0)), 
 ( 2, 1, 6, '', ('82.94.164.162', 80))
]
>>> help(socket.getaddrinfo)
getaddrinfo(...)
    getaddrinfo(host, port [, family, socktype, proto, flags])
        -> list of (family, socktype, proto, canonname, sockaddr)

一些猜测:

  • 由于 getaddrinfo() 中的套接字类型是写死为零的,你无法通过 urllib 的某个支持的 API 接口来覆盖 A 和 AAAA 记录。除非 mechanize 出于其他原因自己进行名称解析,否则它也无法做到。从连接循环的结构来看,这是设计使然。

  • Python 的 socket 模块是对 POSIX 套接字 API 的一个简单封装;我 预计 它们会解析系统上可用和配置的每种类型。请再次检查 Gentoo 的 IPv6 配置。

17

我也遇到了同样的问题,这里有个不太优雅的解决办法(使用需谨慎..),是根据J.J.提供的信息做的。

这个方法基本上是强制把socket.getaddrinfo(..)中的family参数设置为socket.AF_INET,而不是使用socket.AF_UNSPEC(零,这个在socket.create_connection中似乎被用到了)。这样做不仅适用于urllib2的调用,也应该适用于所有对socket.getaddrinfo(..)的调用:

#--------------------
# do this once at program startup
#--------------------
import socket
origGetAddrInfo = socket.getaddrinfo

def getAddrInfoWrapper(host, port, family=0, socktype=0, proto=0, flags=0):
    return origGetAddrInfo(host, port, socket.AF_INET, socktype, proto, flags)

# replace the original socket.getaddrinfo by our version
socket.getaddrinfo = getAddrInfoWrapper

#--------------------
import urllib2

print urllib2.urlopen("http://python.org/").read(100)

在我这个简单的例子中,这个方法是有效的。

撰写回答