wget在进行https下载时超时不生效问题

来源:互联网 发布:免费的代理服务器软件 编辑:程序博客网 时间:2024/06/18 03:41

wget在进行https下载时超时不生效问题

  wget是一个命令行的下载工具,可以通过–timeout设置超时时间(设置–timeout后,会同步设置三个超时值,–dns-timeout, –connect-timeout, –read-timeout),通过-t 设置重试次数(默认重试20次)。
  问题在于它似乎不生效,我通过https协议下载一个文件,然后中途把网线拔掉,wget就一直卡住,不再退出。wget版本为1.11.4.

调试

  使用strace调试了一下,发现在https下载时的系统调用如下(截取部分):

1.write(2, " ", 1)                        = 12.write(2, ".", 1)                        = 13.write(2, ".", 1)                        = 14.write(2, ".", 1)                        = 15.write(2, ".", 1)                        = 16.select(4, [3], NULL, NULL, {5, 0})      = 1 (in [3], left {4, 999971})7.read(3, "\27\3\1@ ", 5)                 = 58.read(3, "\215\302o\201\231\250\347\242WqY\336\247\200\327S\336\206i\363\10\262\345\255\230\231 G;?;\312"..., 16416) = 148019.read(3, 10.

select那里设置的5秒超时,这和我命令行设置的超时时间吻合。select返回然后开始读数据,读到第三次时卡住无响应了,是的,这时我拔掉了网线。
  然后如法炮制,我测试了http下载时的情况,然而每次http下载时,我拔网线的时候都是停在select调用上,这样自然能超时。这是巧合吗?如下是部分系统调用栈:

1.select(4, [3], NULL, NULL, {0, 950000}) = 1 (in [3], left {0, 791529})2.read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 28723.clock_gettime(CLOCK_MONOTONIC, {9281, 681190937}) = 04.write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2872) = 28725.select(4, [3], NULL, NULL, {0, 950000}) = 1 (in [3], left {0, 787252})6.read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 43087.clock_gettime(CLOCK_MONOTONIC, {9281, 846431731}) = 0

发现一个问题没有,select唤醒之后,为什么http下载时只read一次,而https下载时read多次呢?

代码层面的问题

  加载wget的源码搜了下,从sock读数据的部分大致整理如下:

1.int fd_read (int fd, char *buf, int bufsize, double timeout)2.{3.  struct transport_info *info;4.  LAZY_RETRIEVE_INFO (info);5.  if (!poll_internal (fd, info, WAIT_FOR_READ, timeout))6.    return -1;7.  if (info && info->imp->reader)8.    return info->imp->reader (fd, buf, bufsize, info->ctx);9.  else10.    return sock_read (fd, buf, bufsize);11.}12.13.static int sock_read (int fd, char *buf, int bufsize)14.{15.  int res;16.  do17.    res = read (fd, buf, bufsize);18.  while (res == -1 && errno == EINTR);19.  return res;20.}21.22.static int openssl_read (int fd, char *buf, int bufsize, void *arg)23.{24.  int ret;25.  struct openssl_transport_context *ctx = arg;26.  SSL *conn = ctx->conn;27.  do28.    ret = SSL_read (conn, buf, bufsize);29.  while (ret == -130.         && SSL_get_error (conn, ret) == SSL_ERROR_SYSCALL31.         && errno == EINTR);32.  return ret;33.}

  poll_internal层层封装后,最后调用select来等待socket数据到来(是的,读超时的实现是使用select实现,不是通过setsockopt实现),select苏醒来了数据之后开始读数据。注意一下info->imp,当http协议时它为空走普通sock_read,当https协议时,它有注册自己的实现,会真真调用openssl_read函数。该函数如上面看到的那样,调用SSL_read读数据。
  SSL_read比较特殊,基于SSL/TSL记录来读,只有至少读到一条记录大小的数据后才能进行处理(检查完整性和解密)。由于SSL/TSL记录的大小可能超过底层传输的最大数据包大小,因此在SSL_read返回前,可能要读取多个数据包。所以在strace时看到一次select后read了多次,然而网线已经拔掉,不可能read到数据,所以SSL_read会一直卡住。

1.14.2版本实现

  测了下本地ubuntu上的wget,可以超时,版本为1.14.2,下载了源码,看到SSL读操作的封装如下:

1.static int openssl_read (int fd, char *buf, int bufsize, void *arg)2.{3.  struct openssl_read_args args;4.  args.fd = fd;5.  args.buf = buf;6.  args.bufsize = bufsize;7.  args.ctx = (struct openssl_transport_context*) arg;8.9.  if (run_with_timeout(opt.read_timeout, openssl_read_callback, &args))        {10.    return -1;11.  }12.  return args.retval;13.}

  使用了run_with_timeout函数来实现SSL_read读超时(超时的实现原理为:闹钟信号+siglongjump)。strace的打印如下:

1.select(4, [3], NULL, NULL, {5, 0})      = 1 (in [3], left {4, 999997})2.rt_sigaction(SIGALRM, {0x800e3420, [ALRM], SA_RESTART}, {SIG_DFL, [ALRM], SA_RESTART}, 8) = 03.rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 04.setitimer(ITIMER_REAL, {it_interval={0, 0}, it_value={5, 0}}, NULL) = 05.read(3, "\27\3\3@\30", 5)               = 56.read(3, "tV\22\"\335\201\327!\207\355\377<\16\314\320\24\322\370\rNh\351\4k\262,\211\221\310\207{\200"..., 16408) = 46197.read(3, 0x81b73c63, 11789)              = ? ERESTARTSYS (To be restarted if SA_RESTART is set)8.--- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL} ---9.rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 010.