尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

文件上传与下载:大文件分片处理

文件上传与下载:大文件分片处理 # 文件上传与下载:大文件分片处理摘要:Go文件上传下载实战涵盖multipart表单上传、流式处理避免OOM、分片上传协议设计、断点续传下载附临时文件泄漏踩坑经验。去年公司做了个视频处理平台用户上传视频做转码。测试时几十MB的文件一切正常上线第一天就有用户传了个4GB的高清视频服务器直接OOM重启了。排查发现是r.ParseMultipartForm的锅它会把上传文件读进内存超过maxMemory才写临时文件。我们设了1GB的maxMemory想提速大文件直接把内存吃光了。流式处理避免OOM小文件用ParseMultipartForm够了。大文件必须用MultipartReader流式读取内存占用恒定。packagemainimport(fmtionet/httpospath/filepath)funcuploadStreamHandler(w http.ResponseWriter,r*http.Request){// 限制请求体总大小1GB上限r.Bodyhttp.MaxBytesReader(w,r.Body,130)deferr.Body.Close()// 用MultipartReader流式读取不预加载到内存reader,err:r.MultipartReader()iferr!nil{http.Error(w,解析multipart失败,http.StatusBadRequest)return}// 逐个part处理for{part,err:reader.NextPart()iferrio.EOF{break// 所有part读完}iferr!nil{http.Error(w,读取part失败,http.StatusBadRequest)return}// 跳过普通表单字段只处理文件ifpart.FileName(){continue}// 清理文件名防止路径穿越攻击filename:filepath.Base(part.FileName())dst,err:os.Create(filepath.Join(uploads,filename))iferr!nil{http.Error(w,创建文件失败,http.StatusInternalServerError)return}// 32KB缓冲区流式复制不管文件多大内存占用恒定buf:make([]byte,32*1024)_,errio.CopyBuffer(dst,part,buf)dst.Close()iferr!nil{http.Error(w,保存文件失败,http.StatusInternalServerError)return}fmt.Fprintf(w,上传成功: %s\n,filename)}}funcmain(){os.MkdirAll(uploads,0755)http.HandleFunc(/upload,uploadStreamHandler)http.ListenAndServe(:8080,nil)}用MultipartReader内存占用恒定传1GB和1TB的文件内存消耗一样。代价是手动遍历每个part。分片上传实现文件特别大时10GB以上网络不稳定会导致上传失败重来。分片上传把文件切块独立上传失败只需重传那一片。packagemainimport(crypto/md5encoding/hexfmtionet/httpospath/filepathstrconv)// 分片上传接口接收一个分片funcuploadChunkHandler(w http.ResponseWriter,r*http.Request){// 从查询参数获取文件ID和分片序号fileID:r.URL.Query().Get(fileId)chunkIndex:r.URL.Query().Get(chunkIndex)// 为每个文件创建临时目录存放分片chunkDir:filepath.Join(chunks,fileID)os.MkdirAll(chunkDir,0755)// 读取上传的分片数据并保存file,_,err:r.FormFile(chunk)iferr!nil{http.Error(w,读取分片失败,http.StatusBadRequest)return}deferfile.Close()// 创建分片文件必须检查错误否则dst为nil后续io.Copy会panicdst,err:os.Create(filepath.Join(chunkDir,chunkIndex))iferr!nil{http.Error(w,创建分片文件失败,http.StatusInternalServerError)return}deferdst.Close()// 写入分片数据检查错误防止静默失败if_,err:io.Copy(dst,file);err!nil{http.Error(w,写入分片失败,http.StatusInternalServerError)return}fmt.Fprintf(w,分片 %s 上传成功\n,chunkIndex)}// 合并分片接口把所有分片合成完整文件funcmergeChunksHandler(w http.ResponseWriter,r*http.Request){fileID:r.URL.Query().Get(fileId)filename:r.URL.Query().Get(filename)totalChunks,_:strconv.Atoi(r.URL.Query().Get(totalChunks))// 创建最终文件dst,err:os.Create(filepath.Join(uploads,filename))iferr!nil{http.Error(w,创建文件失败,http.StatusInternalServerError)return}deferdst.Close()// 按顺序读取分片追加到最终文件同时计算MD5校验完整性hasher:md5.New()fori:0;itotalChunks;i{chunkFile,err:os.Open(filepath.Join(chunks,fileID,strconv.Itoa(i)))iferr!nil{http.Error(w,fmt.Sprintf(分片 %d 不存在,i),http.StatusBadRequest)return}// 同时写入目标文件和hash计算器检查错误防止合并出坏文件if_,err:io.Copy(io.MultiWriter(dst,hasher),chunkFile);err!nil{chunkFile.Close()http.Error(w,fmt.Sprintf(分片 %d 合并失败,i),http.StatusInternalServerError)return}chunkFile.Close()}md5sum:hex.EncodeToString(hasher.Sum(nil))os.RemoveAll(filepath.Join(chunks,fileID))// 清理临时分片fmt.Fprintf(w,合并成功MD5: %s\n,md5sum)}funcmain(){os.MkdirAll(uploads,0755)os.MkdirAll(chunks,0755)http.HandleFunc(/upload/chunk,uploadChunkHandler)http.HandleFunc(/upload/merge,mergeChunksHandler)http.ListenAndServe(:8080,nil)}客户端把文件切成5MB一块上传每块带fileId和chunkIndex。所有分片上传完后调merge接口合并合并时计算MD5验证完整性。断点续传下载大文件下载用HTTP Range请求实现断点续传Go标准库已内置。packagemainimport(net/httpos)funcdownloadHandler(w http.ResponseWriter,r*http.Request){filename:r.URL.Query().Get(file)file,err:os.Open(uploads/filename)iferr!nil{http.Error(w,文件不存在,http.StatusNotFound)return}deferfile.Close()stat,_:file.Stat()// 设置响应头声明支持Range请求w.Header().Set(Content-Disposition,attachment; filenamefilename)w.Header().Set(Accept-Ranges,bytes)// ServeContent自动解析Range头返回206和对应数据段// 客户端断线重连带 Range: bytes1048576- 即可续传http.ServeContent(w,r,filename,stat.ModTime(),file)}funcmain(){http.HandleFunc(/download,downloadHandler)http.ListenAndServe(:8080,nil)}http.ServeContent自动处理Range请求返回206状态码客户端断线重连带上Range头即可续传。独家踩坑:临时文件泄漏上线后磁盘空间每隔几天就满了。查了半天发现是r.ParseMultipartForm的临时文件没清理。Go处理multipart表单时超过maxMemory会写临时文件到os.TempDir()在RemoveAll()调用时删除。用MultipartReader流式处理则没有临时文件。旧接口还在用ParseMultipartForm上传完没调RemoveAll()runtime不会自动清理文件一直留在磁盘上。// 修复方案加defer清理临时文件funcuploadFixed(w http.ResponseWriter,r*http.Request){r.ParseMultipartForm(3220)ifr.MultipartForm!nil{deferr.MultipartForm.RemoveAll()// 关键这行}file,_,_:r.FormFile(file)// ... 处理文件 ...}加了一行defer r.MultipartForm.RemoveAll()就解决了。开发环境难发现单次临时文件才几十MB。生产环境高并发临时文件堆积就爆磁盘。对比分析方案内存占用支持大文件断点续传实现复杂度ParseMultipartForm高差不支持简单MultipartReader流式极低好不支持中等分片上传极低极好支持复杂Java用Spring的MultipartFile底层原理类似但Go的io.Copy更简洁。Python的Django没有流式API处理超大文件不如Go灵活。总结核心原则就是流式处理别把整个文件读进内存。小文件用ParseMultipartForm大文件用MultipartReader超大文件用分片上传加断点续传。临时文件清理也别忽略上线后迟早要还的。下一篇聊JWT认证与授权讲讲无状态认证的完整实现方案。
返回列表