5

似乎我无法urllib2考虑超时。我确实阅读了 - 我想 - 所有与该主题相关的帖子,看来我没有做错任何事情。我对么?非常感谢您的帮助。

设想:

在继续执行剩余的脚本之前,我需要检查 Internet 连接。然后我编写了一个函数 (Net_Access),如下所示。

  • 当我在连接 LAN 或 Wifi 接口并检查现有主机名的情况下执行此代码时:一切都很好,因为没有错误或问题,因此没有超时。
  • 如果我拔下 LAN 连接器或检查不存在的主机名,超时值似乎被忽略。请问我的代码有什么问题?

一些信息:

  • Ubuntu 10.04.4 LTS(运行到 VirtualBox v4.2.6 VM,主机操作系统是 MAC OS X Lion)
  • cat /proc/sys/kernel/osrelease: 2.6.32-42-generic
  • Python 2.6.5

我的代码:

#!/usr/bin/env python

import socket
import urllib2

myhost = 'http://www.google.com'
timeout = 3

socket.setdefaulttimeout(timeout)
req = urllib2.Request(myhost)

try:
    handle = urllib2.urlopen(req, timeout = timeout)
except urllib2.URLError as e:
    socket.setdefaulttimeout(None)
    print ('[--- Net_Access() --- No network access')
else:
    print ('[--- Net_Access() --- Internet Access OK')

1) 工作,插入 LAN 连接器

$ $ time ./Net_Access 
[--- Net_Access() --- Internet Access OK

real    0m0.223s
user    0m0.060s
sys 0m0.032s

2) 超时不起作用,拔掉 LAN 连接器

$ time ./Net_Access 
[--- Net_Access() --- No network access

real    1m20.235s
user    0m0.048s
sys 0m0.060s

添加到原始帖子:测试结果(使用 IP 而不是 FQDN)

正如@unutbu(见评论)所建议的那样,用 IP 地址替换 myhost 中的 FQDN 可以解决问题:超时生效。

LAN 连接器插入...
$ time ./Net_Access [--- Net_Access() --- Internet Access OK

real    0m0.289s
user    0m0.036s
sys 0m0.040s

LAN 连接器已拔出...
$ time ./Net_Access [--- Net_Access() --- 无网络访问

real    0m3.082s
user    0m0.052s
sys 0m0.024s

这很好,但这意味着超时只能用于 IP 而不能用于 FQDN。诡异的...

是否有人找到了使用 urllib2 超时而不进入预 DNS 解析并将 IP 传递给函数的方法,或者您是否首先使用套接字测试连接,然后在确定可以到达目标时触发 urllib2?

非常感谢。

4

2 回答 2

6

如果您的问题是在没有网络连接的情况下,DNS 查找需要永远(或只是太长时间)超时,那么是的,这是一个已知问题,您无法在其内部做任何事情urllib2来解决这个问题。

那么,所有的希望都落空了吗?嗯,不一定。

首先,让我们看看发生了什么。最终,urlopen依赖getaddrinfo,它(连同它的亲戚一样gethostbyname)是众所周知的套接字 API 的一个关键部分,它不能异步运行或中断(在某些平台上,它甚至不是线程安全的)。如果你想自己追溯源头,urllib2请按照httplib创建连接,调用create_connectionon socket,调用socket_getaddrinfoon _socket,最终调用真正的getaddrinfo函数。这是一个臭名昭著的问题,它影响着世界上用每种语言编写的每个网络客户端或服务器,并且没有好的、简单的解决方案。

一种选择是使用已经解决了这个问题的不同的高级库。我相信requests依赖于urllib3最终有同样的问题,但pycurl依赖于libcurl,如果用 构建c-ares,它会异步进行名称查找,因此可以超时。

或者,当然,您可以使用类似twistedtornado其他异步网络库之类的东西。但显然重写所有代码以使用twistedHTTP 客户端而urllib2不是完全不简单。

urllib2另一种选择是通过猴子补丁标准库来“修复” 。如果你想这样做,有两个步骤。

首先,您必须提供一个 timeoutable getaddrinfo。您可以通过 bindingc-ares或使用ctypes来访问特定于平台的 API(如 linux's )来做到这一点getaddrinfo_a,甚至可以查找名称服务器并直接与它们通信。但真正简单的方法是使用线程。如果你做很多这些,你会想要使用单个线程或小线程池,但对于小规模使用,只需为每个调用分离一个线程。一个非常快速和肮脏(阅读:糟糕)的实现是:

def getaddrinfo_async(*args):
    result = None
    t = threading.Thread(target=lambda: result=socket.getaddrinfo(*args))
    t.start()
    t.join(timeout)
    if t.isAlive():
        raise TimeoutError(blahblahblah)
    return result

接下来,您必须获取您关心的所有库才能使用它。根据您希望补丁的普遍性(和危险性),您可以替换socket.getaddrinfo它自己,或者只是socket.create_connection,或者只是在httplib甚至urllib2.

最后一个选择是在更高的层次上解决这个问题。如果您的网络内容发生在后台线程上,您可以在整个事情上抛出更高级别的超时,如果花费超过timeout几秒钟来确定它是否超时,您就知道它已经超时。

于 2013-01-03T19:36:45.460 回答
2

也许试试这个:

import urllib2

def get_header(url):
    req = urllib2.Request(url)
    req.get_method = lambda : 'HEAD'
    try:
        response = urllib2.urlopen(req)
    except urllib2.URLError:
        # urllib2.URLError: <urlopen error [Errno -2] Name or service not known>
        return False
    return True

url = 'http://www.kernel.org/pub/linux/kernel/v3.0/linux-3.7.1.tar.bz2'
print(get_header(url))

当我拔下我的网络适配器时,它几乎立即打印 False,而在正常情况下,它打印 True。

我不确定为什么与您的原始代码相比,它的工作速度如此之快(即使不需要设置超时参数),但也许它也适用于您。


我今天早上做了一个实验,结果确实get_header没有立即返回。我在关闭路由器的情况下启动了计算机。然后路由器就开机了。然后通过 Ubuntu GUI 启用网络和无线。这未能建立有效的连接。在这个阶段,get_header未能立即返回。

因此,这是一个重量级的解决方案,它get_header使用multiprocessing.Pool. 返回的对象pool.apply_async有一个get带有超时参数的方法。get_header如果在 指定的持续时间内没有返回结果timeout,则终止子进程。

因此,check_http在任何情况下都应该在大约 1 秒内返回结果。

import multiprocessing as mp
import urllib2

def timeout_function(cmd, timeout = None, args = (), kwds = {}):
    pool = mp.Pool(processes = 1)
    result = pool.apply_async(cmd, args = args, kwds = kwds)
    try:
        retval = result.get(timeout = timeout)
    except mp.TimeoutError as err:
        pool.terminate()
        pool.join()
        raise
    else:
        return retval

def get_header(url):
    req = urllib2.Request(url)
    req.get_method = lambda : 'HEAD'
    try:
        response = urllib2.urlopen(req)
    except urllib2.URLError:
        return False
    return True

def check_http(url):
    try:
        response = timeout_function(
            get_header,
            args = (url, ),
            timeout = 1)
        return response
    except mp.TimeoutError:
        return False

print(check_http('http://www.google.com'))
于 2013-01-03T16:32:05.230 回答