作者:張富春(ahfuzhang),轉載時請註明作者和引用鏈接,謝謝! cnblogs博客 zhihu Github 公眾號:一本正經的瞎扯 團隊中之前的文件下載做得比較複雜,因為擔心量太大,是後臺做非同步的下載,最終生成文件,傳送文件到CDN伺服器,最後再告訴用戶下載鏈接。 其實在查詢介面中就可以實 ...
作者:張富春(ahfuzhang),轉載時請註明作者和引用鏈接,謝謝!
團隊中之前的文件下載做得比較複雜,因為擔心量太大,是後臺做非同步的下載,最終生成文件,傳送文件到CDN伺服器,最後再告訴用戶下載鏈接。
其實在查詢介面中就可以實現流式下載,這樣查詢介面和下載介面可以合二為一,更加簡單。
下麵是我的demo:
1.建立一個download_file
的文件夾作為項目文件夾
go mod init download_file
2.生成go.mod文件,並準備對應的包:
go get github.com/gin-gonic/gin@latest
go get github.com/gin-contrib/gzip
go.mod文件內容如下:
module download_file
go 1.17
require github.com/gin-gonic/gin v1.8.1
require (
github.com/gin-contrib/gzip v0.0.6 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
3.main.go文件:
3.1 初始化gin框架
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
engine := gin.New()
// engine.Use(gzip.Gzip(gzip.DefaultCompression)) //如果需要開啟gzip壓縮,取消這一行的註釋
engine.Handle("POST", "/query", downloadFile) // 假定查詢和下載介面都是這條介面實現
engine.Handle("GET", "/", homepage)
engine.Run(":8080")
}
3.2 下載鏈接頁,模擬post到新視窗的場景
func homepage(ctx *gin.Context) {
ctx.Header("Content-Type", "text/html")
ctx.Writer.WriteString(`
<html>
<body>
open window and to download:
<a href="javascript:download()">download</a>
<script>
function download(){
var handle = window.open("about:blank", "my_download_window");
document.forms[0].target = "my_download_window";
document.forms[0].json.value="ahfu test";
document.forms[0].submit();
}
</script>
<form action="/query" method="POST" enctype="multipart/form-data">
<input type="hidden" name="json" value=""/>
</form>
</body>
</html>
`)
}
點擊鏈接後,彈出新視窗,在新視窗中POST json數據
3.3 流式下載功能
func downloadFile(ctx *gin.Context) {
reqData, has := ctx.GetPostForm("json")
if !has {
ctx.Data(400, "text/plain","not found json form data")
return
}
// 此處省略查詢的業務邏輯
// todo:
// 下麵開始下載的準備
ctx.Writer.WriteHeader(200)
ctx.Header("Content-Type", "text/plain; charset=utf-8")
ctx.Header("Transfer-Encoding", "chunked") // 告訴瀏覽器,分段的流式的輸出數據
// ctx.Header("Content-Encoding", "gzip") // 輸出不是gzip內容,又加上這個頭,瀏覽器會拒收。這裡是個實驗,不要加這行代碼
now := time.Now()
fileName := now.Format("20060102_150405.csv")
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) // 設置下載的文件名
ctx.Writer.WriteHeaderNow()
// 下麵模擬一個周期非常長的數據處理和下載過程
for i := 0; i < 100; i++ {
ctx.Writer.WriteString("\"")
ctx.Writer.WriteString(str)
ctx.Writer.WriteString("\"\t")
ctx.Writer.WriteString("\"")
ctx.Writer.WriteString(time.Now().Format("2006-01-02 15:04:05"))
ctx.Writer.WriteString("\"\n")
ctx.Writer.Flush() // 產生一定的數據後, flush到瀏覽器端
time.Sleep(time.Duration(500) * time.Millisecond)
}
}
打開瀏覽器,輸入:http://127.0.0.1:8080
然後點擊鏈接,過一會兒後會出現文件下載框。點擊保存後,可以看見陸續下載文件的過程。
註意:為什麼過了一會兒才出現文件下載框?這是由於瀏覽器的緩衝機制導致的。如果一開始下載的位元組數很多,就會很快出現下載框
3.4 啟用gzip壓縮
大流量的文本下載,可能很占帶寬,我們可以開啟GZIP壓縮:
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
engine := gin.New()
engine.Use(gzip.Gzip(gzip.DefaultCompression)) //如果需要開啟gzip壓縮,取消這一行的註釋
engine.Handle("POST", "/query", downloadFile) // 假定查詢和下載介面都是這條介面實現
engine.Handle("GET", "/", homepage)
engine.Run(":8080")
}
gin框架中已經提供gzip壓縮的能力。
3.5 完整代碼:
// main.go
package main
import (
"fmt"
"log"
"time"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
)
func useGzip(engine *gin.Engine) {
engine.Use(gzip.Gzip(gzip.DefaultCompression))
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
engine := gin.New()
// engine.Use(gzip.Gzip(gzip.DefaultCompression)) //如果需要開啟gzip壓縮,取消這一行的註釋
engine.Handle("POST", "/query", downloadFile)
engine.Handle("GET", "/", homepage)
engine.Run(":8080")
}
func downloadFile(ctx *gin.Context) {
reqData, has := ctx.GetPostForm("json")
if !has {
ctx.Data(400, "text/plain","not found json form data")
return
}
// 此處省略查詢的業務邏輯
// todo:
// 下麵開始下載的準備
ctx.Writer.WriteHeader(200)
ctx.Header("Content-Type", "text/plain; charset=utf-8")
ctx.Header("Transfer-Encoding", "chunked") // 告訴瀏覽器,分段的流式的輸出數據
// ctx.Header("Content-Encoding", "gzip") // 輸出不是gzip內容,又加上這個頭,瀏覽器會拒收。這裡是個實驗,不要加這行代碼
now := time.Now()
fileName := now.Format("20060102_150405.csv")
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) // 設置下載的文件名
ctx.Writer.WriteHeaderNow()
// 下麵模擬一個周期非常長的數據處理和下載過程
for i := 0; i < 100; i++ {
ctx.Writer.WriteString("\"")
ctx.Writer.WriteString(str)
ctx.Writer.WriteString("\"\t")
ctx.Writer.WriteString("\"")
ctx.Writer.WriteString(time.Now().Format("2006-01-02 15:04:05"))
ctx.Writer.WriteString("\"\n")
ctx.Writer.Flush() // 產生一定的數據後, flush到瀏覽器端
time.Sleep(time.Duration(500) * time.Millisecond)
}
}
func homepage(ctx *gin.Context) {
ctx.Header("Content-Type", "text/html")
ctx.Writer.WriteString(`
<html>
<body>
open window and to download:
<a href="javascript:download()">download</a>
<script>
function download(){
var handle = window.open("about:blank", "my_download_window");
document.forms[0].target = "my_download_window";
document.forms[0].json.value="ahfu test";
document.forms[0].submit();
}
</script>
<form action="/query" method="POST" enctype="multipart/form-data">
<input type="hidden" name="json" value=""/>
</form>
</body>
</html>
`)
}
have fun.