【问题标题】:How can I implement an inactivity timeout on an http download如何在 http 下载上实现不活动超时
【发布时间】:2017-10-19 21:57:49
【问题描述】:

我一直在阅读 http 请求上可用的各种超时,它们似乎都是请求总时间的硬性截止日期。

我正在运行 http 下载,我不想在初始握手之后实施硬超时,因为我对我的用户连接一无所知,也不想在慢速连接上超时。理想情况下,我希望在一段时间不活动后超时(当 x 秒内没有下载任何内容时)。有什么办法可以作为内置程序执行此操作,还是我必须根据说明文件来中断?

工作代码有点难以隔离,但我认为这些是相关部分,还有另一个循环来统计文件以提供进度,但我需要进行一些重构以使用它来中断下载:

// httspClientOnNetInterface returns an http client using the named network interface, (via proxy if passed)
func HttpsClientOnNetInterface(interfaceIP []byte, httpsProxy *Proxy) (*http.Client, error) {

    log.Printf("Got IP addr : %s\n", string(interfaceIP))
    // create address for the dialer
    tcpAddr := &net.TCPAddr{
        IP: interfaceIP,
    }

    // create the dialer & transport
    netDialer := net.Dialer{
        LocalAddr: tcpAddr,
    }

    var proxyURL *url.URL
    var err error

    if httpsProxy != nil {
        proxyURL, err = url.Parse(httpsProxy.String())
        if err != nil {
            return nil, fmt.Errorf("Error parsing proxy connection string: %s", err)
        }
    }

    httpTransport := &http.Transport{
        Dial:  netDialer.Dial,
        Proxy: http.ProxyURL(proxyURL),
    }

    httpClient := &http.Client{
        Transport: httpTransport,
    }

    return httpClient, nil
}

/*
StartDownloadWithProgress will initiate a download from a remote url to a local file,
providing download progress information
*/
func StartDownloadWithProgress(interfaceIP []byte, httpsProxy *Proxy, srcURL, dstFilepath string) (*Download, error) {

    // start an http client on the selected net interface
    httpClient, err := HttpsClientOnNetInterface(interfaceIP, httpsProxy)
    if err != nil {
        return nil, err
    }

    // grab the header
    headResp, err := httpClient.Head(srcURL)
    if err != nil {
        log.Printf("error on head request (download size): %s", err)
        return nil, err
    }

    // pull out total size
    size, err := strconv.Atoi(headResp.Header.Get("Content-Length"))
    if err != nil {
        headResp.Body.Close()
        return nil, err
    }
    headResp.Body.Close()

    errChan := make(chan error)
    doneChan := make(chan struct{})

    // spawn the download process
    go func(httpClient *http.Client, srcURL, dstFilepath string, errChan chan error, doneChan chan struct{}) {
        resp, err := httpClient.Get(srcURL)
        if err != nil {
            errChan <- err
            return
        }
        defer resp.Body.Close()

        // create the file
        outFile, err := os.Create(dstFilepath)
        if err != nil {
            errChan <- err
            return
        }
        defer outFile.Close()

        log.Println("starting copy")
        // copy to file as the response arrives
        _, err = io.Copy(outFile, resp.Body)

        // return err
        if err != nil {
            log.Printf("\n Download Copy Error: %s \n", err.Error())
            errChan <- err
            return
        }

        doneChan <- struct{}{}

        return
    }(httpClient, srcURL, dstFilepath, errChan, doneChan)

    // return Download
    return (&Download{
        updateFrequency: time.Microsecond * 500,
        total:           size,
        errRecieve:      errChan,
        doneRecieve:     doneChan,
        filepath:        dstFilepath,
    }).Start(), nil
}

更新 感谢所有为此提供意见的人。

我接受了 JimB 的回答,因为它似乎是一种完全可行的方法,比我选择的解决方案更通用(并且可能对在这里找到自己方式的人更有用)。

在我的例子中,我已经有一个循环监控文件大小,所以当它在 x 秒内没有改变时,我抛出了一个命名错误。通过我现有的错误处理并从那里重试下载,我更容易找到命名错误。

我可能会在后台使用我的方法使至少一个 goroutine 崩溃(稍后我可能会通过一些信号来解决此问题),但由于这是一个运行时间短的应用程序(它是一个安装程序),所以这是可以接受的(至少可以容忍)

【问题讨论】:

  • 您可以将io.Copy 替换为您自己编写的内容,这会为每个Read 调用设置超时,甚至通过将内容写入通道来通知您到目前为止复制的数据量左右。
  • 这不是一个糟糕的解决方案,感觉比我计划的要干净 - 感谢您的建议
  • 请注意,替换io.Copy 比人们想象的要复杂得多,甚至有竞争,同时获得最好的属性:might be relevant
  • 链接中的有趣讨论(并将其链接到下一个问题)。对此进行了更多研究,替换 io.Copy 肯定不是微不足道的。围绕统计监控重构我的一些代码来处理这个问题看起来要容易得多。我很惊讶这似乎没有作为标准库的一部分实现。不这样做可能是很好的技术原因,但我可以作为功能请求提交以查看反馈的内容,如果我这样做,将链接有问题。
  • 注意您的方法:假设您在每次写入时都在对文件进行 fsync,您可能正在这样做,但效率不高。如果您不同步文件,则文件中的数据在关闭之前可能根本不会更改。

标签: http go


【解决方案1】:

手动复制并不是特别困难。如果您不确定如何正确实现它,只需从 io 包中复制和修改几十行以满足您的需要(我只删除了 ErrShortWrite 子句,因为我们可以假设标准库 io.Writer实现是正确的)

这是一个类似复制工作的函数,它也接受取消上下文和空闲超时参数。每次成功读取时,它都会向取消 goroutine 发出信号以继续并启动新的计时器。

func idleTimeoutCopy(dst io.Writer, src io.Reader, timeout time.Duration,
    ctx context.Context, cancel context.CancelFunc) (written int64, err error) { 
    read := make(chan int)
    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case <-time.After(timeout):
                cancel()
            case <-read:
            }
        }
    }()

    buf := make([]byte, 32*1024)
    for {
        nr, er := src.Read(buf)
        if nr > 0 {
            read <- nr
            nw, ew := dst.Write(buf[0:nr])
            written += int64(nw)
            if ew != nil {
                err = ew
                break
            }
        }
        if er != nil {
            if er != io.EOF {
                err = er
            }
            break
        }
    }
    return written, err
}

虽然为了简洁起见,我使用了 time.After,但重复使用 Timer 会更有效。这意味着要注意使用正确的重置模式,因为Reset 函数的返回值已损坏:

    t := time.NewTimer(timeout)
    for {
        select {
        case <-ctx.Done():
            return
        case <-t.C:
            cancel()
        case <-read:
            if !t.Stop() {
                <-t.C
            }
            t.Reset(timeout)
        }
    }

你可以在这里完全跳过调用Stop,因为在我看来,如果定时器在调用重置时触发,它已经足够接近取消,但如果代码被扩展,通常最好让代码是惯用的未来。

【讨论】:

  • 感谢@JimB 的示例,我将尝试一些想法并报告回来。毫无疑问,这会完成这项工作,我可能会实现这样的事情,但我认为如果我只是使用我现有的文件统计循环来检测不活动并通过关闭来杀死副本,那么在我当前的代码的上下文中它会更干净响应正文,但我必须看看它的行为。再次感谢您的建议。现在投票赞成,我可能会接受并用我的最终解决方案更新问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-09
  • 1970-01-01
  • 2016-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多