Python 2.6 urlib2超时问题

时间:2013-01-02 18:21:24

标签: python timeout urllib2

似乎我无法将urllib2超时考虑在内。 我读过 - 我想 - 所有与这个主题相关的帖子似乎都没有做错。我对么? 非常感谢您的帮助。

情境:

在继续使用剩余的脚本之前,我需要检查Internet连接。 然后我写了一个函数(Net_Access),它在下面提供。

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

一些信息:

  • Ubuntu 10.04.4 LTS(运行到VirtualBox v4.2.6虚拟机,主机操作系统是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?

非常感谢。

2 个答案:

答案 0 :(得分:6)

如果你的问题是DNS查找需要永远(或者太长)在没有网络连接时超时,那么是的,这是一个已知的问题,你在urllib2内部无能为力解决这个问题。

那么,所有希望都失去了吗?好吧,不一定。

首先,让我们来看看发生了什么。最终,urlopen依赖于getaddrinfo,它(与其gethostbyname之类的亲戚)是众所周知的套接字API的一个关键部分,无法异步或中断运行(并且一些平台,它甚至不是线程安全的)。如果您想自己跟踪来源,urllib2推迟httplib创建连接,在socket上调用create_connectionsocket_getaddrinfo调用getaddrinfo _socket 3}},最终调用真正的twisted函数。这是一个臭名昭着的问题,影响到世界上每种语言编写的每个网络客户端或服务器,并没有好的,简单的解决方案。

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

或者,当然,您可以使用tornadotwisted或其他异步网络库。但显然重写所有代码以使用urllib2 HTTP客户端而不是urllib2并不是一件容易的事。

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

首先,您必须提供可超时的c-ares。您可以通过绑定ctypes或使用getaddrinfo_a访问特定于平台的API(如linux的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,或仅替换urllib2timeout中的代码

最后一个选择是将其修复到更高级别。如果您的网络内容正在后台线程中发生,您可以在整个事情上抛出更高级别的超时,如果花费超过{{1}}秒来判断它是否超时,您知道它已经

答案 1 :(得分: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.Poolpool.apply_async返回的对象具有带{timeout}参数的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'))