.Net Core 最優 MD5 打開方式!初學者建議收藏(支援 SHA1,SHA256,.Net Framework)

  • 2019 年 10 月 3 日
  • 筆記

 

        public static string GetMd5Hash(string input)          {              using (MD5 md5Hash = MD5.Create())              {                  // Convert the input string to a byte array and compute the hash.                  byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));                    // Create a new Stringbuilder to collect the bytes                  // and create a string.                  StringBuilder sBuilder = new StringBuilder();                    // Loop through each byte of the hashed data                  // and format each one as a hexadecimal string.                  for (int i = 0; i < data.Length; i++)                  {                      sBuilder.Append(data[i].ToString("x2"));                  }                    // Return the hexadecimal string.                  return sBuilder.ToString();              }          }

  這是一段 MSDN 官方的 MD5 示例,例子很簡單且很容易理解。但是,這個例子也有很多的問題,首先上例至少創建了 3 個臨時快取區!且每次執行 GetMd5Hash 都會創建一個 MD5 實例,並在方法執行完成後釋放它。這些都造成了很大的系統資源浪費和增加了 GC 的壓力。

  鑒於官方給的 Demo 並不優秀,且網上也沒有給出很好使用方式,這裡我就拿出我多年使用的 MD5 打開方式,這個方法同時支援 SHA1,SHA256 等,即支援 System.Security.Cryptography 命名空間下的 HashAlgorithm(哈希演算法) 實現。也同時支援 .Net Framework 2.0 之後的所有 .Net 平台。 

  我不想看你的鬼廢話,直接給我上最終程式碼》》》

  先說明,這個文章是基於 System.Security.Cryptography 命名空間的實現,不是自己寫一個 MD5 演算法哦。

  現在我們開始,首先我們先定義一個輔助類:

using System;  using System.Reflection;  using System.Runtime.CompilerServices;  using System.Security.Cryptography;    static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm  {      /// <summary>      /// 執行緒靜態變數。      /// 即:這個變數在每個執行緒中都是唯一的。      /// 再結合泛型類實現:該變數在不同泛型或不同的執行緒下的值都是不一樣的。      /// 這樣做的目的是為了避開多執行緒問題。
/// 關於垃圾回收:當 .NET 執行緒被釋放時,程式中的所有執行緒靜態變數都會被回收,GC 回收時同時將釋放資源,所以不必擔心釋放問題,GC 會幫助我們的。
    /// 這裡描述的 .NET 執行緒釋放不是指 .NET 執行緒回收至執行緒池。很多時候 .NET 的執行緒在程式關閉之前都不會真正釋放,而是在執行緒池中繼續駐留。
    /// 執行緒唯一真的能避免多執行緒問題嗎?答:多個執行緒所用存儲空間都不一樣,那麼臟值就不可能存在,如果這都能出現多執行緒問題,我直播吃....豬紅(本人極其厭惡吃豬紅?)。
/// </summary>h [ThreadStatic] static THashAlgorithm instance; public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } } /// <summary> /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。 /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), // 這段程式碼同 "Create",低版本 C# 可以替換掉 BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, Type.DefaultBinder, Type.EmptyTypes, null); if (createMethod != null) { instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { }); } else { instance = Activator.CreateInstance<THashAlgorithm>(); } return instance; } }

  該輔助類幫助我們避開多執行緒問題,且幫助我們創建指定的 HashAlgorithm 實例。

  這裡說明一下,HashAlgorithm.ComputeHash (同 MD5.ComputeHash) 方法絕對不是執行緒安全的!大家使用它的時候必須要注意,在未執行緒同步下調用同一實例的 ComputeHash 方法得到的結果是錯誤的!

   關於執行緒唯一和泛型唯一:

    還記得老師教我們的時候強調靜態變數就是唯一的,可是現在就突然出現了兩個反例,與之對立,這讓初學者一下子難以接受,其實這也很容易理解的:

      首先 [ThreadStatic] 特性我們可以理解為將欄位封裝為了 ThreadLocal<T>,它在內部區分執行緒,然後返回不同的值。

      然後泛型唯一,舉例:我們都知道 List<int> 和 List<string> 它們不是一個類型!那麼它們的欄位 List<int>.thread_field 和 List<string>.thread_field 也同理不是一個欄位,那麼它們的值當然也不是同一個啦。

  接下來我們再定義實現類:

public static class HashAlgorithmHelper  {
    public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
    {
        var data = THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(Encoding.UTF8.GetBytes(input));
 
        var sBuilder = new StringBuilder();
 
        foreach (var item in data)
        {
            sBuilder.Append(item.ToString(“x2”));
        }
 
        return sBuilder.ToString();
    } 
}

  到這裡我們入門級的 MD5 打開方式就完成了,使用方法:HashAlgorithmHelper.ComputeHash<MD5>(“Hello World!”)

  我們來先測試一下:

    static void Main(string[] args)      {          Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));          Console.WriteLine(GetMd5Hash("Hello World!"));            while (true)          {              var stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);                stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  GetMd5Hash("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);          }      }

  輸出結果:

  可以看出我們的性能已經超官方 Demo 近一倍了。

  接下來我們將進入進階級打開方式,我們現在需要自己寫一個簡單的 byte[] To string 方法,我們先打開 C# 項目的 “允許不安全程式碼” 選項。

  在解決方案中右鍵項目->屬性->生成->勾選“允許不安全程式碼”。

  然後我們在 HashAlgorithmHelper 類中定義新的 ToString 方法。

    static string ToString(byte[] bytes)      {          unsafe          {              const int byte_len = 2; // 表示一個 byte 的字元長度。                var str = new string('', byte_len * bytes.Length); // 創建一個指定長度的空字元串。                fixed(char* pStr = str)              {                  var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變數。                    foreach (var item in bytes)                  {                      *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;                      *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2;                  }              }                return str;          }      }

  然後我們修改 ComputeHash 方法為如下:

    public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm
    {
        var bytes = Encoding.UTF8.GetBytes(input);
 
        var data = THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes);
 
        return ToString(data);
    }

  現在我們再測試就會發現已經比官方 Demo 快 4 倍了!現在這個 MD5 打開方式已經適合絕大多數人了,如果您不喜歡不安全程式碼,也可以用數組代替,效率只差一丟丟而已,該方式我會在下方給出完整程式碼。

  接下來我們使用 .Net Core 以最優的方式打開,我們修改 HashAlgorithmHelper 為如下:(這裡就不再支援 .Net Framework 了)

public static class HashAlgorithmHelper  {      static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
    // 在這個函數里要用到的 bytes 成員 ReadOnlySpan<byte> 與 byte[] 的一致,所以我們只需要修改參數類型即可。
static string ToString(ReadOnlySpan<byte> bytes) { unsafe { const int byte_len = 2; // 表示一個 byte 的字元長度。 var str = new string('', byte_len * bytes.Length); fixed(char* pStr = str) { var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變數。 foreach (var item in bytes) { *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2; *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2; } } return str; } } public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm { var instance = THashAlgorithmInstances<THashAlgorithm>.Instance; // 避免二次取值,微微提高效率(自我感覺)。 var encoding = Encoding.UTF8;
        // 我們在這裡聲明一個足量的 byte 數組,足以容下字元串的 utf-8 位元組碼和 hash 值的位元組碼。
var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))]; var bytesCount = encoding.GetBytes(input, bytes); var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region. var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region. if (bytes.Length - bytesCount > instance.HashSize && instance.TryComputeHash(source, destination, out var bytesWritten)) { return ToString(destination.Slice(0, bytesWritten)); } else {
// 通常情況下這裡就很有可能拋出異常了,但是我們封裝工具方法必須有一個原則,我們盡量不要自行拋出異常。
            // 用戶的參數執行到這裡我們依然調用 HashAlgorithm.ComputeHash,由它內部拋出異常。這樣可以避免很多問題和歧義。
return ToString(instance.ComputeHash(bytes, 0, bytesCount)); } } }

  我們再次測試,結果如下:

  現在我們已經超官方示例達 5 倍了!這就是最終版本了。

  最後附上各個版本實現的完整程式碼:

  Core 2.1+ 包含不安全程式碼版本:

using System;  using System.Diagnostics;  using System.Reflection;  using System.Runtime.CompilerServices;  using System.Security.Cryptography;  using System.Text;    static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm  {      /// <summary>      /// 執行緒靜態變數。      /// 即:這個變數在每個執行緒中都是唯一的。      /// 再結合泛型類實現了該變數在不同泛型或不同的執行緒先的變數都是唯一的。      /// 這樣做的目的是為了避開多執行緒問題。      /// </summary>      [ThreadStatic]      static THashAlgorithm instance;        public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } }        /// <summary>      /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。      /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。      /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。      /// </summary>      [MethodImpl(MethodImplOptions.NoInlining)]      static THashAlgorithm Create()      {          var createMethod = typeof(THashAlgorithm).GetMethod(              nameof(HashAlgorithm.Create), // 這段程式碼同 "Create",低版本 C# 可以替換掉              BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,              Type.DefaultBinder,              Type.EmptyTypes,              null);            if (createMethod != null)          {              instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });          }          else          {              instance = Activator.CreateInstance<THashAlgorithm>();          }            return instance;      }  }    public static class HashAlgorithmHelper  {      static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };        static string ToString(ReadOnlySpan<byte> bytes)      {          unsafe          {              const int byte_len = 2; // 表示一個 byte 的字元長度。                var str = new string('', byte_len * bytes.Length);                fixed(char* pStr = str)              {                  var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變數。                    foreach (var item in bytes)                  {                      *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;                      *pStr2 = Digitals[item & 15/* byte low  */]; ++pStr2;                  }              }                return str;          }      }        public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm      {          var instance = THashAlgorithmInstances<THashAlgorithm>.Instance;          var encoding = Encoding.UTF8;            var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))];            var bytesCount = encoding.GetBytes(input, bytes);            var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region.          var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region.            if (bytes.Length - bytesCount > instance.HashSize && instance.TryComputeHash(source, destination, out var bytesWritten))          {              return ToString(destination.Slice(0, bytesWritten));          }          else          {              return ToString(instance.ComputeHash(bytes, 0, bytesCount));          }      }  }    class Program  {      public static string GetMd5Hash(string input)      {          using (MD5 md5Hash = MD5.Create())          {              // Convert the input string to a byte array and compute the hash.              byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));                // Create a new Stringbuilder to collect the bytes              // and create a string.              StringBuilder sBuilder = new StringBuilder();                // Loop through each byte of the hashed data              // and format each one as a hexadecimal string.              for (int i = 0; i < data.Length; i++)              {                  sBuilder.Append(data[i].ToString("x2"));              }                // Return the hexadecimal string.              return sBuilder.ToString();          }      }        static void Main(string[] args)      {          Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));          Console.WriteLine(GetMd5Hash("Hello World!"));            while (true)          {              var stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);                stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  GetMd5Hash("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);          }      }  }

包含不安全程式碼的通用版本:

using System;  using System.Diagnostics;  using System.Reflection;  using System.Runtime.CompilerServices;  using System.Security.Cryptography;  using System.Text;    static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm  {      /// <summary>      /// 執行緒靜態變數。      /// 即:這個變數在每個執行緒中都是唯一的。      /// 再結合泛型類實現了該變數在不同泛型或不同的執行緒先的變數都是唯一的。      /// 這樣做的目的是為了避開多執行緒問題。      /// </summary>      [ThreadStatic]      static THashAlgorithm instance;        public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } }        /// <summary>      /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。      /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。      /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。      /// </summary>      [MethodImpl(MethodImplOptions.NoInlining)]      static THashAlgorithm Create()      {          var createMethod = typeof(THashAlgorithm).GetMethod(              nameof(HashAlgorithm.Create), // 這段程式碼同 "Create",低版本 C# 可以替換掉              BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,              Type.DefaultBinder,              Type.EmptyTypes,              null);            if (createMethod != null)          {              instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });          }          else          {              instance = Activator.CreateInstance<THashAlgorithm>();          }            return instance;      }  }    public static class HashAlgorithmHelper  {      static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };        static string ToString(byte[] bytes)      {          unsafe          {              const int byte_len = 2; // 表示一個 byte 的字元長度。                var str = new string('', byte_len * bytes.Length);                fixed(char* pStr = str)              {                  var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變數。                    foreach (var item in bytes)                  {                      *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2;                      *pStr2 = Digitals[item & 15/* byte low  */]; ++pStr2;                  }              }                return str;          }      }        public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm      {          var bytes = Encoding.UTF8.GetBytes(input);            return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes));      }  }    class Program  {      public static string GetMd5Hash(string input)      {          using (MD5 md5Hash = MD5.Create())          {              // Convert the input string to a byte array and compute the hash.              byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));                // Create a new Stringbuilder to collect the bytes              // and create a string.              StringBuilder sBuilder = new StringBuilder();                // Loop through each byte of the hashed data              // and format each one as a hexadecimal string.              for (int i = 0; i < data.Length; i++)              {                  sBuilder.Append(data[i].ToString("x2"));              }                // Return the hexadecimal string.              return sBuilder.ToString();          }      }        static void Main(string[] args)      {          Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));          Console.WriteLine(GetMd5Hash("Hello World!"));            while (true)          {              var stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);                stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  GetMd5Hash("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);          }      }  }

不包含不安全程式碼的通用版本:

using System;  using System.Diagnostics;  using System.Reflection;  using System.Runtime.CompilerServices;  using System.Security.Cryptography;  using System.Text;    static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm  {      /// <summary>      /// 執行緒靜態變數。      /// 即:這個變數在每個執行緒中都是唯一的。      /// 再結合泛型類實現了該變數在不同泛型或不同的執行緒先的變數都是唯一的。      /// 這樣做的目的是為了避開多執行緒問題。      /// </summary>      [ThreadStatic]      static THashAlgorithm instance;        public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } }        /// <summary>      /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。      /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。      /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。      /// </summary>      [MethodImpl(MethodImplOptions.NoInlining)]      static THashAlgorithm Create()      {          var createMethod = typeof(THashAlgorithm).GetMethod(              nameof(HashAlgorithm.Create), // 這段程式碼同 "Create",低版本 C# 可以替換掉              BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly,              Type.DefaultBinder,              Type.EmptyTypes,              null);            if (createMethod != null)          {              instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { });          }          else          {              instance = Activator.CreateInstance<THashAlgorithm>();          }            return instance;      }  }    public static class HashAlgorithmHelper  {      static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };        static string ToString(byte[] bytes)      {          const int byte_len = 2; // 表示一個 byte 的字元長度。            var chars = new char[byte_len * bytes.Length];            var index = 0;            foreach (var item in bytes)          {              chars[index] = Digitals[item >> 4/* byte high */]; ++index;              chars[index] = Digitals[item & 15/* byte low  */]; ++index;          }            return new string(chars);      }        public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm      {          var bytes = Encoding.UTF8.GetBytes(input);            return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes));      }  }    class Program  {      public static string GetMd5Hash(string input)      {          using (MD5 md5Hash = MD5.Create())          {              // Convert the input string to a byte array and compute the hash.              byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));                // Create a new Stringbuilder to collect the bytes              // and create a string.              StringBuilder sBuilder = new StringBuilder();                // Loop through each byte of the hashed data              // and format each one as a hexadecimal string.              for (int i = 0; i < data.Length; i++)              {                  sBuilder.Append(data[i].ToString("x2"));              }                // Return the hexadecimal string.              return sBuilder.ToString();          }      }        static void Main(string[] args)      {          Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"));          Console.WriteLine(GetMd5Hash("Hello World!"));            while (true)          {              var stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  HashAlgorithmHelper.ComputeHash<MD5>("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);                stopwatch = Stopwatch.StartNew();                for (int i = 0; i < 1000000; i++)              {                  GetMd5Hash("Hello World!");              }                Console.WriteLine(stopwatch.ElapsedMilliseconds);          }      }  }

不包含不安全程式碼通用版本的性能:(性能依然極佳,建議使用此版本)

 

 註:測試結果僅來自個人電腦,不同平台或硬體可能會有差異!

 

感謝閱讀!