ESFramework 开发手册(03) -- 文件(夹)、流(Stream)传送
本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器中的第三个:文件传送。在最新ESFramework 6.6版本中,增加了对流Stream传送的支持,即可以将一个Stream发送给对方,也可以将接收的文件写入到一个Stream中。
在很多分布式系统中,都有文件传送的需求。ESPlus内置了文件传送(支持自动断点续传)的功能,通过ESPlus.Application.FileTransfering命名空间提供相关服务。最新的ESPlus不仅支持传送单个文件,还支持传送整个文件夹,而且,传送文件夹与传送文件采用完全相同的API和模型。
一.ESPlus的文件传送流程
ESPlus定义了文件传送的标准流程,可以用下图表示:
(1) 由发送方发起传送文件的请求。
(2) 接收方回复同意或者拒绝接收文件。如果拒收,则流程结束;否则进入下一步。
(3) 发送方发送文件数据,接收方接收文件数据。
(4) 如果文件传送过程中,接收方或发送方掉线或者取消文件传送,则文件传送被中断,流程结束。如果文件传送过程一直正常,则到最后完成文件的传送。
有几点需要说明一下:
(1) 发送方可以是客户端,也可以是服务器;接收方也是如此。但无论发送方和接收方的类别如何,它们都遵守这一文件传送流程。
(2) 当接收方同意接收后,框架会自动搜索是否存在匹配的续传项目,若存在,则会启动断点续传。
(3) 进行文件传送的线程是由框架自动控制的,只要发送方收到了接收方同意接收的回复,框架就会自动在后台线程中发送文件数据包;而接收方也会自动处理接收到的文件数据包。
(4) 发送方或接收方都可随时取消正在传送的文件。
(5) 当文件传送被中断或完成时,发送方和接收方都会有相应的事件通知。
二.用于支持文件传送的基础设施
1. TransferingProject
无论是发送方还是接收方,针对每个文件传送任务,都有一个对象来表示它,ESPlus.FileTransceiver.TransferingProject便是一个文件传送项目的封装,里面包含了类似发送者ID、接收者ID、文件名称、是否已经开始传送,等等相关信息。
TransferingProject的大部分属性对于发送方和接收方都是有效的,而有几个属性只对发送方有效(比如SendingFileParas),有几个属性只对接收方有效(如LocalSaveFilePath),这些在帮助文档中都有详细的说明。而且,有些属性(如OriginFileLastUpdateTime)的存在是用于支持断点续传功能的。
2. FileTransDisrupttedType
ESPlus使用FileTransDisrupttedType枚举定义了所有可能导致文件传送中断的原因:
public enum FileTransDisrupttedType { ///<summary> /// 接收方拒绝接收 ///</summary> RejectAccepting, ///<summary> /// 自己主动取消 ///</summary> ActiveCancel, ///<summary> /// 对方取消 ///</summary> DestCancel, ///<summary> /// 对方掉线 ///</summary> DestOffline, ///<summary> /// 与对方的可靠的P2P通道关闭 ///</summary> ReliableP2PChannelClosed, ///<summary> /// 网络中断、自己掉线 ///</summary> SelfOffline, ///<summary> /// 对方拒绝接收文件 ///</summary> DestReject, ///<summary> /// 自己系统内部错误,如文件读取失败等 ///</summary> InnerError, ///<summary> /// 对方系统内部错误,如文件读取失败等 ///</summary> DestInnerError }
当文件传送通过P2P通道进行时,如果P2P通道意外中断,则文件传送也会中断,其中断的原因就是枚举中的ReliableP2PChannelClosed。
3. IFileTransferingEvents 接口
ESPlus定义了IFileTransferingEvents接口,用于暴露所有与文件传送相关的状态和事件:
{
///<summary>
/// 当某个文件开始传送时,触发该事件。
///</summary>
eventCbGeneric<TransferingProject> FileTransStarted;
///<summary>
/// 当某个文件续传开始时,触发该事件。(将不再触发FileTransStarted事件)
///</summary>
eventCbGeneric<TransferingProject> FileResumedTransStarted;
///<summary>
/// 文件传送的进度。参数为fileID(文件编号) ,total(文件大小) ,transfered(已传送字节数)
///</summary>
eventCbFileSendedProgress FileTransProgress;
///<summary>
/// 文件传送中断时,触发该事件。
///</summary>
eventCbGeneric<TransferingProject, FileTransDisrupttedType> FileTransDisruptted;
///<summary>
/// 文件传送完成时,触发该事件。
///</summary>
eventCbGeneric<TransferingProject> FileTransCompleted;
}
通过预定这些事件,我们可以知道每个传送的文件什么时候开始(或断点续传)、什么时候完成、传递的实时进度、传送中断的原因等等。要注意的是,这些事件都是在后台线程中触发的,如果在事件处理函数中需要更新UI,则需要将调用转发到UI线程。
4. SendingFileParas
该对象仅仅包含两个属性:SendingSpanInMSecs和FilePackageSize。发送方可以通过SendingFileParas对象来指定发送文件数据包时的频率与每个数据包的大小。一般来说,为了达到最快的传送速度,SendingSpanInMSecs可以设为0。而FilePackageSize的大小则要根据发送方与接收方的网络环境的好坏进行决定,在Internet上,一般可以设为2048或4096左右;而在局网内,可以设为204800甚至更大(在局网的传送速度可以达到30M/s以上)。
5. IBaseFileController
通过ESPlus.Application.FileTransfering.IBaseFileController接口,我们可以提交发送文件的请求,并且可以主动取消正在接收或发送的文件。IBaseFileController即可用于客户端也可用户服务端。
{
/// <summary>
/// 当文件接收方收到了来自发送方发送文件(夹)的请求时,触发此事件。该事件将在后台线程中触发,如果处理该事件时需要刷新UI,则需要转发到UI线程。
/// 当接收方确定要接收或拒绝文件时,请调用BeginReceiveFile方法或RejectFile方法。
/// </summary>
event CbFileRequestReceived FileRequestReceived;
/// <summary>
/// 当文件接收方回复了同意/拒绝接收文件(夹)时,在发送方触发此事件。参数为 TransmittingProject - bool(同意?)。可以通过参数TransmittingProject的AccepterID属性得知接收方的UserID。
/// 通常,客户端预定该事件,只需要告知文件发送者,而不需要再做任何额外处理。该事件将在后台线程中触发,如果处理该事件时需要刷新UI,则需要转发到UI线程。
/// </summary>
event CbGeneric<TransferingProject, bool> FileResponseReceived;
/// <summary>
/// 该事件接口暴露了所有正在发送文件(夹)的实时状态。
/// </summary>
IFileTransferingEvents FileSendingEvents { get; }
/// <summary>
/// 该事件接口暴露了所有正在接收的文件(夹)的实时状态。
/// </summary>
IFileTransferingEvents FileReceivingEvents { get; }
/// <summary>
/// 发送方准备发送文件(夹)。目标用户必须在线。如果对方同意接收,则后台会自动发送文件(夹);如果对方拒绝接收,则会取消发送。(通过FileResponseReceived事件,可以得知对方是否同意接收。)
/// </summary>
/// <param name="accepterID">接收文件(夹)的用户ID</param>
/// <param name="fileOrDirPath">被发送文件(夹)的路径</param>
/// <param name="comment">其它附加备注。如果是在类似FTP的服务中,该参数可以是保存文件(夹)的路径</param>
/// <param name="projectID">返回文件传送项目的编号</param>
void BeginSendFile(string accepterID, string fileOrDirPath, string comment, out string projectID);
/// <summary>
/// 发送方准备发送文件(夹)。目标用户必须在线。如果对方同意接收,则后台会自动发送文件;如果对方拒绝接收,则会取消发送。(通过FileAnswerReceived事件,可以得知对方是否同意接收。)
/// </summary>
/// <param name="accepterID">接收文件(夹)的用户ID</param>
/// <param name="fileOrDirPath">被发送文件(夹)的路径</param>
/// <param name="comment">其它附加备注。如果是在类似FTP的服务中,该参数可以是保存文件(夹)的路径</param>
/// <param name="paras">发送参数设定。传入null,表示采用IFileSenderManager的默认设置。</param>
/// <param name="projectID">返回文件传送项目的编号</param>
void BeginSendFile(string accepterID, string fileOrDirPath, string comment, SendingFileParas paras, out string projectID);
/// <summary>
/// 接收方如果同意接收文件(夹),则调用该方法。
/// </summary>
/// <param name="projectID">文件传送项目的编号</param>
/// <param name="savePath">存储文件(夹)的路径。请特别注意,如果已经存在同名的文件(夹),将覆盖之。</param>
void BeginReceiveFile(string projectID, string savePath);
/// <summary>
/// 接收方如果拒绝接收文件(夹),则调用该方法。
/// </summary>
/// <param name="projectID">文件传送项目的编号</param>
void RejectFile(string projectID);
/// <summary>
/// 获取与目标用户相关的所有文件传送项目的projectID的列表(包括未被接收方回复的传送项目)。
/// </summary>
/// <param name="destUserID">目标用户ID。如果为null,则表示获取所有正在传送项目的projectID。</param>
/// <returns>projectID的列表</returns>
List<string> GetTransferingAbout(string destUserID);
/// <summary>
/// 主动取消正在发送或接收的文件(夹)(包括未被接收方回复的传送项目),并通知对方。
/// </summary>
void CancelTransfering(string projectID);
/// <summary>
/// 取消与目标用户相关的正在传送项目(包括未被接收方回复的传送项目)。
/// </summary>
/// <param name="destUserID">目标用户ID。如果为null,则表示取消所有正在传送项目。</param>
void CancelTransferingAbout(string destUserID);
/// <summary>
/// 获取正在发送或接收中的文件传送项目(包括未被接收方回复的传送项目)。如果不存在目标项目,则返回null。
/// </summary>
TransferingProject GetTransferingProject(string projectID);
}
请求传送
- BeginSendFile用于向接收方提交发送文件的请求,如果对方同意,则后台会自动开始传递文件。该方法有个out参数projectID,用于传出标记该文件传送项目的唯一编号,比如,你打算将同一个文件发送给两个好友,将会调用两次BeginSendFile方法,而两次得到的projectID是不一样的。也就是说,projectID是用于标记文件传送项目的,而不是标记文件的。该方法有两个重载,区别在于第二个BeginSendFile方法多了一个SendingFileParas参数,用于主动控制文件数据包的大小和发送频率。
在客户端使用时,BeginSendFile方法不仅可以向其他在线用户提交发送文件的请求,也可以直接向服务器提交发送文件的请求 -- 即此时文件的接收者为服务端。我们只需要将accepterID参数传入NetServer.SystemUserID,以指明由服务端而不是其他用户来接收即将发送的文件。 - 当发送方调用了BeginSendFile方法后,接收方会触发FileRequestReceived事件,事件参数包含了与此次文件传送相关的详细信息,请特别注意其ResumedProjectItem参数,若该参数的值不为null,则表示发现了续传项目,可以启用续传,而接下来是要续传还是重新传送,取决于接收方调用BeginReceiveFile方法时传入的allowResume参数的值。
- 另外,由于FileRequestReceived事件是在后台线程中被框架调用的,如果处理该事件的方法中需要刷新应用程序的UI,则注意一定要转发到UI线程。
同意/拒绝接收
- 如果接收方同意接收文件,则应该调用BeginReceiveFile方法;否则,调用RejectFile方法。注意,有可能在调用BeginReceiveFile方法或RejectFile方法之前,发送方已经取消了文件的发送(此时,会在接收方会触发FileReceivingEvents属性的FileTransDisruptted事件)。
- 无论接收方是调用BeginReceiveFile方法还是调用RejectFile方法,都会在发送方触发FileResponseReceived事件,事件的第二个bool参数表明了对方是同意还是拒绝接收文件;还可以通过事件的第一个参数TransferingProject的AccepterID属性得知接收方的UserID。应用程序在处理FileResponseReceived事件时,最多只需要告知文件发送者,而不需要再做任何其它的额外处理,因为框架已经帮你打理好了一切。
- 当接收方同意接收文件后,与该文件传送项目相关的事件会通过IFileTransferingEvents接口(FileSendingEvents和FileReceivingEvents属性)相继触发。
- 当接收方调用RejectFile方法拒绝接收文件时,发送方会触发FileSendingEvents的FileTransDisruptted事件,接收方会自己也会触发FileReceivingEvents的FileTransDisruptted事件。且触发的这两个事件的第二个参数的值都是FileTransDisrupttedType.RejectAccepting。
获取传送状态
- GetTransferingProject方法可以获取任何一个正在发送或正在接收的项目信息,该方法也可获取一个还未被接收方回复的文件传送项目的信息。通过TransferingProject的IsTransfering属性,我们可以间接地知道接收方对发送请求是否给出了回复。如果IsTransfering为false,则表示接收方还未给出回复,文件传送还未开始;反之亦然。
- GetTransferingAbout方法可以获取与目标用户相关的所有文件传送项目的projectID的列表,其中还包括那些还未被接收方回复的文件传送项目。
- FileSendingEvents属性用于暴露自己作为发送者的所有正在进行的文件传送项目的实时状态;而FileReceivingEvents属性用于暴露自己作为接收者的所有正在进行的文件传送项目的实时状态。
取消传送项目
- CancelTransfering方法用于取消正在发送或接收的某个文件传送项目,该方法还可以取消已经请求发送但还未收到接收方回复的文件传送项目。调用该方法时,框架会自动通知文件传送的另一端用户,并触发FileReceivingEvents或FileSendingEvents中的FileTransDisruptted事件,而另一端也会自动触发FileTransDisruptted事件。
- CancelTransferingAbout方法用于取消与某个指定用户相关的正在传送项目(包括已经请求发送但还未收到接收方回复的文件传送项目)。比如,我们正在与aa01用户聊天,并且与aa01有多个文件正在传送,此时,如果要关闭与aa01的聊天窗口,那么关闭之前,通常会先调用CancelTransferingAbout方法来取消与aa01相关的所有文件传送。所以你经常会看到类似的提示:“您与aa01有文件正在传送中,关闭当前窗口将导致正在传送的文件中断,您确定要关闭吗?”。如果用户确认关闭,此时就正是我们要调用CancelTransferingAbout方法的时候了。
- 要特别注意一点,对于那些已经发送文件请求但还未收到接收方回复的文件传送项目,其与正在传送的文件项目采用的是相同的处理模式。我们可以通过TransferingProject的IsTransfering属性来区分这两种类型。
三.断点续传
1. 断点续传是由接收方来管理的。
2. 如果之前用户A发送给用户B的某个文件传送到一半时中断了(可能是因为主动取消、或网络断开等),那么,现在A又发送文件给B,当B端程序检测到以下条件满足时,则会启动续传:
(1)A发送的是同一个文件。即被发送文件的绝对路径、文件大小、文件的最后修改时间, 这三个要素完全一致。
(2)本次发送文件的请求距离上次中断的时间间隔不超过5分钟。
(3)在此期间,接收方B的程序没有重启过,也没有调用过Rapid引擎的Initialize方法。
(4)B方上次接收中断对应的临时文件(扩展名.tmpe$)没有被手动或其它程序删除。
(5)B同意接收文件,且存放接收文件的路径与上次选择的路径完全一致。
3. 当上面的前(1)(2)(3)(4)满足时,B端触发的FileRequestReceived事件的ResumedProjectItem参数值就不为null,表示可以开启续传。
4. 即使达到了续传的条件,B仍然可以要求重新传送整个文件,只要B在同意接收文件时,调用BeginReceiveFile方法传入的allowResume参数的值为false即可。
四.客户端 IFileOutter
同ESPlus的Basic应用或CustomizeInfo应用一样,在客户端支持文件传送功能需要使用到相应的“Outter”组件IFileOutter。
客户端通过ESPlus.Application.FileTransfering.Passive.IFileOutter接口提供的方法来提交发送文件请求等操作。我们可以从ESPlus.Rapid.IRapidPassiveEngine暴露的FileOutter属性来获取IFileOutter引用。IFileOutter接口直接从IBaseFileController继承,且未增加任何新的内容:
{
ESFramework.Boost(源码开放) 提供了默认的传送项目的状态查看器控件ESFramework.Boost.Controls.FileTransferingViewer,如果没有特殊需求,大家在项目中可以直接使用它来显示文件传送的实时状态,它的界面截图如下所示:
你只需要把这个控件拖拽到你的UI上,然后将IFileOutter传入FileTransferingViewer的Initialize方法,它就可以正常工作了。
FileTransferingViewer的Initialize方法的第一个参数friendUserID表示当前的FileTransferingViewer控件要显示与哪个好友相关的所有文件传送项目的状态。以QQ作类比,你同时在与多个好友传送文件,那么就会有多个聊天窗口,每个聊天窗口都会有一个FileTransferingViewer实例,而这个FileTransferingViewer实例仅仅显示与当前聊天窗口对应的好友的传送项目。如此一来,你与aa01用户传送文件的进度查看器就不会在你与aa02的聊天窗口上显示出来。
如果你的FileTransferingViewer需要捕捉所有正在传送的项目的实时状态,那么,调用其Initialize方法时,friendUserID参数传入null就可以了。另外,FileTransferingViewer实现了IFileTransferingViewer接口:
{
/// 当某个文件开始续传时,触发该事件。参数为FileName - isSending
/// </summary>
event CbGeneric<string, bool> FileResumedTransStarted;
/// <summary>
/// 当某个文件传送完毕时,触发该事件。参数为FileName - isSending
/// </summary>
event CbGeneric<string, bool> FileTransCompleted;
/// <summary>
/// 当某个文件传送中断时,触发该事件。参数为FileName - isSending - FileTransDisrupttedType
/// </summary>
event CbGeneric<string, bool, FileTransDisrupttedType> FileTransDisruptted;
/// <summary>
/// 当某个文件传送开始时,触发该事件。参数为FileName - isSending
/// </summary>
event CbGeneric<string, bool> FileTransStarted;
/// <summary>
/// 当所有文件都传送完成时,触发该事件。
/// </summary>
event CbSimple AllTaskFinished;
/// <summary>
/// 当前是否有文件正在传送中。
/// </summary>
bool IsFileTransfering { get; }
}
你也可以通过该接口来关注FileTransferingViewer查看器捕捉到的(正如前所述,不一定是全部)文件传送项目的状态,而且,该接口的事件都是在UI线程中触发的,你可以直接在其处理函数中操控UI显示。
五. 服务端IFileController
服务端也可以接收客户端发送的文件(即上传),甚至可以发送文件给客户端(即下载),它遵循同样的文件传送流程。如果需要服务端也参与到文件的发送与接收中来,可以使用服务端的ESPlus.Application.FileTransfering.Server. IFileController接口,直接从IBaseFileController接口继承,而且未增加任何新的内容。我们可以从IRapidServerEngine暴露的FileController属性来获取IFileController引用。
六. 对流Stream的支持
ESFramework v6.6+ 可以发送一个Stream,或者将接收的文件写入到一个Stream中。也就是说,发送方或接收方都可以是文件或Stream。为了做到这点,IBaseFileController 增加了如下两个方法:
/// <summary> /// 发送方准备发送流。目标用户必须在线。如果对方同意接收,则后台会自动发送;如果对方拒绝接收,则会取消发送。(通过FileResponseReceived事件,可以得知对方是否同意接收。) /// </summary> /// <param name="accepterID">接收流的用户ID</param> /// <param name="stream">被发送的流。请特别注意,调用该方法前请保证流已经被打开;另外,发送中断或完成,ESFramework并不会关闭该流。</param> /// <param name="projectName">项目的名称</param> /// <param name="size">要发送的字节数</param> /// <param name="comment">其它附加备注。</param> /// <param name="paras">发送参数设定。传入null,表示采用IFileSenderManager的默认设置。</param> /// <param name="projectID">返回文件传送项目的编号</param> void BeginSendFile(string accepterID, Stream stream, string projectName, ulong size, string comment, SendingFileParas paras, out string projectID); /// <summary> /// 接收方如果同意接收文件(夹)或流,则调用该方法,该方法会将接收的数据写入到目标流(saveStream)中。 /// </summary> /// <param name="projectID">文件传送项目的编号</param> /// <param name="saveStream">存储文件的流。请特别注意,调用该方法前请保证流已经被打开;另外,接收中断或完成,ESFramework并不会关闭该流。</param> void BeginReceiveFile(string projectID, Stream saveStream);
您可以通过查看文件传送demo以进一步了解如何使用相关的API。
下一篇:ESFramework 开发手册(04) -- 可靠的P2P
上一篇:ESFramework 开发手册(02) -- 在线用户管理、基础功能及状态通知
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008