Tus协议地址http://www.tus.io/protocols/resumable-upload.html,大家可以看看英文原文介绍,我这里也简单介绍下。 这是一个基于http的可续传的协议,主要是不同的HttpMethod和相关的自定义header来实现续传功能。 具体的Method和header介绍: 1、HEAD head请求是获取文件已上传的状态,服务端返回该文件哪些范围已写入,哪些没写入。 (PS,我刚发现协议已经变了,以前不是offset) 但是我这个实现和tus也略有不同,我支持一个文件可以多线程同时上传,所以不能完全追寻他的协议。 客户端接收到的header例子: range:bytes=0-9,20-29 意思表名0-9和20-29这俩个块已被写入,所以续传的时候不需要上传了。 如果接收到的是 range:bytes=0-1023(假如文件就1024长度,那么说明文件已上传完毕) 2、POST post请求是用于创建文件,header里必须要有声明文件的大小,例如 content-range:*/1023 这说明文件的大小是1024。 返回结果:告诉客户端这个文件的地址,好让客户端知道往什么地址PUT数据。例如: Location: http://tus.example.org/files/24e533e02ec3bc40c387f1a0e460e216 3、PUT put请求是上传文件用的,如果想实现续传,那么就要把文件拆分为N个小块,按块上传,这样如果断掉,下次可以通过HEAD请求看到哪些块已经上传。 4、GET get请求就是下载文件,这里不必多说了。 协议大致如此,只是相对于tus原本的协议有所变化(可以多块同时上传,所以并非是顺序上传,在Head请求的返回自然有所变化)。 协议实现思路: 客户端分块上传,服务端接收到每个块之后存储到服务端。如何记住哪些块被上传了呢? 我的实现思路是需要一个metadata的辅助元数据文件来继续这个文件的上传状态、文件大小、文件原名称等等。 现在是用文件存储的metadata,这只是一个demo,如果是真正使用,我想用数据库更合适一些。 我走的弯路: 第一次: 如何实现块能够写入到目标文件里?这个问题也和以前的同事交流过,当时没有找到解决办法。除非顺序写入然后用append的模式。 但是客户端上传确实多线程非顺序上传,服务端该如何保证顺序? 最后的解决办法是把客户端上传的每一个块保存为单个小文件,每次上传完一个块之后判断是不是所有的块上传完毕,如果上传完毕做一次合并。 判断文件块是否写完,我就要遍历每一个小文件,判断他们是否连续。由于nodejs的文件操作需要异步,所以实现起来需要写各种递归,异常痛苦,大家可以看我的GitHub提交的历史版本,最老的版本里面有很多递归。代码非常丑陋,而且难以维护。 第二次: 为了减少递归,我使用global全局缓存,而且限定了文件上传的块的大小必须一样,没上传完一个块,我就把他存储到global缓存里。这样就不需要遍历小文件,减少了很多异步操作,代码清晰了很多。 第三次: 小文件合并的方案虽然可行,但毕竟感觉有些愚蠢,如果是特别大的文件,那可以想象合并和遍历都很慢,管理也不方便。所以如果能解决Nodejs的按offset写入stream的问题就可以解决。我又翻了下Nodejs的文档,发现以前使用的都是w和w+的打开方式,这种打开方式会清空文件的内容,而r和r+不会。 于是我尝试着修改了代码,由于是并发写入,实际上文件写入是不能并发的,所以需要加锁,当块写入的时候判断是否锁住就可以了。 这一下去掉了好多的代码,代码更加的明亮起来。 为什么用NodeJS? 因为这是一个面试题的作业,所以必须用Nodejs。 我在FileMetadata初始化是依然用的fs.readSync,我以前同事告诉我nodejs不可以使用sync方法,这样会hold住整个进程。这是现实太恐怖了。 所以我个人觉得nodejs的弊端也很明显,对代码复杂度的增加显而易见。 至于Javascript的非阻塞的先天优势,其他的语言也可以做到。那为什么还用Nodejs呢?而且Javascript的语法比较弱,很多实现都比较困难。我对nodejs没有太多好感。