我一直在阅读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(我稍后可能会通过一些信号来解决这个问题),但因为这是一个运行时很短的应用程序(它是一个安装程序)所以这是可以接受的(至少可以容忍)
答案 0 :(得分:2)
手动复制并不是特别困难。如果您不确定如何正确实施它,那么io包中只有几十行可以根据您的需要进行复制和修改(我只删除了ErrShortWrite
条款,因为我们可以假设std库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
,因为在我看来,如果定时器在调用Reset时触发,它就足够接近取消,但是通常很好的代码是惯用的如果将来扩展此代码。