2017-10-17 83 views
4

我一直在閱讀關於http請求上可用的各種超時的問題,並且他們似乎都充當請求總時間的嚴格最後期限。如何在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秒內沒有改變時,我拋出了一個命名錯誤。我通過現有的錯誤處理獲取指定的錯誤並從那裏重試下載更容易。

我可能會崩潰至少一個夠程與我的做法的背景(我以後可能會用一些信令解決這個問題),但因爲這是一個短期運行的應用程序(它的安裝程序),所以這是可以接受的(至少是容忍)

+2

你可以用你自己寫的東西來替代'io.Copy',它爲每個單獨的'Read'調用設置一個超時時間,並且通過向某個通道寫入某些東西,甚至可以通知您目前複製的數據量。 – Krom

+0

這不是一個糟糕的解決方案,感覺比我的計劃更清潔 - 感謝您的建議 – WebweaverD

+0

被警告,更換'io.Copy'要複雜得多,然後人們會認爲,甚至有競爭要做,而獲得最多不錯的屬性:[可能是相關的](https://groups.google。com/d/msg/golang-nuts/434c3YInH_M/3Qd7C0uDUqQJ) – Krom

回答

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乾脆在這裏,因爲如果定時器觸發同時呼籲復位,它已經足夠接近無論如何取消了,但通常情況下代碼是習慣性的,以防將來的代碼被擴展。

+0

感謝@JimB這個例子,我將發揮一些想法並報告。毫無疑問,這可以完成這項工作,我可以很好地實現這樣的功能,但是我認爲如果我只是使用現有的文件統計循環來檢測不活動並通過關閉反應機構,但我不得不看看這是如何表現。再次感謝您的建議。現在進行投票,我可能會接受並用最終解決方案更新這個問題。 – WebweaverD