fasthttp实现客户端分块文件传输以及断点续传

分片上传思想

假如我们对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)
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值