题外话:这个系列的文章记录了本人最近写的一个小工程,主要包含了两个功能,一是对文件的断点续传的功能,二是基于WCF的一对多文件主动发送的功能,顺便这也是我自己在WCF学习路上的一个小成果吧。
在网上找了很多关于文件断点续传的例子,没看到有写的特别好的,可能好的代码都是内部使用的吧。因为自己在项目中时常会遇到文件传输的问题,所以这次就按照自己的想法来实现了一个,要说写的时候有没有参考过别人的代码,那肯定是没有的,因为我参考的是某c论坛里关于bt种子和迅雷文件下载的实现思路,虽然只有几句话而已,但也激发了我的灵感,于是就诞生了这个程序。
文章先不上源代码,想看干货的各位请直接翻到文章最底部查看我的Github。想要我的源代码吗?想要的话可以全部给你,去找吧!我把所有源码都放在那里!
正篇:
写这篇文章的目的有两个,一是作为学习日记,记录一下程序思想建立的过程,二是想对保存这个文件断点续传的功能,方便日后重复使用。
首先记录一下思路,一开始也会从网上先找找看有没有现成的关于文件传输的工具类,但方式大同小异,使用一个while循环,在循环内不停地读取文件,然后调用发送方法,直到文件读取完毕为止,示例代码见Fig.1。用这种直接的方式读文件发文件,是做不到断点续传的,那么问题来了,要怎么改才能让它支持断点续传呢?
Fig.1 普通的文件读取方式
我们可以从通信上下手,想要实现断点续传,不附带一些相关的文件信息是不可能的,最好形成消息闭环。简单的实现思路如Fig.2所示。为了方便接下来的说明,我写到一半又补画了个流程图…..,所以如果出现文笔不通顺的情况,那就是突然出现的流程图的锅。
Fig.2 双方协议交互流程
下面要讲的是发送端和接收端双方的交互协议,每次发送文件前,读取将要发送的文件信息FileInfo,包括文件名FileName、文件大小FileLength、文件的MD5之类的信息,然后再人为选取一个数值作为单个文件块大小BlockSize,将整个文件分成N个小文件块,根据文件大小计算出文件块的数量BlockCount、最后一个文件块的大小LastBlockSize,我们将文件块的信息称为FileBlockInfo。
PS:这里插一嘴,文件块大小的选取有一定的规则,若选的太小,文件块数量会较多,导致发送次数也会多,由于每次发送会附带除文件之外的相关信息,所以这将会额外的增加网络的负担;而若将文件块大小选的过大,则可能会超过单次网络发送数据长度的限制导致连帧或粘包的问题。这个大小应该根据具体使用的网络协议(HTTP、TCP等)来确定,最好测试一下。
有了这些发送时附带的信息还不够,还需要接收端的反馈,说了要形成闭环嘛。具体文件接收端除了将数据流写入文件之外,还需要做的事情,就应该在这个时候考虑了。为了实现断点续传,最重要的一步,是接收端需要给发送端反馈文件已经写到哪个位置了,也就是【断点】Writing Offset,发送端再根据这个【断点】指向的位置,调整接下来要发送的数据即可。等到所有数据都发送完毕后,再进行一次整体的文件MD5哈希校验,保证文件在传输后也是准确的,如果校验失败的话,就要考虑重新发送文件。
大体的实现思路可以说就是这样了,具体实现的时候还会遇到诸如“如何判断文件断点的位置”、“大文件校验失败后如何处理”、“在一个接收函数里写的判断分支太多了怎么维护”,这些问题我会在下面进行介绍。
问题一:如何判断文件断点的位置?
既然提到了断点,那么前置条件一是:该文件已存在,若文件不存在,断点续传无从说起;前置条件二是:该文件的长度小于发送端的文件长度,说白了就是这个文件处于传了一部分但是没传完的状态。
有一种简单暴力的思路,就是直接回传该文件当前的长度,让发送端接着后面发送数据就可以了。我觉得这种思路有一定的可行性,但缺点是,一旦出现网络波动导致数据断帧,而发送端误以为已经发送完整,就会导致后面接收的数据全都错位了。所以,回传的不是已存在文件的长度,而是根据已存在文件的长度和FileBlockInfo计算得出当前最后一个文件块的位置,让发送端从该位置发送,这样就保证了我后续文件块的完整性,如Fig.3。
Fig.3 断点位置计算
问题二:大文件校验失败后如何处理?
一般来说,当获取到数据发送失败时,应当再发送一次数据。但考虑到大文件的校验失败,可能仅因为小部分文件块没有正确写入而导致的,那直接重传不仅耗流量,而且也很耗时间。所以针对这种情况,我才在要在发送时计算各个文件块的MD5值,为的就是在整体文件的MD5检查失败后,对各个文件块进行块的MD5对比,这样可以不用把所有文件块都重新传输一次,可以节约不少时间。虽然这样牺牲了发送的效率,但可以使用多线程的方法缩短计算BlockMD5时产生的空档。
问题三:数据接收事件里写的判断分支又那么多,怎么维护?
文章前面说了不少“遇到某某情况该怎么怎么处理”,即每一种情况就要写一个分支,而在分支中的处理过程又长,全都写在接收函数里会让这块代码难以维护。于是我想到了使用设计模式中的状态模式(状态机),将具体处理的部*部化,将不同状态的行为分割开来,而且,各个状态之间还可以灵活地相互转化。关于Fig.4这种设计模式,不了解它的童鞋可以在网上搜一搜,我就不重复介绍啦。
Fig.4 状态模式UML图
Fig.5 文件数据接收状态模式的实现
以下是完整工程文件,使用VS2017编译,已经在我的个人云服务器上测试过了,可以跨局域网传输。
https://github.com/wingsziye/RemoteWcfFileTransfer
Fig.6 一些效果如图
2018年12月21日星期五