在所有的通信系统中,文件传送是最常见也是最重要的功能之一,ESFramework对文件传送的强大支持也是其亮点之一,使用ESFramework可以非常轻松地实现与文件传送相关的所有需求。ESPlus.Application.FileTransfering命名空间完整地解决了通信中与文件收发相关的问题,可以支持客户端与客户端之间的文件对传、上传文件到服务器以及从服务器下载文件,并且可以监控每个文件传送的实时状态、且内置了文件续传等功能。
一.ESPlus的文件传送流程
ESPlus定义了文件传送的标准流程,可以用下图表示:
(1)由发送方发起传送文件的请求。
(2)接收方回复同意或者拒绝接收文件。如果拒收,则流程结束;否则进入下一步。
(3)发送方发送文件数据,接收方接收文件数据。
(4)如果文件传送过程中,接收方或发送方掉线或者取消文件传送,则文件传送被中断,流程结束。如果文件传送过程一直正常,则到最后完成文件的传送。
有几点需要说明一下:
(1)发送方可以是客户端,也可以是服务器;接收方也是如此。但无论发送方和接收方的类别如何,它们都遵守这一文件传送流程;就像ESFramework所有的通信引擎都公用同一套消息处理骨架流程一样。
(2)当接收方同意接收后,框架会自动搜索是否存在匹配的续传项目,若存在,则会启动断点续传。当然,我们可以通过文件接收管理器的属性来控制断点续传功能是否开启。关于断点续传的更多内容,可以参考ESFramework 4.0 文件断点续传原理与实现。
(3)进行文件传送的线程是由框架自动控制的,只要发送方收到了接收方同意接收的回复,框架就会自动在后台线程中发送文件数据包;同样,此时接收方也会自动处理接收到的文件数据包。
(4)发送方或接收方都可随时取消正在传送的文件。
(5)当文件传送被中断或完成时,发送方和接收方都会有相应的事件通知。
二.ESPlus用于支持文件传送的基础设施
1.TransmittingFileInfo
无论是发送方还是接收方,针对每个文件传送任务,都需要有个对象来表示它,TransmittingFileInfo便是一个文件传送项目的封装,里面包含了类似发送者ID、接收者ID、文件名称等相关信息。
TransmittingFileInfo的大部分属性对于发送方和接收方都是有效的,而有几个属性只对发送方有效(比如SendingFileParas),有几个属性只对接收方有效(如LocalSaveFilePath),这些在帮助文档中都有详细的说明。而且,有些属性(如OriginFileLastUpdateTime)的存在是用于支持断点续传功能的。
2.FileTransDisrupttedType
ESPlus使用FileTransDisrupttedType枚举定义了所有可能导致文件传送中断的原因:
{
/// <summary>
/// 自己主动取消
/// </summary>
ActiveCancel,
/// <summary>
/// 对方取消
/// </summary>
DestCancel,
/// <summary>
/// 对方掉线
/// </summary>
DestOffline,
/// <summary>
/// 网络中断、自己掉线
/// </summary>
SelfOffline,
/// <summary>
/// 对方拒绝接收文件
/// </summary>
DestReject,
/// <summary>
/// 其它原因,如文件读取失败等
/// </summary>
OtherCause
}
3.IFileTransferingEvents 接口
ESPlus定义了IFileTransferingEvents接口,用于暴露所有与文件传送相关的状态和事件:
{
/// <summary>
/// 当某个文件开始传送时,触发该事件。
/// </summary>
event CbGeneric<TransmittingFileInfo> FileTransStarted;
/// <summary>
/// 当某个文件续传开始时,触发该事件。(将不再触发FileTransStarted事件)
/// </summary>
event CbGeneric<TransmittingFileInfo> FileResumedTransStarted;
/// <summary>
/// 文件传送的进度。参数为fileID(文件编号) ,total(文件大小) ,transfered(已传送字节数)
/// </summary>
event CbFileSendedProgress FileTransProgress;
/// <summary>
/// 文件传送中断时,触发该事件。
/// </summary>
event CbGeneric<TransmittingFileInfo, FileTransDisrupttedType> FileTransDisruptted;
/// <summary>
/// 文件传送完成时,触发该事件。
/// </summary>
event CbGeneric<TransmittingFileInfo> FileTransCompleted;
}
通过预定这些事件,我们可以知道每个传送的文件什么时候开始(或断点续传)、什么时候完成、传递的实时进度、传送中断的原因等等。要注意的是,这些事件都是在后台线程中触发的,如果在事件处理函数中需要更新UI,则需要将调用转发到UI线程。
4.SendingFileParas
该对象仅仅包含两个属性:SendingSpanInMSecs和FilePackageSize。发送方可以通过SendingFileParas对象来指定发送文件数据包时的频率与每个数据包的大小。一般来说,为了达到最快的传送速度,SendingSpanInMSecs可以设为0。而FilePackageSize的大小则要根据发送方与接收方的网络环境的好坏进行决定,在Internet上,一般可以设为2048或4096左右;而在局网内,可以设为204800甚至更大(在局网的传送速度可以达到30M/s以上)。
5.IFileController
通过ESPlus.Application.FileTransfering.IFileController接口,我们可以提交发送文件的请求,并且可以主动取消正在接收或发送的文件。IFileController即可用于客户端也可用户服务端。
{
/// <summary>
/// 该事件接口暴露了所有正在发送文件的实时状态。
/// </summary>
IFileTransferingEvents FileSendingEvents { get; }
/// <summary>
/// 该事件接口暴露了所有正在接收的文件的实时状态。
/// </summary>
IFileTransferingEvents FileReceivingEvents { get; }
/// <summary>
/// 准备发送文件。如果对方同意接收,则后台会自动发送文件;如果对方拒绝接收,则会取消发送。
/// </summary>
/// <param name="accepterID">接收文件的用户ID</param>
/// <param name="filePath">被发送文件的路径</param>
/// <param name="comment">其它附加备注。如果是在类似FTP的服务中,该参数可以是保存文件的路径</param>
/// <param name="fileID">返回即将发送文件的唯一编号</param>
void BeginSendFile(string accepterID, string filePath, string comment, out string fileID);
/// <summary>
/// 准备发送文件。如果对方同意接收,则后台会自动发送文件;如果对方拒绝接收,则会取消发送。
/// </summary>
/// <param name="accepterID">接收文件的用户ID</param>
/// <param name="filePath">被发送文件的路径</param>
/// <param name="comment">其它附加备注。如果是在类似FTP的服务中,该参数可以是保存文件的路径</param>
/// <param name="paras">发送参数设定。传入null,表示采用IFileSenderManager的默认设置。</param>
/// <param name="fileID">返回即将发送文件的唯一编号</param>
void BeginSendFile(string accepterID, string filePath, string comment,SendingFileParas paras, out string fileID);
/// <summary>
/// 主动取消正在发送或接收的文件,并通知对方。
void CancelFileTransfering(string fileID);
/// <summary>
/// 取消与某个用户相关的正在传送项目。
/// <summary>
/// 获取正在发送或接收中的文件的相关信息。
/// </summary>
TransmittingFileInfo GetFileInfo(string fileID);
}
(1)BeginSendFile用于向接收方提交发送文件的请求,如果对方同意,则后台会自动开始传递文件。该方法有个out参数fileID,用于传出标记该文件传送项目的唯一编号,比如,你打算将同一个文件发送给两个好友,将会调用两次BeginSendFile方法,而两次得到的fileID是不一样的。也就是说,fileID是用于标记文件传送项目的,而不是标记文件的。
该方法有两个重载,区别在于第二个BeginSendFile方法多了一个SendingFileParas参数,用于主动控制文件数据包的大小和发送频率。
在客户端使用时,BeginSendFile方法不仅可以向其他在线用户提交发送文件的请求,也可以直接向服务器提交发送文件的请求 -- 即此时文件的接收者为服务端。我们只需要将accepterID参数传入NetServer.SystemUserID,以指明由服务端而不是其他用户来接收即将发送的文件。
(2)GetFileInfo方法可以获取任何一个正在发送或正在接收的项目信息。
(3)CancelFileTransfering方法用于取消正在发送或接收的某个文件传送项目,调用该方法时,框架会自动通知文件传送的另一端用户,并触发FileReceivingEvents或FileSendingEvents中的FileTransDisruptted事件,而另一端也会自动触发FileTransDisruptted事件。
(4)CancelFileTransferingAbout方法用于取消与某个指定用户相关的正在传送项目。比如,我们正在与aa01用户聊天,并且与aa01有多个文件正在传送,此时,如果要关闭与aa01的聊天窗口,那么关闭之前,通常会先调用CancelFileTransferingAbout方法来取消与aa01相关的所有文件传送。所以你经常会看到类似的提示:“您与aa01有文件正在传送中,关闭当前窗口将导致正在传送的文件中断,您确定要关闭吗?”。如果用户确认关闭,此时就正是我们要调用CancelFileTransferingAbout方法的时候了。
(5)FileSendingEvents属性用于暴露自己作为发送者的所有正在进行的文件传送项目的实时状态;FileReceivingEvents属性用于暴露自己作为接收者的所有正在进行的文件传送项目的实时状态。
6.IFileHandler
IFileHandler接口将被框架回调以实现文件传送机制。同IFileController一样,其既可用于客户端也可用户服务端。
我们需要实现ESPlus.Application.FileTransfering.IFileHandler接口来获取与文件传送请求相关通知:
{
/// <summary>
/// 是否同意接收文件?
/// <param name="senderID">发送者的ID。如果为NetServer.SystemUserID,则表示是服务端发送的。</param>
/// <param name="fileName">文件名称。</param>
/// <param name="fileLength">文件大小。</param>
/// <param name="comment">其它附加备注。如果是在类似FTP的服务中,该参数可以是保存文件的路径</param>
/// <param name="fileID">文件ID。</param>
/// <param name="resumedFileItem">如果能续传,则不为null。</param>
/// <returns>返回值为保存文件的路径,如果为null,表示拒绝接收/拒绝续传文件。如果参数resumedFileItem不为null,而且返回路径等于resumedFileItem.LocalFileSavePath,则表示续传;否则表示另存。</returns>
string ReadyToAcceptFile(string senderID, string fileName, long fileLength, string comment, string fileID, ResumedFileItem resumedFileItem);
/// <summary>
/// 接收者对自己发送文件请求的回复 -- 同意/拒绝接收。
/// <param name="info">文件传送项目的相关信息</param>
/// <param name="agreed">对方是否同意</param>
void OnResponseOfReceiver(TransmittingFileInfo info ,bool agreed);
}
(1)ReadyToAcceptFile方法是当前用户作为接收方时被框架回调的;而OnResponseOfReceiver方法是当前用户作为发送方时,被框架回调的。
(2)IFileHandler接口的两个方法都将在后台线程中被框架调用,如果实现该方法时需要刷新应用程序的UI,则注意一定要转发到UI线程。
(3)当发送方提交了发送文件的请求后,框架会在接收方回调ReadyToAcceptFileAsyn方法以询问是否同意接收,如果同意,ReadyToAcceptFileAsyn应返回有效的存储接收文件的路径,否则,返回null。
(4)当接收方同意或拒绝接收文件,框架会在发送方回调OnResponseOfReceiver方法以通知发送者。通常,应用程序在实现OnResponseOfReceiver方法时,最多只需要告知文件发送者,而不需要再做任何其它的额外处理。因为框架已经帮你打理好了一切。
当接收方同意接收文件后,与该文件传送项目相关的事件会通过IFileOutter暴露的IFileTransferingEvents接口相继触发。
三.客户端
同ESPlus的Basic应用或CustomizeInfo应用一样,在客户端支持文件传送功能也需要使用到相应的“Outter”组件和实现相应的“BusinessHandler”接口。
1.IFileOutter
ESPlus.Application.FileTransfering.Passive.IFileOutter接口从IFileController继承,并增加了一个属性和一个方法:
{
/// <summary>
/// 发送文件数据包时所采用的消息优先级。
/// </summary>
DataPriority DataPriority4SendingFile { get; set; }
/// <summary>
/// 初始化文件传送查看器控件。
/// </summary>
/// <param name="viewer">文件传送查看器控件对象</param>
/// <param name="destUserID">目标用户的ID。返回的查看器将显示与该用户相关的所有文件传送状态。如果传入null,则显示与任何用户的文件传送的实时状态。</param>
void InitializeFileTransferingViewer(FileTransferingViewer viewer, string destUserID);
}
(1)我们可以通过设置DataPriority4SendingFile属性以控制发送文件数据包的优先级,在一般系统中,可以将其设置为Common或Low,但绝不能设置为CanBeDiscarded,否则将可能导致接收方接收到的文件不完整。
(2)ESPlus提供了默认的传送项目的状态查看器控件FileTransferingViewer,如果没有特殊需求,大家在项目中可以直接使用它来显示文件传送的实时状态,它的界面截图如下所示:
你只需要把这个控件拖拽到你的UI上,然后将其传入IFileOutter的InitializeFileTransferingViewer方法进行初始化后,它就会正常工作了。
InitializeFileTransferingViewer方法的第二个参数destUserID表示当前的FileTransferingViewer控件要显示与哪个好友相关的所有文件传送项目的状态。以QQ作类比,你同时在与多个好友传送文件,那么就会有多个聊天窗口,每个聊天窗口都会有一个FileTransferingViewer实例,而这个FileTransferingViewer实例仅仅显示与当前聊天窗口对应的好友的传送项目。这样依赖,你与aa01用户传送文件的进度查看器就不会在你与aa02的聊天窗口上显示出来。
如果你的FileTransferingViewer查看器需要捕捉所有正在传送的项目的实时状态,那么,调用InitializeFileTransferingViewer方法时,destUserID参数可以传入null。
另外,FileTransferingViewer实现了IFileTransferingViewer接口:
{
/// <summary>
/// 当某个文件开始续传时,触发该事件。参数为FileName - isSend
/// </summary>
event CbGeneric<string, bool> FileResumedTransStarted;
/// <summary>
/// 当某个文件传送完毕时,触发该事件。参数为FileName - isSend
/// </summary>
event CbGeneric<string, bool> FileTransCompleted;
/// <summary>
/// 当某个文件传送中断时,触发该事件。参数为FileName - isSend - FileTransDisrupttedType
/// </summary>
event CbGeneric<string, bool, FileTransDisrupttedType> FileTransDisruptted;
/// <summary>
/// 当某个文件传送开始时,触发该事件。参数为FileName - isSend
/// </summary>
event CbGeneric<string, bool> FileTransStarted;
/// <summary>
/// 当所有文件都传送完成时,触发该事件。
/// </summary>
event CbSimple AllTaskFinished;
/// <summary>
/// 当点击取消按钮终止某个文件传送时,触发该事件。
/// </summary>
event CbCancelFile CancelFileButtonClicked;
/// <summary>
/// 当前是否有文件正在传送中。
/// </summary>
bool IsFileTransfering();
}
你也可以通过该接口来关注FileTransferingViewer查看器捕捉到的(正如前所述,不一定是全部)文件传送项目的状态,而且,该接口的事件都是在UI线程中触发的,你可以直接在其处理函数中操控UI显示。
2.IFileBusinessHandler
客户端的ESPlus.Application.FileTransfering.Passive.IFileBusinessHandler直接从IFileHandler继承,而且没有增加额外的内容:
{
}
四.服务端
服务端也可以接收客户端发送的文件(即上传),甚至可以发送文件给客户端(即下载),它遵循同样的文件传送流程。
1.IFileTransferingController
如果需要服务端也参与到文件的发送与接收中来,则同客户端一样,服务端的ESPlus.Application.FileTransfering.Server.IFileTransferingController接口也从IFileController继承,以提交文件发送请求、或取消正在发送中的文件等,这里就不重复解释了。
2.IFileBusinessHandler
服务端需要实现ESPlus.Application.FileTransfering.Server.IFileBusinessHandler接口,也是直接从IFileHandler继承,而且没有增加额外的内容。
五.Rapid引擎对文件传送的支持
新版本的Rapid引擎(可以从ESFramework 4.0 概述文末下载 ),增加了对文件传送的支持,使用Rapid引擎的朋友可以很方便的利用框架提供的文件传送功能。
1.客户端
IRapidPassiveEngine的Initialize方法增加了一个重载:
该重载增加了一个IFileBusinessHandler参数,用于支持文件传送机制。
另外,IRapidPassiveEngine增加了一个IFileOutter只读属性,通过暴露的该属性,我们就可以提交发送文件的请求或监控文件传送的状态了。当然,如果在Initialize方法中没有传入有效的IFileBusinessHandler引用,则FileOutter属性将返回null。
2.服务端
同客户端一样,服务端IRapidServerEngine的Initialize方法也增加了一个重载:
IRapidServerEngine提供了IFileTransferingController只读属性,用于提交发送文件的请求或监控文件传送的状态。同样的,如果在Initialize方法中没有传入有效的IFileBusinessHandler引用,则FileTransferingController属性将返回null。
Rapid引擎已经为我们组装好了与文件传送相关的所有组件,我们直接使用即可。如果后面有时间,我们会更深入地剖析ESPlus的文件传送机制背后的原理,以及如何一步步地搭建起文件传送的功能。
ESPlus使得在基于ESFramework的通信系统中增加文件传送的功能变得如此简单,甚至,基于ESPlus提供的文件传送功能,我们可以很快地实现文件服务器,以支持文件上传和下载。