分片上传思想
假如我们对1G的文件进行分片上传, 每片大小是4M, 那么总共就是256个分片。客户端将每个分片的数据保存到请求体,通过fasthttp发起post请求传给服务端,服务端获取数据后保存本地目录里并将文件此时大小记录到redis中, 如果中间出现闪断, 再次传文件时可以读取redis中的当前文件大小, 继续进行上传.
实现思路
1.将文件拆分为多个块,进行分块传输。
2.使用fasthttp发起请求。 并实现断点续传,当文件上传过程中出现中断或网络问题时,可以通过实现断点续传功能。
3.限制文件上传大小。防止过多的上传请求导致服务器崩溃。
上传校验。
4.使用md5对文件加密,生成文件传输唯一标识。
5.对不同文件传输使用压缩,多协程方式传输提高传输效率。
fasthttp做了哪些优化
1、 使用sync.Pool进行复用;fasthttp是利用一个 worker 复用 goroutine
2、请求对象 Request 和响应对象 Response 都是通过对象池进行管理的
3、 使用[]byte代替string,减小string转[]bytes内存消耗
4、使用bytebufferpool代替 bytes.Buffer,避免内存拷贝 + 底层 byte 切片复用
服务端
package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"liujun/day1/test17/md4"
"liujun/day1/test17/redi"
"mime/multipart"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/valyala/bytebufferpool"
)
const (
SavePath = "uploads/"
)
var (
// FilesMax 限制上传文件的大小为10MB
FilesMax int64 = 10 << 20
// ValuesMax 限制POST字段内容的大小
ValuesMax int64 = 512
)
func main() {
l, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
http.HandleFunc("/upload", uploadHandler)
fmt.Println("Using port:", l.Addr())
panic(http.Serve(l, nil))
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
AsyncUpload(w, r)
}
func AsyncUpload(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
// 不支持的 Content-Type 类型
http.Error(w, " 不支持的 Content-Type 类型", http.StatusBadRequest)
return
}
// 设置请求体最大值
r.Body = http.MaxBytesReader(w, r.Body, FilesMax+ValuesMax)
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for {
//一块一块的获取
part, err := reader.NextPart()
if err != nil {
if err == io.EOF { //读完结束
break
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//获取文件名
fileName := part.FileName()
//获取表单名
formName := part.FormName()
var buf = &bytebufferpool.ByteBuffer{}
// 文本数据
if fileName == "" {
var limitError = "请求主体中文本字段" + formName + "超出大小限制"
// 判断传输的数据是否超过设置的阈值
err = uploadSizeLimit(buf, part, ValuesMax, limitError)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
continue
}
// 文件字段大小限制
var limitError = "请求主体中文件" + fileName + "超出大小限制"
err = uploadSizeLimit(buf, part, FilesMax, limitError)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 保存文件
if err := SaveFile(fileName, buf.Bytes()); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
return
}
http.NotFound(w, r)
}
// 上传内容大小限制
func uploadSizeLimit(buf *bytebufferpool.ByteBuffer, part *multipart.Part, maxLimit int64, limitError string) error {
n, err := io.CopyN(buf, part, maxLimit+1)
if err != nil && err != io.EOF {
return err
}
maxLimit -= n
if maxLimit < 0 {
return errors.New(limitError)
}
return nil
}
func SaveFile(fileName string, res []byte) error {
file_name := SavePath + fileName
newFile, err := os.OpenFile(file_name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
if err != nil {
return err
}
defer func() {
newFile.Close()
}()
bufferedWriter := bufio.NewWriter(newFile)
_, err = bufferedWriter.Write(res)
if err != nil && err != io.EOF {
return err
}
// 记录当前分片偏移到redis
f, _ := newFile.Stat()
size := f.Size()
key := md4.Md5Encode(fileName)
redi.RDB.Set(context.Background(), key, size, time.Hour)
return bufferedWriter.Flush()
}
客户端
package main
import (
"bytes"
"context"
"fmt"
"io"
"src/day1/test/md4"
"src/day1/test/redi"
"mime/multipart"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/valyala/fasthttp"
)
// 分片大小设置为4M
const chunkSize = 4 * 1024 * 1024
// 请求体
type Request struct {
Method string
Url string
Timeout time.Duration
Headers map[string]string //设置请求header
Values map[string]string //普通post字段内容
File map[string]string //文件
body *bytes.Buffer
multipartWriter *multipart.Writer
Partition bool //是否分片上传
offset int64 //文件偏移量
}
var wg sync.WaitGroup
func main() {
//1.文件切片上传
st := time.Now()
var r1 = &Request{
Method: "POST",
Url: "http://localhost:8080/upload",
Timeout: 0,
Headers: make(map[string]string),
File: make(map[string]string),
Partition: true,
offset: 0,
}
var r2 = &Request{
Method: "POST",
Url: "http://localhost:8080/upload",
Timeout: 0,
Headers: make(map[string]string),
File: make(map[string]string),
Partition: true,
offset: 0,
}
var r3 = &Request{
Method: "POST",
Url: "http://localhost:8080/upload",
Timeout: 0,
Headers: make(map[string]string),
File: make(map[string]string),
Partition: true,
offset: 0,
}
var r4 = &Request{
Method: "POST",
Url: "http://localhost:8080/upload",
Timeout: 0,
Headers: make(map[string]string),
File: make(map[string]string),
Partition: true,
offset: 0,
}
wg.Add(4)
r1.File["users"] = "users.zip"
r2.File["logs"] = "logs.zip"
r3.File["statements"] = "statements.zip"
r4.File["sql"] = "sql.sql"
go r1.Upload()
go r2.Upload()
go r3.Upload()
go r4.Upload()
wg.Wait()
end := time.Since(st)
fmt.Println("传输时间:", end)
}
func (r *Request) Upload() {
defer wg.Done()
if r.Partition {
r.PartitionUpload()
} else {
r.FileUpload()
}
}
// 分片上传
func (r *Request) PartitionUpload() {
if !r.Partition {
return
}
for flag, filename := range r.File {
//打开传输的文件
file, err := os.Open(filename)
if err != nil {
panic(err)
}
defer file.Close()
//ns表示每次设置偏移之前,上次传输的分块大小
ns := 0
//对文件名进行md5加密,形成唯一标识,保存在redis,值为文件当前内存大小
key := md4.Md5Encode(file.Name())
offset, _ := redi.RDB.Get(context.Background(), key).Result()
//存在,获取文件偏移
if offset != "" {
r.offset, _ = strconv.ParseInt(offset, 10, 64)
file.Seek(r.offset, 0)
} else { //如果不存在,说明该文件第一次传输 ,便宜设为0
r.offset = 0
}
for {
//将文件放到请求体
r.body = &bytes.Buffer{}
//通过mutltipart写入
r.multipartWriter = multipart.NewWriter(r.body)
//每次按chunksize大小传输数据
var buf = make([]byte, chunkSize)
//将分块数据保存到buf里
n, err := file.Read(buf)
//重新设置文件偏移量,上次的位置+这次传输分块大小
r.offset += int64(ns)
//更新上次分块大小
ns = n
if err != nil {
//读完就结束本次传输
if err == io.EOF {
break
}
panic(err)
}
//将分块内容添加到请求体
if err := r.AddToReqBody(flag, file.Name(), int64(n)); err != nil {
panic(err)
}
if err := r.multipartWriter.Close(); err != nil {
panic(err)
}
//设置请求头文件类型
r.Headers = map[string]string{
"Content-Type": r.multipartWriter.FormDataContentType(),
}
// 发送 POST 请求上传文件
r.SendFile()
}
}
}
func (r *Request) FileUpload() {
// 创建多部分上传请求
r.body = &bytes.Buffer{}
//通过包multipart实现了MIME的multipart解析
r.multipartWriter = multipart.NewWriter(r.body)
if err := r.multiTypeUpload(); err != nil {
panic(err)
}
// 发送 POST 请求上传文件
r.Headers = map[string]string{
"Content-Type": r.multipartWriter.FormDataContentType(),
}
r.SendFile()
}
// 创建fasthttp请求
func (r *Request) SendFile() {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
// 设置头信息为:multipart/form-data
for k, v := range r.Headers {
req.Header.Set(k, v)
}
req.Header.SetMethod("POST")
req.SetRequestURI(r.Url)
req.SetBody(r.body.Bytes())
resp := fasthttp.AcquireResponse()
// 用完需要释放资源
defer fasthttp.ReleaseResponse(resp)
if err := fasthttp.Do(req, resp); err != nil {
fmt.Println("请求失败:", err.Error())
return
}
b := resp.Body()
fmt.Println(200, "-----", string(b), "偏移量:", r.offset)
}
// 创建多种类型的表单数据
func (r *Request) multiTypeUpload() error {
var err error = nil
if r.Values != nil {
for k, v := range r.Values {
err = r.multipartWriter.WriteField(k, v)
}
if err != nil {
return err
}
}
if r.File != nil {
for k, v := range r.File {
err = r.AddToReqBody(k, v, 0)
}
if err != nil {
return err
}
}
if err := r.multipartWriter.Close(); err != nil {
return err
}
return err
}
func (r *Request) AddToReqBody(fieldName string, file string, n int64) error {
//创建表单文件
_, err := r.multipartWriter.CreateFormFile(fieldName, filepath.Base(file))
if err != nil {
return err
}
// 打开文件
f, err := os.Open(file)
//移到上次传输的位置
f.Seek(r.offset, 0)
if err != nil {
return err
}
defer f.Close()
// 将这次分块的内容拷贝到请求体中
if r.Partition {
_, err = io.CopyN(r.body, f, n)
} else {
_, err = io.Copy(r.body, f)
}
if err != nil {
return err
}
return nil
}
redis
package redi
import (
"github.com/go-redis/redis/v8"
)
var RDB *redis.Client
func init() {
RDB = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // 密码
DB: 0, // 数据库
PoolSize: 20, // 连接池大小
})
}
md5
package md4
import (
"crypto/md5"
"encoding/hex"
)
func Md5Encode(data string) string {
h := md5.New()
h.Write([]byte(data))
str := h.Sum(nil)
return hex.EncodeToString(str)
}