http下载原理

下载如何实现的断点续传

1.假设服务器域名为www.test/down.zip,文件名为down.zip。

2.请求报文

Get /down.zip http/ 1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
Excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: mozilla/ 4.0  (compatible; msie  5.01 ; windows nt  5.0 )
Connection: Keep-Alive

3.服务器收到请求后,按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:

Content-Length= 106786028 #文件长度
Accept-Ranges=bytes #表示服务器支持range 网络中断后客户端下载续传,客户端下载失败就可以记录上次下载流读取位置
Date=mon,  30  apr  2001  12 : 56 : 11  gmt
Etag=w/ "02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=microsoft-iis/ 5.0
Last-Modified=mon,  30  apr  2001  12 : 56 : 11  gmt
Connection="keep-alive" #保存长连接

可以通过指定文件名字,如果未指定 则使用url 比如上面的down.zip url没有带则为默认

writer.Header().Set("Content-Disposition", "attachment; filename=RXJH_Patch_for_22000-22041.exe")

 

4.下载过程中出现网络中断

  • 当用户希望恢复下载时,客户端向服务器发送带有 Range 头部的 HTTP 请求。
  • Range 头部指定了客户端希望从哪个字节开始继续下载。通常格式为 "Range: bytes=start-end",其中 start 表示起始字节位置,end 表示结束字节位置。如果 end 未指定,表示从 start 到文件末尾的所有内容。
  • 例如,Range 头部可以是 "Range: bytes=5000-",表示从文件的第 5001 个字节(从 0 开始计数)开始继续下载。
get /down.zip http/ 1.0
user-agent: netfox
range: bytes= 5000 -
accept: text/html, image/gif, image/jpeg, *; q=. 2 , */*; q=. 2

5.服务器处理断点续传请求:

  • 服务器收到带有 Range 头部的请求后,会根据 Range 头部指定的范围,只返回该范围内的文件内容。
  • 服务器响应的状态码通常为 206 Partial Content,表示服务器成功处理了部分 GET 请求。
  • 服务器返回的响应中包含实际返回的文件内容以及其他相关信息。
HTTP/1.1 206 Partial Content
Date: Mon, 22 Feb 2024 12:00:00 GMT
Server: Apache
Last-Modified: Sun, 21 Feb 2024 12:00:00 GMT
Content-Length: 10000
Content-Range: bytes 5000-14999/20000
Content-Type: application/octet-stream

[文件内容,从第5001个字节到第15000个字节]

 

6.客户端继续下载

  • 客户端接收到服务器的响应后,将继续从断点处开始接收文件内容,并将其写入本地文件。
  • 客户端会持续接收直到文件下载完成或者再次中断。

注针对浏览器默认实现了现在的断点续传,下载带上了range 分段获取 ,如果使用java代码或者c++下载要实现此功能就要按照http协议按照以上流程实现

误区

以前下载都是直接走oss和cdn,经常开发做的下载就是导出,

假如需求是通过自己应用转发一次下载呢。以前想着是跟导出一样一次性加载到内存,然后response返回,大文件并发高了会占用大量内存

其实针对下载http是走的长连接,比如以下go的例子,自己自身只是做了个转发,然后是我们与客户端保存长连接,以及对下载源保持长连接,一边从下载源读一部分,一边写回给客户端。

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    _ "net/http/pprof"
    "net/textproto"
    "time"
)

type RateLimiterWriter struct {
    Writer io.Writer
    Rate   int64 // example: 8 Mbps = (8 * 1024 * 1024)
}

// Write implements the io.Writer interface.
func (rlw *RateLimiterWriter) Write(b []byte) (int, error) {
    <-time.After(time.Duration(len(b)*8*int(time.Second)) / time.Duration(rlw.Rate))
    return rlw.Writer.Write(b)
}

func main() {
    http.HandleFunc("/download", func(writer http.ResponseWriter, request *http.Request) {

        fileUrl := "http://downloadn1.rxjh.cdcgames.net/rxjh/rxjhclient/RXJH_Patch_for_22000-22041.exe"
        if request.Method == http.MethodHead {
            response, _ := http.Get(fileUrl)
            writer.Header().Set("Content-Length", fmt.Sprintf("%d", response.ContentLength))
            return
        }

        httpRequest, err := http.NewRequest("GET", fileUrl, nil)
        //浏览器原封不动转发 range协议
        requestHeader := textproto.MIMEHeader(request.Header)
        for k, v := range requestHeader {
            //不需要转发的head可以移除
            httpRequest.Header.Set(k, v[0])
        }
        response, err := http.DefaultClient.Do(httpRequest)
        if err != nil {
            log.Printf("http.Get() error: %v", err)
            return
        }
        responseHeader := textproto.MIMEHeader(response.Header)
        //
        for k, v := range responseHeader {
            writer.Header().Set(k, v[0])
        }
        writer.Header().Set("Content-Disposition", "attachment; filename=RXJH_Patch_for_22000-22041.exe")
        //liqiangtodo 可以加一些自定head
        defer response.Body.Close()

        limitedWriter := &RateLimiterWriter{
            Writer: writer,
            Rate:   64 * 1024 * 1024, // 64 Mbps (约8MB/s)
        }
//从源站读取,写入客户端流,这个时候客户端暂停重新下载下载都是经过这里保存的链接,当网络断开,才会重新发请求带上range written, err :
= io.Copy(limitedWriter, response.Body) fmt.Println(written, err) }) log.Printf("http://localhost:8080\n") if err := http.ListenAndServe(":8080", nil); err != nil { log.Panicf("http.ListenAndServe() error: %v", err) } }

错误例子

下面例子就是针对源站的响应自己写死了。应该转发源站的。还有就是是否支持断点续传下载的head头没有给客户端,正常应该用源站

package main  
  
import (  
    "fmt"  
    "io"    
    "log"    
    "net/http"   
    _ "net/http/pprof"  
    "time"
)  
  
type RateLimiterWriter struct {  
    Writer io.Writer  
    Rate   int64 // example: 8 Mbps = (8 * 1024 * 1024)  
}  
  
// Write implements the io.Writer interface.
func (rlw *RateLimiterWriter) Write(b []byte) (int, error) {  
    <-time.After(time.Duration(len(b)*8*int(time.Second)) / time.Duration(rlw.Rate))  
    return rlw.Writer.Write(b)  
}  
  
func main() {  
    http.HandleFunc("/download", func(writer http.ResponseWriter, request *http.Request) {  
       fileUrl := "https://url_11377219084_1691404041_1691404041_8d5501a50969ccca2af6b7f644796e7689e44aa6.mp4?OSSAccessKeyId=LTAI5t5owzQFFZA634E8UYYR&Expires=2693889016&Signature=9heet1maiZadPPkbxil1fzHie90%3D"  
  
       if request.Method == http.MethodHead {  
          response, _ := http.Get(fileUrl)  
          writer.Header().Set("Content-Length", fmt.Sprintf("%d", response.ContentLength))  
          return  
       }  
  
       httpRequest, err := http.NewRequest("GET", fileUrl, nil)  
       httpRange := request.Header.Get("Range")  
       if httpRange != "" {  
          httpRequest.Header.Set("Range", httpRange)  
       }  
       response, err := http.DefaultClient.Do(httpRequest)  
  
       if err != nil {  
          log.Printf("http.Get() error: %v", err)  
          return  
       }  
       if httpRange != "" {  
          writer.WriteHeader(http.StatusPartialContent)  
          writer.Header().Set("Accept-Ranges", response.Header.Get("Accept-Ranges"))  
          writer.Header().Set("Content-Range", response.Header.Get("Content-Range"))  
       }  
       //文件大小
       writer.Header().Set("Content-Length", fmt.Sprintf("%d", response.ContentLength))  
       //告知浏览器是字节流
       writer.Header().Set("Content-Type", "application/octet-stream")  
       //描述文件类型
       writer.Header().Set("Content-Disposition", "attachment; filename=ewei-itkeeping-help.pdf")  
  
       defer response.Body.Close()  
  
       limitedWriter := &RateLimiterWriter{  
          Writer: writer,  
          Rate:   64 * 1024 * 1024, // 64 Mbps (约8MB/s)  
       }  
       written, err := io.Copy(limitedWriter, response.Body)  
       fmt.Println(written, err)  
    })  
  
    log.Printf("http://localhost:8080\n")  
    if err := http.ListenAndServe(":8080", nil); err != nil {  
       log.Panicf("http.ListenAndServe() error: %v", err)  
    }  
}

 

posted @ 2024-02-22 22:45  意犹未尽  阅读(43)  评论(0编辑  收藏  举报