C#文件断点续传
文件采取分块发送,每块单独校验,能够保证文件的完整性.同时还提供磁盘缓存功能.
经过实际测试,通过局域网(有线和WiFi)传送一个5G左右的文件取得成功.
最大缺点是CPU占用率过高,测试中发送端(939AMD3000+)达到40%,接收端(双核T9600、939AMD3200+)分别为15%和35%左右.
性能确实还有待改进....
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TransferFilesClient//进行文件传输的类
{//Version 0.6
#region CRC32算法
/// <summary>
/// CRC32快速检测算法
/// 本文来自****博客,转载请标明出处:http://blog.****.net/ZZJ_4Ever/archive/2009/03/31/4038551.aspx
/// 有稍作修改
/// </summary>
public static class CRC32
{
static UInt32[] crcTable = {
0x0, 0x77073096, 0xee0e612c, 0x990951ba, 0x76dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0xedb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x9b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x1db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x6b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0xf00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x86d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x3b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x4db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0xd6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0xa00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x26d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x5005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0xcb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0xbdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
};
public static int GetCRC32(byte[] bytes)
{
int iCount = bytes.Length;
UInt32 crc = 0xFFFFFFFF;
for (int i = 0; i < iCount; i++)
{
crc = ((crc >> 8) & 0x00FFFFFF) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
}
UInt32 temp = crc ^ 0xFFFFFFFF;
int t = (int)temp;
return (t);
}
}
#endregion
#region 一些常量和扩展方法
/// <summary>
/// 一些常量和扩展方法
/// </summary>
public static class Consts
{
/// <summary>
/// 文件区块数据标头
/// </summary>
public const byte FileBlockHeader = 0;
/// <summary>
/// 字符串信息标头
/// </summary>
public const byte StringHeader = 1;
/// <summary>
/// 分块大小1MB
/// </summary>
public const int BlockSize = 1048576;
/// <summary>
/// 网络上传送的数据包最大大小
/// </summary>
public const int NetBlockMaxSize = BlockSize + 9;
/// <summary>
/// 默认磁盘缓存大小(单位:区块数)
/// </summary>
public const int DefaultIOBufferSize = 8;
/// <summary>
/// 空格
/// </summary>
public const string Space = " ";
/// <summary>
/// 空格替代符
/// </summary>
public const string SpaceReplacement = @"<SPACE>";
/// <summary>
/// 获取校验值
/// </summary>
/// <param name="bytes">输入数据</param>
/// <returns>输出的校验值</returns>
public static byte[] GetHash(this byte[] bytes)
{
return BitConverter.GetBytes(CRC32.GetCRC32(bytes));
}
/// <summary>
/// 比较两二进制数据内容是否完全相同(用于MD5值的比较)
/// </summary>
/// <param name="THIS">数据一</param>
/// <param name="obj">数据二</param>
public static bool BytesEqual(this byte[] THIS, byte[] obj)
{
if (THIS.Length != obj.Length)
return false;
for (int index = 0; index < obj.Length; index++)
{
if (THIS[index] != obj[index])
return false;
}
return true;
}
/// <summary>
/// 将指令字符串转化为二进制数据并添加标头
/// </summary>
public static byte[] ToBytes(this string str_input)
{
byte[] strdata = Encoding.UTF8.GetBytes(str_input);
byte[] output = new byte[1 + strdata.Length];
output[0] = StringHeader;
System.Array.Copy(strdata, 0, output, 1, strdata.Length);
return output;
}
/// <summary>
/// 将二进制数据转化为指令字符串
/// </summary>
public static string ToFTString(this byte[] bytes_input)
{
if (bytes_input[0] != StringHeader)
throw new FormatException("Bad Header!");
return Encoding.UTF8.GetString(bytes_input, 1, bytes_input.Length - 1).TrimEnd('\0');
}
/// <summary>
/// 替换可能会对命令解析造成干扰的字符
/// </summary>
public static string DoReplace(this string str_input)
{
return str_input.Replace(Space, SpaceReplacement);
}
/// <summary>
/// 还原被替换的字符
/// </summary>
public static string DeReplace(this string str_input)
{
return str_input.Replace(SpaceReplacement, Space);
}
}
#endregion
#region 一些委托
public delegate void BlockFinishedEventHandler(object sender, BlockFinishedEventArgs e);
public delegate void CommandReceivedEventHandler(object sender, CommandReceivedEventArgs e);
public delegate void FileTransmissionErrorOccurEventHandler(object sender, FileTransmissionErrorOccurEventArgs e);
public delegate void Delegate_SendBlocks(int Start, int End);
public delegate void Delegate_Void_Bool(bool logic);
public delegate int Delegate_Int_Int(int value);
#endregion
#region 文件区块类
public class FileBlockException : Exception
{
public enum ErrorCode
{
BadHeader,
BadIndex,
IllegalFileBlockSize,
ChecksumError,
}
public ErrorCode Code { get; set; }
public FileBlockException(string message, ErrorCode ErrorCode)
: base(message)
{
Code = ErrorCode;
}
}
/// <summary>
/// 文件区块类
/// </summary>
public class FileBlock : IComparable<FileBlock>
{
/// <summary>
/// 与该区块关联的传输对象
/// </summary>
internal FileTransmission _Task;
/// <summary>
/// 与该区块关联的FileStream
/// </summary>
internal FileStream _FileStream;
/// <summary>
/// 文件数据
/// </summary>
internal byte[] _Data;
/// <summary>
/// 数据长度
/// </summary>
internal int _DataLength;
/// <summary>
/// 数据的Hash值
/// </summary>
internal byte[] _DataHash;
/// <summary>
/// 获取或设置该区块的序号(该区块在文件中的位置)
/// </summary>
public int Index { get; set; }
/// <summary>
/// 获取该区块的数据长度
/// </summary>
public int DataLength { get { return _DataLength; } }
/// <summary>
/// 获取该数据块的校验值
/// </summary>
public byte[] DataHash { get { return _DataHash; } }
/// <summary>
/// 构造函数
/// 用于从文件读入区块
/// </summary>
/// <param name="fStream">输入的文件流</param>
/// <param name="BlockIndex">分块位置</param>
/// <param name="ReadOnCreated">是否立即从文件读取数据</param>
public FileBlock(FileTransmission TransmissionTask, int BlockIndex, bool ReadOnCreated)
{
_Task = TransmissionTask;
_FileStream = _Task.FileStream;
Index = BlockIndex;
if (ReadOnCreated)
this.Read(true);
}
/// <summary>
/// 构造函数
/// 用于从二进制数据读入区块
/// </summary>
/// <param name="fStream">要保存的文件流</param>
/// <param name="ReceivedData">输入的二进制数据</param>
public FileBlock(FileTransmission TransmissionTask, byte[] ReceivedData)
{
_Task = TransmissionTask;
_FileStream = _Task.FileStream;
if (ReceivedData[0] != Consts.FileBlockHeader)
throw new FileBlockException("Bad Header!", FileBlockException.ErrorCode.BadHeader);
Index = BitConverter.ToInt32(ReceivedData, 1);
_DataLength = ReceivedData.Length - 9;
if (_DataLength > Consts.BlockSize)
throw new FileBlockException("Illegal FileBlock Size!", FileBlockException.ErrorCode.IllegalFileBlockSize);
_Data = new byte[_DataLength];
_DataHash = new byte[4];
System.Array.Copy(ReceivedData, 5, _DataHash, 0, 4);
System.Array.Copy(ReceivedData, 9, _Data, 0, _DataLength);
if (!_DataHash.BytesEqual(_Data.GetHash()))
throw new FileBlockException("Error Hash!", FileBlockException.ErrorCode.ChecksumError);
}
/// <summary>
/// 从文件读入
/// </summary>
/// <param name="CalcHashAfterRead">是否在读取后立即计算校验值</param>
/// <returns>读取块的大小</returns>
public int Read(bool CalcHashAfterRead)
{
_Data = new byte[Consts.BlockSize];
lock (_FileStream)
{
_FileStream.Position = (long)Index * (long)Consts.BlockSize;
_DataLength = _FileStream.Read(_Data, 0, Consts.BlockSize);
}
if (_Data.Length != _DataLength)
{
byte[] old = _Data;
_Data = new byte[_DataLength];
System.Array.Copy(old, _Data, _DataLength);
}
if (CalcHashAfterRead)
CalcHash();
return _DataLength;
}
/// <summary>
/// 计算校验值
/// </summary>
/// <returns>校验值</returns>
public byte[] CalcHash()
{
return _DataHash = _Data.GetHash();
}
/// <summary>
/// 将该区块写入文件
/// </summary>
public void Write()
{
lock (_FileStream)
{
_FileStream.Position = (long)Index * (long)Consts.BlockSize;
_FileStream.Write(_Data, 0, _DataLength);
}
}
/// <summary>
/// 转化为二进制数据以传输
/// </summary>
/// <returns></returns>
public byte[] GetBytes()
{
MemoryStream mStream = new MemoryStream(1 + 4 + 4 + _DataLength);
byte[] Header = new byte[1] { Consts.FileBlockHeader };
mStream.Write(Header, 0, 1);
mStream.Write(BitConverter.GetBytes(Index), 0, 4);
mStream.Write(_DataHash, 0, 4);
mStream.Write(_Data, 0, _DataLength);
return mStream.ToArray();
}
int System.IComparable<FileBlock>.CompareTo(FileBlock obj)
{
return (Index as IComparable<int>).CompareTo(obj.Index);
}
}
#endregion
#region 事件参数类
public class BlockFinishedEventArgs : EventArgs
{
public int BlockIndex { get; set; }
public BlockFinishedEventArgs(int BlockIndex) { this.BlockIndex = BlockIndex; }
}
public class CommandReceivedEventArgs : EventArgs
{
public string CommandLine { get; set; }
public CommandReceivedEventArgs(string CommandLine) { this.CommandLine = CommandLine; }
}
public class FileTransmissionErrorOccurEventArgs : EventArgs
{
public Exception InnerException { get; set; }
/// <summary>
/// 指示是否继续运行
/// </summary>
public bool Continue { get; set; }
public FileTransmissionErrorOccurEventArgs(Exception innerException)
{
InnerException = innerException;
Continue = false;
}
}
#endregion
#region 文件区块抽象集合类
/// <summary>
/// 文件区块的抽象集合
/// 之所以说抽象是因为该集合并不存储实际的区块(缓存区除外)
/// 而是通过一个索引器来读写文件
/// 并提供磁盘缓存
/// </summary>
public class FileBlockCollection
{
/// <summary>
/// 与该区块关联的传输对象
/// </summary>
internal FileTransmission _Task;
/// <summary>
/// 与该区块关联的FileStream
/// </summary>
internal FileStream _FileStream;
internal bool _EnabledIOBuffer;
/// <summary>
/// 磁盘缓存区
/// </summary>
internal Dictionary<int, FileBlock> _IOBuffer;
public FileBlockCollection(FileTransmission TransmissionTask)
{
_Task = TransmissionTask;
_FileStream = _Task.FileStream;
_IOBufferSize = Consts.DefaultIOBufferSize;
}
/// <summary>
/// 获取或设置一个值,该值指示是否启用磁盘缓存
/// </summary>
internal bool EnabledIOBuffer
{
get { return _EnabledIOBuffer; }
set
{
_EnabledIOBuffer = value;
if (value)
_IOBuffer = new Dictionary<int, FileBlock>();
else
{
if (_Task is FileReceiver)
WriteAllBlock();
_IOBuffer = null;
}
}
}
internal int _IOBufferSize;
/// <summary>
/// 获取已接收或已发送的区块序号列表
/// </summary>
public List<int> Finished { get { return _Task._FinishedBlock; } }
/// <summary>
/// 获取已存在(Hash成功)的区块序号列表
/// </summary>
public List<int> Exist
{
get
{
if (_Task is FileReceiver)
return ((FileReceiver)_Task)._ExistBlock;
else
return null;
}
}
/// <summary>
/// 获取被丢弃的区块序号列表
/// </summary>
public List<int> Cast
{
get
{
if (_Task is FileReceiver)
return ((FileReceiver)_Task)._CastBlock;
else
return null;
}
}
/// <summary>
/// 获取总区块数
/// </summary>
public int Count { get { return _Task._TotalBlock; } }
/// <summary>
/// 获取有效区块数(已存在+已接收)
/// </summary>
public int CountValid
{
get
{
if (_Task is FileReceiver)
return _Task._FinishedBlock.Count + ((FileReceiver)_Task)._ExistBlock.Count;
else
return _Task._FinishedBlock.Count;
}
}
/// <summary>
/// 将缓存中的区块全部写入磁盘
/// </summary>
/// <returns>写入的区块数量</returns>
public int WriteAllBlock()
{
if (!_EnabledIOBuffer)
return -1;
int count = 0;
lock (_IOBuffer)
{
foreach (var b in _IOBuffer)
{
b.Value.Write();
count++;
}
if (count != _IOBuffer.Count)
throw new IOException("Can not Write All FileBlocks!");
_IOBuffer.Clear();
}
return count;
}
/// <summary>
/// 读取数据以填充缓存
/// </summary>
/// <param name="StartIndex">起始区块</param>
/// <returns>读取的区块数量</returns>
public int FillIOBuffer(int StartIndex)
{
int Index;
lock (_IOBuffer)
{
_IOBuffer.Clear();
for (Index = StartIndex; _IOBuffer.Count < _IOBufferSize && Index < _Task.Blocks.Count; Index++)
{
_IOBuffer.Add(Index, new FileBlock(_Task, Index, true));
}
}
return Index - StartIndex;
}
/// <summary>
/// 异步填充缓存
/// </summary>
/// <param name="StartIndex">起始区块</param>
public IAsyncResult BeginFillIOBuffer(int StartIndex, AsyncCallback callback, object state)
{
return new Delegate_Int_Int(FillIOBuffer).BeginInvoke(StartIndex, callback, state);
}
/// <summary>
/// 写入区块
/// </summary>
/// <param name="value">区块对象</param>
public void Write(FileBlock value)
{
if (_EnabledIOBuffer)
{
if (_IOBuffer.Count >= _IOBufferSize)
WriteAllBlock();
lock (_IOBuffer)
_IOBuffer.Add(value.Index, value);
}
else
value.Write();
}
/// <summary>
/// 读取或写入区块
/// </summary>
/// <param name="BlockIndex">区块序号</param>
public FileBlock this[int BlockIndex]
{
get
{
FileBlock output;
if (_EnabledIOBuffer)
{
bool IsInBuf;
lock (_IOBuffer)
IsInBuf = _IOBuffer.TryGetValue(BlockIndex, out output);
if (IsInBuf)
return output;
else
{
output = new FileBlock(_Task, BlockIndex, true);
BeginFillIOBuffer(BlockIndex + 1, null, null);
}
}
else
output = new FileBlock(_Task, BlockIndex, true);
return output;
}
set
{
if (BlockIndex != value.Index)
throw new FileBlockException("Bad Index!", FileBlockException.ErrorCode.BadIndex);
Write(value);
}
}
}
#endregion
#region 文件传输基类
public abstract class FileTransmission : IDisposable
{
internal FileStream _FileStream;
//internal readonly TransmissionMode _Mode;
/// <summary>
/// 总区块数
/// </summary>
internal int _TotalBlock;
/// <summary>
/// 最后一个区块的大小
/// </summary>
internal int _LastBlockSize;
internal List<int> _FinishedBlock;
internal byte[] ReceiveBuf;
internal Socket _Socket;
internal EventWaitHandle _WaitHandle;
internal bool _IsAlive;
internal FileBlockCollection _Blocks;
internal DateTime _StartTime;
internal int blockindex;
/// <summary>
/// 上一个区块完成的时间
/// </summary>
internal DateTime _PriorBlockTime;
internal double _ByteSpeed;
/// <summary>
/// 获取或设置一个值,该值指示是否启用磁盘缓存
/// </summary>
public bool EnabledIOBuffer
{
get { return _Blocks._EnabledIOBuffer; }
set { _Blocks.EnabledIOBuffer = value; }
}
/// <summary>
/// 获取或设置磁盘缓存的大小(单位:区块数)
/// </summary>
public int IOBufferSize
{
get { return _Blocks._IOBufferSize; }
set
{
if (!_Blocks._EnabledIOBuffer)
throw new InvalidOperationException("IOBuffer is not enabled!");
_Blocks._IOBufferSize = value;
}
}
/// <summary>
/// 获取当前磁盘缓存中的区块数
/// </summary>
public int CurrentIOBufferSize
{
get
{
if (!_Blocks._EnabledIOBuffer)
return 0;
return _Blocks._IOBuffer.Count;
}
}
/// <summary>
/// 获取或设置该传输的目标连接
/// </summary>
public Socket Socket
{
get { return _Socket; }
set
{
try
{
//if (value.ProtocolType != ProtocolType.Tcp)
// throw new ArgumentException("Socket Protocol must be TCP", "Socket");
_Socket = value;
if (_Socket != null)
_Socket.ReceiveBufferSize = _Socket.SendBufferSize = Consts.NetBlockMaxSize;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
}
/// <summary>
/// 获取与此传输关联的文件流
/// </summary>
public FileStream FileStream { get { return _FileStream; } }
/// <summary>
/// 获取或设置文件路径
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// 获取或设置文件名
/// </summary>
public string FileName { get; set; }
/// <summary>
/// 获取或设置文件名(包括路径)
/// </summary>
///
private SendFileInformation fileinfo;
public SendFileInformation FileInfo
{
get
{return fileinfo; }
set
{fileinfo=value;}
}
public string FullFileName
{
get
{
try
{
return FilePath.TrimEnd('\\') + "\\" + FileName;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return null;
}
}
set
{
try
{
int i = value.LastIndexOf('\\');
if (i > 0)
FilePath = value.Substring(0, i);
else
FilePath = Environment.CurrentDirectory;
FileName = value.Substring(i + 1);
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
}
/// <summary>
/// 一个区块完成时发生
/// </summary>
public event BlockFinishedEventHandler BlockFinished;
/// <summary>
/// 全部完成时发生
/// </summary>
public event EventHandler AllFinished;
/// <summary>
/// 连接中断时发生
/// </summary>
public event EventHandler ConnectLost;
/// <summary>
/// 出现错误时发生
/// </summary>
public event FileTransmissionErrorOccurEventHandler ErrorOccurred;
/// <summary>
/// 获取一个值,该值指示传输是否正在进行
/// </summary>
public bool IsAlive { get { return _IsAlive; } }
/// <summary>
/// 获取传输开始的时间
/// </summary>
public DateTime StartTime { get { return _StartTime; } }
/// <summary>
/// 获取已用时
/// </summary>
public TimeSpan TimePast { get { return DateTime.Now - _StartTime; } }
/// <summary>
/// 获取估计剩余时间
/// </summary>
public abstract TimeSpan TimeRemaining { get; }
/// <summary>
/// 获取平均速率(区块/秒)
/// </summary>
public double BlockAverSpeed
{
get
{
return _FinishedBlock.Count / TimePast.TotalSeconds;
}
}
/// <summary>
/// 获取平均速率(字节/秒)
/// </summary>
public double ByteAverSpeed
{
get
{
return BlockAverSpeed * Consts.BlockSize;
}
}
/// <summary>
/// 获取平均速率(千字节/秒)
/// </summary>
public double KByteAverSpeed
{
get
{
return ByteAverSpeed / 1024;
}
}
/// <summary>
/// 获取瞬时速率(字节/秒)
/// </summary>
public double ByteSpeed
{
get
{
return _ByteSpeed;
}
}
/// <summary>
/// 获取瞬时速率(千字节/秒)
/// </summary>
public double KByteSpeed
{
get
{
return _ByteSpeed / 1024;
}
}
/// <summary>
/// 获取文件总长度
/// </summary>
public long TotalSize
{
get
{
return (long)(_TotalBlock - 1) * (long)Consts.BlockSize + (long)_LastBlockSize;
}
}
/// <summary>
/// 获取已完成的数据长度
/// </summary>
public abstract long FinishedSize { get; }
/// <summary>
/// 获取进度值(%)
/// </summary>
public double Progress
{
get
{
return ((double)FinishedSize / (double)TotalSize) * 100;
}
}
/// <summary>
/// 获取该传输的区块集合
/// </summary>
public FileBlockCollection Blocks { get { return _Blocks; } }
/// <summary>
/// 默认构造函数
/// </summary>
public FileTransmission()
{
_FinishedBlock = new List<int>();
_WaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
_Blocks = new FileBlockCollection(this);
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="FilePath">文件路径</param>
/// <param name="FileName">文件名</param>
public FileTransmission(string FilePath, string FileName)
{
_FinishedBlock = new List<int>();
_WaitHandle = new EventWaitHandle(true, EventResetMode.ManualReset);
_Blocks = new FileBlockCollection(this);
this.FilePath = FilePath;
this.FileName = FileName;
}
/// <summary>
/// 初始化接收缓存
/// </summary>
internal void InitializeReceiveBuf()
{
try
{
ReceiveBuf = new byte[_Socket.ReceiveBufferSize];
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
/// <summary>
/// 开始异步接收
/// </summary>
internal abstract IAsyncResult BeginReceive();
/// <summary>
/// 开始传输
/// </summary>
public virtual void Start()
{
try
{
_IsAlive = true;
_StartTime = DateTime.Now;
_WaitHandle.Reset();
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
/// <summary>
/// 中止传输
/// </summary>
/// <param name="ShutDownSocket">是否关闭Socket</param>
public virtual void Stop(bool ShutDownSocket)
{
try
{
_IsAlive = false;
_FileStream.Close();
_FileStream = null;
_WaitHandle.Set();
if (ShutDownSocket)
{
Socket.Shutdown(SocketShutdown.Both);
_Socket.Close();
}
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
/// <summary>
/// 异步中止传输,不关闭Socket
/// </summary>
internal void Stop()
{
new Delegate_Void_Bool(Stop).BeginInvoke(false, null, null);
}
/// <summary>
/// 等待传输完成
/// </summary>
public bool WaitForExit()
{
return _WaitHandle.WaitOne();
}
/// <summary>
/// 等待传输完成
/// </summary>
public bool WaitForExit(int millisecondsTimeout, bool exitContext)
{
return _WaitHandle.WaitOne(millisecondsTimeout, exitContext);
}
/// <summary>
/// 等待传输完成
/// </summary>
public bool WaitForExit(TimeSpan timeout, bool exitContext)
{
return _WaitHandle.WaitOne(timeout, exitContext);
}
internal virtual void OnBlockFinished(int BlockIndex)
{
this.blockindex = BlockIndex+1;
if (!_FinishedBlock.Exists(a => a == BlockIndex))
_FinishedBlock.Add(BlockIndex);
if (BlockIndex == _TotalBlock - 1)
_ByteSpeed = _LastBlockSize / (DateTime.Now - _PriorBlockTime).TotalSeconds;
else
_ByteSpeed = Consts.BlockSize / (DateTime.Now - _PriorBlockTime).TotalSeconds;
_PriorBlockTime = DateTime.Now;
if (BlockFinished != null)
BlockFinished(this, new BlockFinishedEventArgs(BlockIndex));
}
internal virtual void OnConnectLost()
{
if (!_IsAlive)
return;
Stop();
if (ConnectLost != null)
ConnectLost(this, new EventArgs());
}
/// <summary>
/// 同步发送字符串
/// </summary>
public int SendString(string str)
{
try
{
return _Socket.EndSend(BeginSendString(str, null, null));
}
catch (SocketException)
{
OnConnectLost();
return 0;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return 0;
}
}
/// <summary>
/// 异步发送字符串并使用默认的回调方法
/// </summary>
public void SendStringAsync(string str)
{
BeginSendString(str, SendCallback, null);
}
/// <summary>
/// 异步发送字符串并使用指定的的回调方法和参数
/// </summary>
public IAsyncResult BeginSendString(string str, AsyncCallback callback, object state)
{
try
{
if (!_IsAlive)
throw new InvalidOperationException("Is Not Alive");
byte[] ToSend = str.ToBytes();
return _Socket.BeginSend(ToSend, 0, ToSend.Length, SocketFlags.None, callback, state);
}
catch (SocketException)
{
OnConnectLost();
return null;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return null;
}
}
internal void SendCallback(IAsyncResult ar)
{
try
{
_Socket.EndSend(ar);
}
catch (SocketException)
{
OnConnectLost();
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
if (ar.AsyncState != null)
{
if (ar.AsyncState is int)
{
OnBlockFinished((int)ar.AsyncState);
}
}
}
internal virtual void OnAllFinished()
{
if (AllFinished != null)
AllFinished(this, new EventArgs());
}
internal virtual void OnErrorOccurred(Exception innerException)
{
FileTransmissionErrorOccurEventArgs eventargs = new FileTransmissionErrorOccurEventArgs(innerException);
if (ErrorOccurred != null)
ErrorOccurred(this, eventargs);
//if (!eventargs.Continue)
// throw innerException;
}
void System.IDisposable.Dispose()
{
_FileStream.Close();
if (_Socket != null)
_Socket.Close();
}
}
#endregion
#region 发送端类
/// <summary>
/// 发送端
/// 传输前发送端创建该类实例
/// 设置必要属性后
/// 调用Start()方法开始传输
/// </summary>
public class FileSender : FileTransmission
{
/// <summary>
/// 接收到命令时发生
/// </summary>
public event CommandReceivedEventHandler CommandReceived;
/// <summary>
/// 开始异步接收
/// </summary>
internal override IAsyncResult BeginReceive()
{
InitializeReceiveBuf();
try
{
return _Socket.BeginReceive(ReceiveBuf, 0, ReceiveBuf.Length, SocketFlags.None, ReceiveCallback, null);
}
catch (SocketException)
{
OnConnectLost();
return null;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return null;
}
}
/// <summary>
/// 开始传输
/// </summary>
public override void Start()
{
base.Start();
try
{
IAsyncResult result =BeginReceive();
//while (result.IsCompleted == false)
// Thread.Sleep(100);
_FileStream = new FileStream(FullFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
_TotalBlock = (int)(_FileStream.Length / (long)Consts.BlockSize) + 1;
_LastBlockSize = (int)(_FileStream.Length - ((long)_TotalBlock - 1) * (long)Consts.BlockSize);
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
/// <summary>
/// 获取估计剩余时间
/// </summary>
public override TimeSpan TimeRemaining
{
get
{
int BlockRemaining = _TotalBlock - _FinishedBlock.Count;
return TimeSpan.FromSeconds(BlockRemaining / BlockAverSpeed);
}
}
/// <summary>
/// 获取已完成的数据长度
/// </summary>
public override long FinishedSize
{
get
{
return (long)blockindex * (long)Consts.BlockSize;
}
}
/// <summary>
/// 同步发送区块
/// </summary>
/// <param name="BlockIndex">区块序号</param>
/// <returns>发送的数据长度</returns>
public int SendBlock(int BlockIndex)
{
try
{
int ret = _Socket.EndSend(BeginSendBlock(BlockIndex, null, null));
OnBlockFinished(BlockIndex);
return ret;
}
catch (SocketException)
{
OnConnectLost();
return 0;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return 0;
}
}
/// <summary>
/// 异步发送区块并使用默认的回调方法
/// </summary>
/// <param name="BlockIndex">区块序号</param>
public void SendBlockAsync(int BlockIndex)
{
BeginSendBlock(BlockIndex, SendCallback, BlockIndex);
}
/// <summary>
/// 异步发送区块并使用指定的回调方法和参数
/// </summary>
/// <param name="BlockIndex">区块序号</param>
public IAsyncResult BeginSendBlock(int BlockIndex, AsyncCallback callback, object state)
{
try
{
if (!_IsAlive)
throw new InvalidOperationException("Is Not Alive");
if (BlockIndex >= _TotalBlock)
throw new ArgumentOutOfRangeException("BlockIndex");
byte[] ToSend = _Blocks[BlockIndex].GetBytes();
return _Socket.BeginSend(ToSend, 0, ToSend.Length, SocketFlags.None, callback, state);
}
catch (SocketException)
{
OnConnectLost();
return null;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return null;
}
}
internal void ReceiveCallback(IAsyncResult ar)
{
bool ContinueReceive = true;
int count = 0;
try
{
count = _Socket.EndReceive(ar);
}
catch (SocketException)
{
OnConnectLost();
return;
}
catch (Exception ex)
{
try
{
OnErrorOccurred(ex);
}
catch { return; }
}
try
{
if (count == 0)
return;
switch (ReceiveBuf[0])
{
case Consts.StringHeader:
ContinueReceive = OnCommandReceived(ReceiveBuf.ToFTString());
break;
default:
throw new FormatException("Bad Header!");
}
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
if (ContinueReceive)
{
BeginReceive();
}
}
/// <summary>
/// 命令处理
/// </summary>
/// <param name="str">收到的命令</param>
/// <returns>是否继续接收</returns>
internal bool OnCommandReceived(string str)
{
if (CommandReceived != null)
CommandReceived(this, new CommandReceivedEventArgs(str));
bool ContinueReceive = true;
string[] Msg = str.Split(' ');
if (Msg[0] == "Exit")
{
OnAllFinished();
ContinueReceive = false;
Stop();
}
else if (Msg[0] == "GET")
{
if (Msg[1] == "FileBlock")
{
int BlockIndex;
if (!int.TryParse(Msg[2], out BlockIndex))
throw new FormatException("Bad BlockIndex " + Msg[2]);
SendBlock(BlockIndex);
}
else if (Msg[1] == "BlockHash")
{
int BlockIndex;
if (!int.TryParse(Msg[2], out BlockIndex))
throw new FormatException("Bad BlockIndex " + Msg[2]);
byte[] hash = _Blocks[BlockIndex].DataHash;
SendStringAsync(string.Format("BlockHash {0} {1}", BlockIndex, BitConverter.ToInt32(hash, 0)));
}
else if (Msg[1] == "FileName")
{
SendString(string.Format("SET:FileName:{0}", FileName));
//SendStringAsync(string.Format("SET:FileName:{0}", FileName));
}
else if (Msg[1] == "TotalBlock")
{
SendString(string.Format("SET TotalBlock {0}", _TotalBlock));
//SendStringAsync(string.Format("SET TotalBlock {0}", _TotalBlock));
}
else if (Msg[1] == "LastBlockSize")
{
SendString(string.Format("SET LastBlockSize {0}", _LastBlockSize));
// SendStringAsync(string.Format("SET LastBlockSize {0}", _LastBlockSize));
}
else
throw new FormatException("Bad Command " + Msg[1]);
}
else
throw new FormatException("Bad Command " + Msg[0]);
return ContinueReceive;
}
}
#endregion
#region 接收端类
/// <summary>
/// 接收端
/// 传输前接收端创建该类实例
/// 设置必要属性后
/// 调用Start()方法开始传输
/// </summary>
public class FileReceiver : FileTransmission
{
internal List<int> _ExistBlock;
internal List<int> _CastBlock;
/// <summary>
/// 下载线程
/// </summary>
internal Thread _DownThread;
public event BlockFinishedEventHandler BlockHashed;
/// <summary>
/// 开始异步接收
/// </summary>
internal override IAsyncResult BeginReceive()
{
InitializeReceiveBuf();
try
{
return _Socket.BeginReceive(ReceiveBuf, 0, ReceiveBuf.Length, SocketFlags.None, null, null);
}
catch (SocketException)
{
OnConnectLost();
return null;
}
catch (Exception ex)
{
OnErrorOccurred(ex);
return null;
}
}
/// <summary>
/// 获取估计剩余时间
/// </summary>
public override TimeSpan TimeRemaining
{
get
{
int BlockRemaining = _TotalBlock - _FinishedBlock.Count - ((FileReceiver)this)._ExistBlock.Count;
return TimeSpan.FromSeconds(BlockRemaining / BlockAverSpeed);
}
}
/// <summary>
/// 获取已完成的数据长度
/// </summary>
public override long FinishedSize
{
get
{
return ((long)_FinishedBlock.Count + (long)_ExistBlock.Count - 1) * (long)Consts.BlockSize + (long)_LastBlockSize;
}
}
/// <summary>
/// 开始传输
/// </summary>
public override void Start()
{
base.Start();
try
{
_CastBlock = new List<int>();
_ExistBlock = new List<int>();
_DownThread = new Thread(DownLoad);
_DownThread.IsBackground = true;
_DownThread.Name = "DownThread";
_DownThread.Start();
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
/// <summary>
/// 中止传输
/// </summary>
/// <param name="ShutDownSocket">是否关闭Socket</param>
public override void Stop(bool ShutDownSocket)
{
try
{
if (_DownThread != null)
{
if ((_DownThread.ThreadState & ThreadState.Running) == ThreadState.Running)
_DownThread.Abort();
}
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
base.Stop(ShutDownSocket);
}
internal string ReceiveString()
{
int count = 0;
try
{
count = _Socket.EndReceive(BeginReceive());
}
catch (Exception ex)
{
OnConnectLost();
throw ex;
}
if (count == 0)
return null;
else
return ReceiveBuf.ToFTString();
}
internal FileBlock ReceiveFileBlock()
{
MemoryStream mStream = new MemoryStream();
while (true)
{
int count = 0;
try
{
count = _Socket.EndReceive(BeginReceive());
if (count == 0) throw new Exception();
}
catch (Exception ex)
{
OnConnectLost();
throw ex;
}
mStream.Write(ReceiveBuf, 0, count);
try
{//接收到正确的区块则返回
return new FileBlock(this, mStream.ToArray());
}
catch (FileBlockException ex)
{//接收到不完整或错误的区块,若不完整则继续接收
if (mStream.Length >= Consts.NetBlockMaxSize)
throw ex;//区块已达到指定大小但仍然错误,则抛出错误
}
}
}
/// <summary>
/// 从发送端获取文件名
/// </summary>
public void GetFileName()
{
while (true)
{
SendString("GET FileName");
string[] Msg = ReceiveString().Split(' ');
if (Msg[0] == "SET" && Msg[1] == "FileName")
{
FileName = Msg[2];
break;
}
}
}
/// <summary>
/// 从发送端获取区块总数
/// </summary>
public void GetTotalBlock()
{
while (true)
{
SendString("GET TotalBlock");
string[] Msg = ReceiveString().Split(' ');
if (Msg[0] == "SET" && Msg[1] == "TotalBlock")
{
if (int.TryParse(Msg[2], out _TotalBlock))
break;
}
}
}
/// <summary>
/// 从发送端获取最后一个区块的大小
/// </summary>
public void GetLastBlockSize()
{
while (true)
{
SendString("GET LastBlockSize");
string[] Msg = ReceiveString().Split(' ');
if (Msg[0] == "SET" && Msg[1] == "LastBlockSize")
{
if (int.TryParse(Msg[2], out _LastBlockSize))
break;
}
}
}
/// <summary>
/// 校验文件
/// </summary>
/// <returns>损坏或尚未下载的区块序号列表</returns>
public List<int> HashFile()
{
_FileStream.Position = 0;
_ExistBlock.Clear();
for (int count = 0; _FileStream.Position < _FileStream.Length && count < _TotalBlock; count++)
{//校验已存在的区块
FileBlock TestBlock = new FileBlock(this, count, true);
SendString(string.Format("GET BlockHash {0}", count));
string[] Msg = ReceiveString().Split(':');
if (Msg[0] == "BlockHash")
{
if (Convert.ToInt32(Msg[1]) == count)
{
if (BitConverter.ToInt32(TestBlock.DataHash, 0) == Convert.ToInt32(Msg[2]))
_ExistBlock.Add(count);
}
}
if (BlockHashed != null)
BlockHashed(this, new BlockFinishedEventArgs(count));
}
int MaxExistBlockIndex;//已存在的区块最大序号
try
{
MaxExistBlockIndex = _ExistBlock.Max();
}
catch
{
MaxExistBlockIndex = 0;
}
List<int> BlockRemaining = new List<int>();
for (int index = 0; index < _TotalBlock; )
{//计算仍需传输的区块
if (index <= MaxExistBlockIndex)
{
if (_ExistBlock.Exists(a => a == index))
{
index++;
continue;
}
}
BlockRemaining.Add(index++);
}
return BlockRemaining;
}
/// <summary>
/// 接收整个文件
/// </summary>
internal void DownLoad()
{
try
{
if (string.IsNullOrEmpty(FilePath))//未指定路径时默认为接收程序所在路径
FilePath = Environment.CurrentDirectory;
//FilePath = "F:\\";
if (string.IsNullOrEmpty(FileName))//未指定文件名时从发送端获取
{
GetFileName();
}
_FileStream = new FileStream(FullFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);//temp
GetTotalBlock();
GetLastBlockSize();
List<int> BlockRemaining = HashFile();
if (_FileStream.Length > TotalSize)//如果已存在的文件比目标文件长则截断它
_FileStream.SetLength(TotalSize);
_StartTime = DateTime.Now;
foreach (int index in BlockRemaining)
{
FileBlock Block;
while (true)
{
SendString(string.Format("GET FileBlock {0}", index));
try
{
Block = ReceiveFileBlock();
break;
}
catch (FileBlockException)
{//接收到错误的区块,抛弃该数据并重新请求
_CastBlock.Add(index);
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
while (true)
{
try
{
_Blocks[index] = Block;//写入区块
OnBlockFinished(index);
break;
}
catch (IOException ex)
{//磁盘写入错误时
try
{
OnErrorOccurred(ex);
//重试
}
catch
{//退出
Stop();
return;
}
}
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
}
SendStringAsync("Exit");
_Blocks.WriteAllBlock();
OnAllFinished();
Stop();
}
catch (SocketException) { }
catch (Exception ex)
{
OnErrorOccurred(ex);
}
}
}
#endregion
}
ESPlus文件传输方案:
http://blog.oraycn.com/ESFramework_03.aspx
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接口,用于暴露所有与文件传送相关的状态和事件:
publicinterfaceIFileTransferingEvents
{
///<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即可用于客户端也可用户服务端。
public interface 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继承,且未增加任何新的内容:
public interface IFileOutter :IOutter,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接口:
public interface IFileTransferingViewer
{
/// <summary>
/// 当某个文件开始续传时,触发该事件。参数为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);