1.实现目的
主要目的是用来熟悉go语言,同时利用该任务熟悉文件存储相关的业务,通过该项目可以熟悉到的go知识点:
(1)go语言语法;
(2)go的goroutine使用方式;
(3)go通道chan的使用
(4)等待所有goroutine结束的同步信号使用;
(5)go的结构体定义和方法使用;
2.实现的功能点
(1)支持批量上传下载文件,并进行md5值校验;
(2)支持查看文件列表;
(3)支持断点续传和下载文件;
(4)支持大文件切片上传和大文件切片下载;
(5)支持分片失败重传和失败重下载;
(6)支持控制每个文件上传和下载的最大goroutine数量;
(7)服务端根据配置文件读取保存文件目录、绑定IP和端口号信息等。
3.实现设计
3.1.总体上传下载文件结构图
3.2.上传文件流程图
流程概述:
(1)上传文件时首先判断文件是否大于1M,如果小于1M则没必要进行分片,直接整个文件通过HTTP请求发送到Server端进行保存;
(2)如果文件大于1M,则首先判断该文件是不是上传了一部分的文件,这个通过查找当前目录下有没有对应元数据文件来判断,如果没有则是全新要上传的文件,否则是需要断点续传的文件;
(3)如果是全新要上传的文件,则会先对该文件进行计算,比如该文件能被切分成多个个分片,生成上传uuid,作为唯一上传标识,并把这些数据保存到本地文件(创建一个隐藏文件,后缀加上.uploading);
(4)同时还需向服务端先发送一个请求,让其先在保存目录创建一个uuid的目录,用来待会保存分片文件使用,同时服务端也会生成一个元数据文件;
(5)如果是需要断点续传的文件,则需要请求服务端获取该文件还缺少哪些文件分片没有上传(服务端只需从保存目录中已保存的序号结合文件元数据即可识别到哪些序号还没上传),并将这些切片序号发送回给客户端,假设有1,5,6,7,没发送,则服务端只需要发送1,5,-1即可标识,1分片和5到最后一个分片都没上传成功;
(6)在上传前会先启动一个goroutine,专门用来重传失败分片的,它会不断的从RetryChannel通道中读分片数据,如果没有则阻塞,如果有则重传该分片,如果再失败则再发送到通道中,可以看做当队列使用;
(7)开始读文件,如果是续传的,还可以根据续传情况进行偏移量偏移,跳过指定的切片段,读时每次只读1M,然后判断该切片是否已被上传过,如果上传过则无须再上传,可直接跳过,否则就创建一个goroutine进行异步上传;
(8)当所有goroutine都运行完成表示切片都上传完成了则发送请求告诉服务端切片已上传完成,服务端也会把文件状态置为active。
3.3.下载文件流程图
流程概述:实现思路跟上传相似,这里不再概述。
3.4.核心的设计点
(1)文件分片
将大文件进行分片,定义分片规格为1M,比如有5M大小的文件,那么就会分成文件名为0,1,2,3,4,5这6个文件,上传到服务端后,服务端会创建一个uuid的文件夹用来保存这5个分片文件,并且会记录一个元数据文件,里面保存着该元数据文件对应哪个目录,文件切片大小、文件大小和文件md5等原信息。对于小于1M的普通文件则不进行切片处理,就是正常的一个文件,所以我程序里规定了文件名为.slice结尾的文件则为切片文件,否则为普通文件。
(2)断点续传和断点续载
首先是断点续传,在开始传文件时,我会创建一个隐藏文件作为上传元数据文件,比如文件名是file.txt,则我创建的元数据文件名为.file.txt.uploading,里面记录着文件元数据信息,比如上传UUID、文件大小和文件md5等信息,如果用户上传完成,则起最后会被删除掉,表示整个文件都上传完成了,假设上传过程中出现了中断,则下次重新上传该文件时我检测到该隐藏文件就知道它是还未上传完整的文件,会先去服务端请求看缺少哪些分片数据,比如该文件一共有1,2,3,4,5片,服务端响应回来说只收到了1,3,5片,那么待会我就只需要把0,2,4片重传一次即可。
断点续载同理,也是需要在客户端维护元数据,且通过查找已下载的分片来找出未下载的分片序号,然后只需要重新下载没有的分片即可。
(3)失败重传
上传器和下载器的结构体定义我都会定义一个RetryChannel,这是一个分片结构体类型的通道,当分片上传或下载失败时,会将分片发送到这个通道,在上传或下载开始时我都会启动一个goroutine,专门负责从这个通道读数据,读到了就对这个分片进行重新上传或下载。
(4)并发上传或下载
并发使用了go的goroutine,并发单位以文件切片为单位,同时通过通道(申请有数量限制的通道)的方式控制运行的goroutine的数量,同时采用go里的同步信号量来控制是否所有goroutine都运行完成了。
4.实现代码
客户端目录结构:
上传器结构体定义:
// Uploader 上传器 type Uploader struct { common.FileMetadata // 文件元数据 common.SliceSeq // 需要重传的序号 waitGoroutine sync.WaitGroup // 同步goroutine NewLoader bool // 是否是新创建的上传器 FilePath string // 上传文件路径 SliceBytes int // 切片大小 RetryChannel chan *FilePart // 重传channel通道 MaxGtChannel chan struct{} // 限制上传的goroutine的数量通道 StartTime int64 // 上传开始时间 }
下载器结构体定义:
// Downloader 下载器 type Downloader struct { common.FileMetadata // 文件元数据 common.SliceSeq // 需要重传的序号 waitGoroutine sync.WaitGroup // 同步goroutine DownloadDir string // 下载文件保存目录 RetryChannel chan int // 重传channel通道 MaxGtChannel chan struct{} // 限制上传的goroutine的数量通道 StartTime int64 // 下载开始时间 }
Client端项目github地址:xxx
服务端目录结构:
文件的传输采用的是HTTP协议,服务端的工作主要是起一个HTTP Server,然后监听对应URL,绑定对应的响应方法,同时把接收到的文件数据保存到指定目录下。
Server端项目github地址:xxx
使用方法可用–help查看:
上传、列出和下载使用方法示例(图中的下载路径和文件路径可以自行修改,且确保服务端FtpServer目录下的etc目录下的config.json文件里指定的StoreDir指定目录存在):
服务端先启动:进入项目目录,执行:go run main.go
上传文件示例:
客户端运行:
服务端响应:
列出文件列表示例:
下载文件示例:
5.可改进点
当然,这个小项目可改进点还有很多,我这里列出几个我想到的:
(1)需重传的序号计算算法还可以实现的更好点,比如还没传的,可以用1-3,5-9这样来表示;
(2)当前的md5计算是计算整个文件的,但其实可以给每个分片都赋予一个md5,这样就不用再最后累计一边整个文件的md5,降低读IO次数;
(3)下载文件时,其实可以开辟一个指定size的空洞文件,然后接收到文件分片可以按照偏移量写到给新文件中,避免了最后一步的合并过程的IO。