今天在做需求的时候,需要把MD5的计算集成到应用中。其实计算MD5本身并不难,C#本身提供了计算Md5值的工具:
using (FileStream fs = File.OpenRead(path))
{
using (var crypto = MD5.Create())
{
var md5Hash = crypto.ComputeHash(fs);
return md5Hash;
}
}
请注意,对fs的计算是MD5提供的一个重载方法。本质上Md5是对byte[]类型进行的Hash计算,但是当你尝试得到一个大文件(比如2G以上)的byte[]时,会发现无法在c#里声明这么大的数组。因此必须以流的形式传给MD5工具类进行计算
然后可以将得到的byte[]形式存储的md5信息进行格式化
public static string GetHexString(byte[] bytes)
{
string hexString = bytes.Aggregate(string.Empty, (res, b) => res = res + b.ToString("X2"));
return hexString;
}
这样得到的就是32位的十六进制字符串了,这里可以用Convert方法计算,不过Convert好像印象中需要自己再处理一下连字符-,我当时没顾得查API,考虑到对性能影响不大,就用这种方式写了
这是一种同步的计算方式,但是会卡IO,特别是对多个文件进行Md5计算的时候,任务进度不好掌控。实际上Md5还提供了另一个流式运算(分块计算)
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount);
大概意思就是,你可以提供一个比较小的缓冲区inputBuffer用来给MD5工具进行迭代计算,在计算最后一个Hash块时,需要调用TransformFinalBlock方法,这会返回给你最终的Hash数组。我是按照参考调用Md5.Hash也可以获得最终的Hash结果
这样的好处就是,你可以把摘要计算做成一个异步的方法。因为文件流的读取本身就可以异步,可以分块。你只需要每帧读取一部分的数据,然后传给MD5进行部分计算,缓存结果,这样整个流程就可控了
下边是我自己封装的一个分段计算Md5的类,可以找到分段的参考。去掉了一些不必要的信息
using System;
using System.IO;
using System.Security.Cryptography;
class Md5CheckTask
{
public bool IsComplete;
public float Progress;
public string Result;
public string Path;
private MD5 _crypto;
private Stream _stream;
private byte[] _cache;
private long _curLen;
private int _blockSize = 1024 * 1024;
private int _readLen = 0;
private byte[] _outPut;
public Md5CheckTask(string path)
{
Path = path;
_crypto = MD5.Create();
_stream = File.OpenRead(path);
_cache = new byte[_blockSize];
_outPut = new byte[_blockSize];
_curLen = 0;
IsComplete = false;
}
private void Finish()
{
_crypto.TransformFinalBlock(_cache, 0, 0);
_stream.Close();
//这里就是最终的MD5字符串
string s = "MD5: " + Helper.GetHexString(_crypto.Hash);
}
public void Update()
{
if (IsComplete)
{
return;
}
_readLen = _stream.Read(_cache, 0, _blockSize);
if (_readLen > 0)
{
_curLen += _readLen;
Progress = (float)((double)_curLen / _stream.Length);
_crypto.TransformBlock(_cache, 0, _readLen, _outPut, 0);
}
if (_curLen >= _stream.Length)
{
IsComplete = true;
Finish();
return;
}
}
}
其中Update()方法是由外部驱动的,我这里是每帧调用,并且在类里记录了Progress用来给外部做进度条展示
这样就不会卡死UI,效果要好的多
然后就是_blockSize的大小了,这里定义的是1M,太小的话,速度会很慢,太大的话,卡顿感明显,而且会掉帧,导致又像是同步的了,所以这里选一个合适的就好。因为我的任务是从共享盘里计算文件的大小,所以还有一个局域网带宽的限制。这种参数,还是根据实际调整一下为好。