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

陳鑫偉發表於2019-08-13

 

        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(); /// <summary> /// 尋找 THashAlgorithm 型別下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 呼叫構造方法建立例項。 /// 如果 Activator.CreateInstance 方法執行失敗,它會丟擲異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), 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 方法得到的結果是錯誤的!

 

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

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('\0', 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('\0', 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 high */]; ++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();

    /// <summary>
    /// 尋找 THashAlgorithm 型別下的 Create 靜態方法,並執行它。
    /// 如果沒找到,則執行 Activator.CreateInstance 呼叫構造方法建立例項。
    /// 如果 Activator.CreateInstance 方法執行失敗,它會丟擲異常。
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    static THashAlgorithm Create()
    {
        var createMethod = typeof(THashAlgorithm).GetMethod(
            nameof(HashAlgorithm.Create),
            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('\0', 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 high */]; ++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();

    /// <summary>
    /// 尋找 THashAlgorithm 型別下的 Create 靜態方法,並執行它。
    /// 如果沒找到,則執行 Activator.CreateInstance 呼叫構造方法建立例項。
    /// 如果 Activator.CreateInstance 方法執行失敗,它會丟擲異常。
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    static THashAlgorithm Create()
    {
        var createMethod = typeof(THashAlgorithm).GetMethod(
            nameof(HashAlgorithm.Create),
            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('\0', 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 high */]; ++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();

    /// <summary>
    /// 尋找 THashAlgorithm 型別下的 Create 靜態方法,並執行它。
    /// 如果沒找到,則執行 Activator.CreateInstance 呼叫構造方法建立例項。
    /// 如果 Activator.CreateInstance 方法執行失敗,它會丟擲異常。
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    static THashAlgorithm Create()
    {
        var createMethod = typeof(THashAlgorithm).GetMethod(
            nameof(HashAlgorithm.Create),
            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 high */]; ++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);
        }
    }
}

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

 

 

感謝閱讀!

相關文章