Gin實現(xiàn)大文件分片上傳與斷點續(xù)傳過程
在Web開發(fā)中,大文件上傳是常見的需求場景(如視頻、壓縮包、大型數(shù)據(jù)集等),但傳統(tǒng)的單文件上傳方式存在諸多問題:網(wǎng)絡(luò)中斷導(dǎo)致上傳失敗需從頭重傳、單次請求體過大引發(fā)超時、服務(wù)器內(nèi)存占用過高易觸發(fā)OOM等。為解決這些問題,分片上傳+斷點續(xù)傳 成為主流解決方案。
本文將基于Go語言的Gin Web框架,從零實現(xiàn)大文件分片上傳與斷點續(xù)傳功能,并結(jié)合實際場景拓展相關(guān)優(yōu)化思路。
一、核心原理剖析
在編寫代碼前,先理清分片上傳和斷點續(xù)傳的核心邏輯,這是實現(xiàn)的基礎(chǔ):
1. 分片上傳(Chunked Upload)
將一個大文件按照固定大?。ㄈ?MB)拆分成多個小分片(Chunk),客戶端逐個將分片上傳至服務(wù)端;
服務(wù)端接收所有分片后,按照分片的索引順序?qū)⑵浜喜橥暾脑嘉募?/p>
2. 斷點續(xù)傳(Resumable Upload)
基于分片上傳的基礎(chǔ),為每個文件生成唯一標(biāo)識(通常是文件內(nèi)容的MD5值,避免文件名重復(fù)導(dǎo)致的沖突)。
客戶端上傳前先向服務(wù)端查詢該文件已上傳的分片列表,僅上傳未完成的分片;服務(wù)端則記錄每個文件的分片上傳狀態(tài),確保斷點續(xù)傳的可行性。
3. 核心流程
- 客戶端:計算文件MD5 → 拆分文件為分片 → 查詢服務(wù)端已上傳分片 → 上傳未完成分片 → 全部上傳完成后請求服務(wù)端合并分片。
- 服務(wù)端:提供「分片查詢接口」→ 提供「分片接收接口」→ 提供「分片合并接口」→ 合并完成后清理臨時分片文件。
二、技術(shù)選型與環(huán)境準(zhǔn)備
1. 技術(shù)棧
- 服務(wù)端:Go 1.20+ + Gin框架(輕量、高性能,適合Web API開發(fā))。
- 客戶端:Go編寫簡易客戶端(也可替換為前端JS/TS,原理一致)。
- 存儲:本地文件系統(tǒng)(臨時存儲分片,合并后存儲完整文件;生產(chǎn)環(huán)境可替換為MinIO、S3等對象存儲)。
2. 環(huán)境初始化
首先創(chuàng)建項目并安裝Gin依賴:
# 創(chuàng)建項目目錄 mkdir gin-large-file-upload && cd gin-large-file-upload # 初始化Go模塊 go mod init gin-large-file-upload # 安裝Gin go get github.com/gin-gonic/gin
三、服務(wù)端實現(xiàn)
服務(wù)端需要實現(xiàn)三個核心接口:分片查詢、分片上傳、分片合并。同時需處理臨時文件存儲、分片狀態(tài)記錄、文件合并等邏輯。
1. 全局配置與工具函數(shù)
先定義全局配置(如分片大小、臨時目錄、目標(biāo)文件目錄)和通用工具函數(shù)(如生成文件MD5、檢查目錄是否存在):
// main.go
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
// 全局配置
const (
ChunkSize = 5 * 1024 * 1024 // 分片大小:5MB
TempDir = "./temp_chunks" // 分片臨時存儲目錄
UploadDir = "./upload_files" // 最終文件存儲目錄
MaxFileSize = 100 * 1024 * 1024 // 最大文件限制:100MB
)
// 初始化目錄(確保臨時目錄和上傳目錄存在)
func initDir() error {
if err := os.MkdirAll(TempDir, 0755); err != nil {
return fmt.Errorf("創(chuàng)建臨時目錄失?。?v", err)
}
if err := os.MkdirAll(UploadDir, 0755); err != nil {
return fmt.Errorf("創(chuàng)建上傳目錄失?。?v", err)
}
return nil
}
// 計算文件MD5(用于文件唯一標(biāo)識)
func calculateFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// 獲取分片文件的存儲路徑
func getChunkPath(fileMD5 string, chunkIndex int) string {
return filepath.Join(TempDir, fmt.Sprintf("%s_%d", fileMD5, chunkIndex))
}
// 檢查分片是否已上傳
func isChunkExist(fileMD5 string, chunkIndex int) bool {
chunkPath := getChunkPath(fileMD5, chunkIndex)
_, err := os.Stat(chunkPath)
return err == nil // 無錯誤則表示文件存在
}
// 檢查所有分片是否上傳完成
func isAllChunksUploaded(fileMD5 string, totalChunks int) bool {
for i := 0; i < totalChunks; i++ {
if !isChunkExist(fileMD5, i) {
return false
}
}
return true
}
2. 核心接口實現(xiàn)
(1)分片查詢接口
客戶端上傳前調(diào)用該接口,查詢指定文件的已上傳分片列表,返回未上傳的分片索引。
// 查詢已上傳分片
func checkChunkHandler(c *gin.Context) {
// 獲取請求參數(shù)
fileMD5 := c.Query("file_md5")
totalChunksStr := c.Query("total_chunks")
if fileMD5 == "" || totalChunksStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "file_md5和total_chunks為必傳參數(shù)",
})
return
}
// 轉(zhuǎn)換總分片數(shù)為整數(shù)
totalChunks, err := strconv.Atoi(totalChunksStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "total_chunks必須為整數(shù)",
})
return
}
// 收集已上傳的分片索引
uploadedChunks := make([]int, 0)
for i := 0; i < totalChunks; i++ {
if isChunkExist(fileMD5, i) {
uploadedChunks = append(uploadedChunks, i)
}
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "查詢成功",
"uploaded_chunks": uploadedChunks,
})
}
(2)分片上傳接口
接收客戶端上傳的分片文件,保存至臨時目錄,并返回上傳結(jié)果。
// 上傳分片
func uploadChunkHandler(c *gin.Context) {
// 解析表單(分片文件+參數(shù))
file, fileHeader, err := c.Request.FormFile("chunk")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "獲取分片文件失?。? + err.Error(),
})
return
}
defer file.Close()
// 獲取其他參數(shù)
fileMD5 := c.PostForm("file_md5")
chunkIndexStr := c.PostForm("chunk_index")
fileName := c.PostForm("file_name")
if fileMD5 == "" || chunkIndexStr == "" || fileName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "file_md5、chunk_index、file_name為必傳參數(shù)",
})
return
}
// 轉(zhuǎn)換分片索引為整數(shù)
chunkIndex, err := strconv.Atoi(chunkIndexStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "chunk_index必須為整數(shù)",
})
return
}
// 檢查分片大?。蛇x,防止客戶端上傳過大分片)
if fileHeader.Size > ChunkSize {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": fmt.Sprintf("分片大小不能超過%dMB", ChunkSize/1024/1024),
})
return
}
// 創(chuàng)建分片文件并寫入內(nèi)容
chunkPath := getChunkPath(fileMD5, chunkIndex)
chunkFile, err := os.Create(chunkPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "創(chuàng)建分片文件失?。? + err.Error(),
})
return
}
defer chunkFile.Close()
// 讀取分片內(nèi)容并寫入文件(使用緩沖區(qū),避免內(nèi)存溢出)
buf := make([]byte, 1024*1024) // 1MB緩沖區(qū)
_, err = io.CopyBuffer(chunkFile, file, buf)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "寫入分片文件失?。? + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "分片上傳成功",
"data": gin.H{
"file_md5": fileMD5,
"chunk_index": chunkIndex,
},
})
}
(3)分片合并接口
客戶端確認(rèn)所有分片上傳完成后,調(diào)用該接口,服務(wù)端將所有分片按索引合并為完整文件。
// 合并分片
func mergeChunkHandler(c *gin.Context) {
// 獲取請求參數(shù)
fileMD5 := c.PostForm("file_md5")
totalChunksStr := c.PostForm("total_chunks")
fileName := c.PostForm("file_name")
if fileMD5 == "" || totalChunksStr == "" || fileName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "file_md5、total_chunks、file_name為必傳參數(shù)",
})
return
}
// 轉(zhuǎn)換總分片數(shù)為整數(shù)
totalChunks, err := strconv.Atoi(totalChunksStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "total_chunks必須為整數(shù)",
})
return
}
// 檢查所有分片是否上傳完成
if !isAllChunksUploaded(fileMD5, totalChunks) {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "存在未上傳的分片,無法合并",
})
return
}
// 生成最終文件路徑(避免文件名重復(fù),拼接MD5前綴)
ext := filepath.Ext(fileName)
baseName := strings.TrimSuffix(fileName, ext)
finalFileName := fmt.Sprintf("%s_%s%s", baseName, fileMD5, ext)
finalFilePath := filepath.Join(UploadDir, finalFileName)
// 創(chuàng)建最終文件
finalFile, err := os.Create(finalFilePath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "創(chuàng)建最終文件失?。? + err.Error(),
})
return
}
defer finalFile.Close()
// 按索引順序合并所有分片
buf := make([]byte, 1024*1024) // 1MB緩沖區(qū)
for i := 0; i < totalChunks; i++ {
chunkPath := getChunkPath(fileMD5, i)
chunkFile, err := os.Open(chunkPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": fmt.Sprintf("打開分片%d失敗:%v", i, err),
})
return
}
// 將分片內(nèi)容寫入最終文件
_, err = io.CopyBuffer(finalFile, chunkFile, buf)
chunkFile.Close() // 及時關(guān)閉分片文件
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": fmt.Sprintf("合并分片%d失?。?v", i, err),
})
return
}
// 刪除已合并的分片文件(可選,也可定時清理)
os.Remove(chunkPath)
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "文件合并成功",
"data": gin.H{
"file_name": finalFileName,
"file_path": finalFilePath,
},
})
}
3. 注冊路由并啟動服務(wù)
將上述接口注冊到Gin路由,并啟動服務(wù):
func main() {
// 初始化目錄
if err := initDir(); err != nil {
fmt.Printf("目錄初始化失敗:%v\n", err)
return
}
// 創(chuàng)建Gin引擎
r := gin.Default()
// 注冊路由
r.GET("/check-chunk", checkChunkHandler) // 分片查詢
r.POST("/upload-chunk", uploadChunkHandler) // 分片上傳
r.POST("/merge-chunk", mergeChunkHandler) // 分片合并
// 啟動服務(wù)
fmt.Println("服務(wù)啟動成功,監(jiān)聽端口:8080")
if err := r.Run(":8080"); err != nil {
fmt.Printf("服務(wù)啟動失?。?v\n", err)
}
}
四、客戶端實現(xiàn)
為了驗證服務(wù)端功能,編寫一個簡易的Go客戶端,實現(xiàn)文件分片、斷點續(xù)傳、合并請求的邏輯:
// client.go
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
const (
ServerAddr = "http://127.0.0.1:8080" // 服務(wù)端地址
ClientChunkSize = 5 * 1024 * 1024 // 分片大?。ㄅc服務(wù)端一致)
)
// 計算文件MD5(同服務(wù)端邏輯)
func calcFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// 查詢已上傳分片
func checkUploadedChunks(fileMD5 string, totalChunks int) ([]int, error) {
// 構(gòu)造請求URL
url := fmt.Sprintf("%s/check-chunk?file_md5=%s&total_chunks=%d", ServerAddr, fileMD5, totalChunks)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 解析響應(yīng)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
UploadedChunks []int `json:"uploaded_chunks"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if result.Code != 200 {
return nil, fmt.Errorf("查詢失敗:%s", result.Message)
}
return result.UploadedChunks, nil
}
// 上傳單個分片
func uploadChunk(filePath string, fileMD5 string, chunkIndex int, chunkData []byte, fileName string) error {
// 構(gòu)造multipart/form-data請求
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加分片文件
part, err := writer.CreateFormFile("chunk", fmt.Sprintf("chunk_%d", chunkIndex))
if err != nil {
return err
}
_, err = part.Write(chunkData)
if err != nil {
return err
}
// 添加其他參數(shù)
_ = writer.WriteField("file_md5", fileMD5)
_ = writer.WriteField("chunk_index", strconv.Itoa(chunkIndex))
_ = writer.WriteField("file_name", fileName)
// 關(guān)閉writer,完成表單構(gòu)造
writer.Close()
// 發(fā)送POST請求
url := fmt.Sprintf("%s/upload-chunk", ServerAddr)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 解析響應(yīng)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Code != 200 {
return fmt.Errorf("分片%d上傳失?。?s", chunkIndex, result.Message)
}
fmt.Printf("分片%d上傳成功\n", chunkIndex)
return nil
}
// 請求合并分片
func mergeChunks(fileMD5 string, totalChunks int, fileName string) error {
// 構(gòu)造請求參數(shù)
data := strings.NewReader(fmt.Sprintf("file_md5=%s&total_chunks=%d&file_name=%s", fileMD5, totalChunks, fileName))
url := fmt.Sprintf("%s/merge-chunk", ServerAddr)
req, err := http.NewRequest("POST", url, data)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 解析響應(yīng)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
FileName string `json:"file_name"`
FilePath string `json:"file_path"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Code != 200 {
return fmt.Errorf("合并分片失?。?s", result.Message)
}
fmt.Printf("文件合并成功!最終文件:%s,路徑:%s\n", result.Data.FileName, result.Data.FilePath)
return nil
}
// 大文件上傳主邏輯
func uploadLargeFile(filePath string) error {
// 1. 打開文件并獲取基本信息
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return err
}
fileSize := fileInfo.Size()
fileName := filepath.Base(filePath)
// 2. 計算文件MD5
fmt.Println("正在計算文件MD5...")
fileMD5, err := calcFileMD5(filePath)
if err != nil {
return fmt.Errorf("計算MD5失?。?v", err)
}
fmt.Printf("文件MD5:%s,大?。?dMB\n", fileMD5, fileSize/1024/1024)
// 3. 計算總分片數(shù)
totalChunks := int(fileSize / ClientChunkSize)
if fileSize%ClientChunkSize != 0 {
totalChunks += 1
}
fmt.Printf("文件將拆分為%d個分片,每個分片%dMB\n", totalChunks, ClientChunkSize/1024/1024)
// 4. 查詢已上傳分片
fmt.Println("查詢已上傳分片...")
uploadedChunks, err := checkUploadedChunks(fileMD5, totalChunks)
if err != nil {
return fmt.Errorf("查詢已上傳分片失?。?v", err)
}
fmt.Printf("已上傳分片:%v\n", uploadedChunks)
// 5. 遍歷所有分片,上傳未完成的分片
buf := make([]byte, ClientChunkSize)
for i := 0; i < totalChunks; i++ {
// 跳過已上傳的分片
skip := false
for _, idx := range uploadedChunks {
if idx == i {
skip = true
break
}
}
if skip {
fmt.Printf("分片%d已上傳,跳過\n", i)
continue
}
// 讀取分片數(shù)據(jù)
offset := int64(i) * ClientChunkSize
_, err := file.Seek(offset, io.SeekStart)
if err != nil {
return fmt.Errorf("讀取分片%d失?。?v", i, err)
}
n, err := file.Read(buf)
if err != nil && err != io.EOF {
return fmt.Errorf("讀取分片%d失敗:%v", i, err)
}
// 上傳分片
if err := uploadChunk(filePath, fileMD5, i, buf[:n], fileName); err != nil {
return err
}
}
// 6. 所有分片上傳完成,請求合并
fmt.Println("所有分片上傳完成,請求合并...")
if err := mergeChunks(fileMD5, totalChunks, fileName); err != nil {
return err
}
return nil
}
func main() {
// 替換為你要上傳的大文件路徑
filePath := "./test_large_file.mp4"
if err := uploadLargeFile(filePath); err != nil {
fmt.Printf("文件上傳失?。?v\n", err)
}
}
五、功能測試
1. 啟動服務(wù)端
go run main.go
輸出「服務(wù)啟動成功,監(jiān)聽端口:8080」表示服務(wù)端啟動正常。
2. 運(yùn)行客戶端
將client.go中的filePath替換為本地的大文件路徑(如視頻、壓縮包),然后運(yùn)行:
go run client.go
客戶端會輸出如下日志,表明上傳流程正常:
正在計算文件MD5... 文件MD5:xxxxxx,大?。?0MB 文件將拆分為4個分片,每個分片5MB 查詢已上傳分片... 已上傳分片:[] 分片0上傳成功 分片1上傳成功 分片2上傳成功 分片3上傳成功 所有分片上傳完成,請求合并... 文件合并成功!最終文件:test_large_file_xxxxxx.mp4,路徑:./upload_files/test_large_file_xxxxxx.mp4
3. 斷點續(xù)傳測試
手動中斷客戶端上傳(如按Ctrl+C),重新運(yùn)行客戶端,會看到客戶端跳過已上傳的分片,僅上傳未完成的分片:
正在計算文件MD5... 文件MD5:xxxxxx,大小:20MB 文件將拆分為4個分片,每個分片5MB 查詢已上傳分片... 已上傳分片:[0 1] 分片0已上傳,跳過 分片1已上傳,跳過 分片2上傳成功 分片3上傳成功 所有分片上傳完成,請求合并... 文件合并成功!最終文件:test_large_file_xxxxxx.mp4,路徑:./upload_files/test_large_file_xxxxxx.mp4
六、進(jìn)階拓展與優(yōu)化
上述實現(xiàn)滿足了基礎(chǔ)的分片上傳和斷點續(xù)傳需求,但在生產(chǎn)環(huán)境中,還需要考慮以下優(yōu)化方向:
1. 分片大小動態(tài)調(diào)整
固定5MB分片大小并非適用于所有場景:
- 分片過?。涸黾親TTP請求次數(shù),加重服務(wù)端負(fù)擔(dān);
- 分片過大:單分片上傳超時風(fēng)險高。
可根據(jù)文件大小動態(tài)調(diào)整分片大小(如100MB以內(nèi)用5MB分片,1GB以內(nèi)用10MB分片),或允許客戶端自定義分片大?。ǚ?wù)端做上限限制)。
2. 并發(fā)上傳分片
客戶端可通過goroutine并發(fā)上傳多個分片,提升上傳速度。需注意控制并發(fā)數(shù)(如5-10個),避免請求過多導(dǎo)致服務(wù)端壓力過大:
// 并發(fā)上傳示例(客戶端)
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 限制5個并發(fā)
for i := 0; i < totalChunks; i++ {
// 跳過已上傳分片...
wg.Add(1)
sem <- struct{}{}
go func(idx int) {
defer wg.Done()
defer func() { <-sem }()
// 讀取分片并上傳...
}(i)
}
wg.Wait()
3. 臨時分片文件清理
服務(wù)端的臨時分片文件如果長時間未合并(如客戶端中斷上傳),會占用磁盤空間??商砑佣〞r任務(wù)(如每小時)清理超過24小時未合并的分片文件:
// 定時清理臨時分片文件
func cleanExpiredChunks() {
ticker := time.NewTicker(1 * time.Hour) // 每小時執(zhí)行一次
for range ticker.C {
// 遍歷臨時目錄
entries, err := os.ReadDir(TempDir)
if err != nil {
fmt.Printf("讀取臨時目錄失敗:%v\n", err)
continue
}
// 清理超過24小時的文件
expireTime := time.Now().Add(-24 * time.Hour)
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
if info.ModTime().Before(expireTime) {
os.Remove(filepath.Join(TempDir, entry.Name()))
fmt.Printf("清理過期分片:%s\n", entry.Name())
}
}
}
}
// 在main函數(shù)中啟動定時任務(wù)
go cleanExpiredChunks()
4. 分片校驗機(jī)制
為避免分片傳輸過程中數(shù)據(jù)損壞,可在客戶端計算每個分片的MD5,上傳時攜帶分片MD5;服務(wù)端接收后校驗分片MD5,確保數(shù)據(jù)完整性:
// 客戶端:計算分片MD5
chunkMD5 := md5.Sum(buf[:n])
chunkMD5Str := hex.EncodeToString(chunkMD5[:])
// 上傳時添加分片MD5參數(shù)
_ = writer.WriteField("chunk_md5", chunkMD5Str)
// 服務(wù)端:校驗分片MD5
chunkMD5 := c.PostForm("chunk_md5")
// 計算接收到的分片內(nèi)容的MD5,與客戶端傳入的對比
5. 分布式場景適配
如果服務(wù)端部署在多節(jié)點,本地文件系統(tǒng)無法共享分片文件,需將分片存儲至分布式對象存儲(如MinIO、AWS S3),并將分片狀態(tài)記錄至Redis:
- 分片存儲:將分片上傳至MinIO,而非本地目錄;
- 狀態(tài)記錄:Redis的Set結(jié)構(gòu)存儲已上傳的分片索引(key為文件MD5,value為分片索引集合);
- 合并邏輯:從MinIO下載所有分片,合并后再上傳至MinIO。
6. 上傳進(jìn)度反饋
服務(wù)端可新增接口,根據(jù)文件MD5返回已上傳分片數(shù)/總分片數(shù),客戶端據(jù)此計算上傳進(jìn)度并展示:
// 服務(wù)端進(jìn)度查詢接口
func uploadProgressHandler(c *gin.Context) {
fileMD5 := c.Query("file_md5")
totalChunksStr := c.Query("total_chunks")
// ... 校驗參數(shù) ...
uploadedCount := len(uploadedChunks)
progress := float64(uploadedCount) / float64(totalChunks) * 100
c.JSON(http.StatusOK, gin.H{
"code": 200,
"progress": progress, // 百分比
})
}
七、注意事項
- 目錄權(quán)限:確保服務(wù)端的
TempDir和UploadDir有讀寫權(quán)限,避免文件創(chuàng)建/寫入失敗。 - 文件命名沖突:通過文件MD5+原文件名的方式命名最終文件,避免不同文件重名覆蓋。
- 內(nèi)存占用:讀寫文件時使用緩沖區(qū)(如1MB),避免一次性加載大文件/分片到內(nèi)存,引發(fā)OOM。
- 超時設(shè)置:客戶端和服務(wù)端都需設(shè)置合理的HTTP超時時間,避免大分片上傳超時。
- 異常處理:增加文件鎖、重試機(jī)制,避免并發(fā)上傳同一分片導(dǎo)致的文件損壞。
八、總結(jié)
本文基于Gin框架實現(xiàn)了大文件分片上傳與斷點續(xù)傳的核心功能,從原理剖析到代碼實現(xiàn),再到進(jìn)階優(yōu)化,覆蓋了從基礎(chǔ)到生產(chǎn)級別的關(guān)鍵要點。該方案解決了傳統(tǒng)單文件上傳的痛點,具備高可用性和可擴(kuò)展性。
在實際項目中,可根據(jù)業(yè)務(wù)需求靈活調(diào)整:前端可替換為WebUploader、Resumable.js等成熟的分片上傳庫,服務(wù)端可對接分布式存儲和緩存,進(jìn)一步提升系統(tǒng)的穩(wěn)定性和性能。Go語言的高性能特性結(jié)合Gin框架的輕量優(yōu)勢,使得該方案能夠輕松應(yīng)對高并發(fā)的大文件上傳場景。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
k8s容器互聯(lián)flannel?vxlan通信原理
這篇文章主要為大家介紹了k8s容器互聯(lián)flannel?vxlan通信原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
GoLang jwt無感刷新與SSO單點登錄限制解除方法詳解
這篇文章主要介紹了GoLang jwt無感刷新與SSO單點登錄限制解除方法,JWT是一個簽名的JSON對象,通常用作Oauth2的Bearer token,JWT包括三個用.分割的部分。本文將利用JWT進(jìn)行認(rèn)證和加密,感興趣的可以了解一下2023-03-03
golang判斷chan channel是否關(guān)閉的方法
這篇文章主要介紹了golang判斷chan channel是否關(guān)閉的方法,結(jié)合實例形式對比分析了Go語言判斷chan沒有關(guān)閉的后果及關(guān)閉的方法,需要的朋友可以參考下2016-07-07

