頗特殊的需求: 一個跨平台整合在傳遞以GUID為Primary Key資料時,對方的參數欄位只接受最長30個字元,即使使用16進位數字表示法(例如: 4854c292c333480890f916d1a062b8e3),GUID字串也長達32字元,超出限制。另外想一種不會重複的識別編號法則是種解法,但要做到GUID等級的唯一性得付出不少代價。因此,另一個思考方向是如何用較短的字串長度表示GUID,評估是較省力的做法。

要比16進位表示法更簡短,最簡便的做法是將其GUID先轉為byte[],再用Base64編碼轉為字串。例如: 4854c292c333480890f916d1a062b8e3 可以轉換成 ksJUSDPDCEiQ+RbRoGK44w==,只要24個字元。但有個問題,對方系統的資料庫定序(Collation)被設為不分大小寫,而Base64編碼需大小寫有別,唯一限制及查詢比對可能出問題。

原本想拿出自己在VB6時代發明的36進位表示去(使用A-Z及0-9共36個英數元表示二進位資料),但轉念一想,如果有標準可循,還是別自己造輪子好。查詢後,真的有所謂的Base32編碼,而且還是RFC標準(RFC 4648)! Base32只用A-Z及2-7共32個字元表示,編碼結果比Base64長約20%,但好處是:

  • 適用於不分大小寫的情境
  • 數字段避開0, 1, 8,可避免與字母O, I, B混淆
  • 使用於URL時完全不需要UrlEncoding

只是Base32的應用不若Base64普遍,在.NET基本類別庫無現成可用的函數編解碼。所幸,既是公開標準,就不難找到前輩先進寫好的範例。我在stackoverflow找到一個實作範例,但測試發現它在預估字串長度時計算有誤差,會有多餘的Padding字元"=",但這只需要小小調整就可改善: [已留言建議]

原本是 charCount = (int)Math.Ceiling(input.Length / 5d) * 8;

應改為 charCount = (int)Math.Ceiling(input.Length / 5d * 8);

以下是測試範例,建立10,000個GUID進行編碼及還原,逐筆驗證還原結果無誤並計算執行時間。(同時支援Base64、Base32兩種格式)

    class Program
    {
        static void Main(string[] args)
        {
 
            List<Guid> guidPool = new List<Guid>();
            for (int i = 0; i < 10000; i++)
            {
                guidPool.Add(Guid.NewGuid());
            }
 
            string str = null;
            Guid restored = Guid.Empty;
            Stopwatch sw = new Stopwatch();
            //Base64 version test
            sw.Start();
            foreach (Guid uid in guidPool)
            {
                str = GetShortGuidString(uid);
                restored = ParseShortGuidString(str);
                if (!uid.Equals(restored))
                    throw new ApplicationException("Test Failed!");
            }
            sw.Stop();
            Console.WriteLine("Base64 Version in {0:N}ms,\n  {1:N} -> {2}",
                sw.ElapsedMilliseconds, restored, str);
 
            //Base32 version test
            sw.Restart();
            foreach (Guid uid in guidPool)
            {
                str = GetShortGuidString(uid, true);
                restored = ParseShortGuidString(str, true);
                if (!uid.Equals(restored))
                    throw new ApplicationException("Test Failed!");
            }
            sw.Stop();
            Console.WriteLine("Base32 Version in {0:N}ms,\n  {1:N} -> {2}",
                sw.ElapsedMilliseconds, restored, str);
 
            Console.Read();
        }
 
        static string GetShortGuidString(Guid uid, bool useBase32 = false)
        {
            if (useBase32)
                return Base32Encoding.ToString(uid.ToByteArray());
            else
                return Convert.ToBase64String(uid.ToByteArray());
        }
 
        static Guid ParseShortGuidString(string s, bool useBase32 = false)
        {
            if (useBase32)
                return new Guid(Base32Encoding.ToBytes(s));
            else
                return new Guid(Convert.FromBase64String(s));
        }
    }

測試結果如下:

Base64 Version in 13.00ms,
  4854c292c333480890f916d1a062b8e3 -> ksJUSDPDCEiQ+RbRoGK44w==
Base32 Version in 38.00ms,
  4854c292c333480890f916d1a062b8e3 –> SLBFISBTYMEEREHZC3I2AYVY4M

推測目前找到的Base32編碼邏輯,最佳化程度不及.NET內建的Convert.ToBase64String,故Base32執行速度較Base64慢3倍,但1萬筆只需0.04秒,在整合應用時成為瓶頸的可能性不高,測試驗證可行。


Comments

# by Robin

感謝分享~~

# by 豬公峰

另可考慮用Alphanumerics, 就是把16進位從F擴充到Z 就有36進位(不分大小寫)資料長度比Base32短 分大小寫的話是62進位資料, 比Base64大一點, 但好處是都是文數字

# by Aqua

我觉得 charCount = (int)Math.Ceiling(input.Length / 5d) * 8 是对的吧,需要补齐到8个字符的倍数。 否则,例如输入6个字节,按照 charCount = (int)Math.Ceiling(input.Length / 5d * 8) 计算,输出9个字符就没法编码了。

# by Jeffrey

to Aqua, 依我的理解補=與否並不影響解析,用以下程式實測,(int)Math.Ceiling(input.Length / 5d * 8)可正確編碼及還原長度1到長度16的byte[]: for (var i = 1; i < 17; i++) { byte[] raw = new byte[i]; for (var j = 0; j < i; j++) raw[j] = (byte)j; var enc = Base32Encoding.ToString(raw); Console.WriteLine(enc); var res = Base32Encoding.ToBytes(enc); Console.WriteLine(BitConverter.ToString(res)); }

# by Kevin

把32長度壓縮到30長度,似乎增加了碰撞的風險,這一點有考慮嗎?

# by Jeffrey

to Kevin, 文章所提的應用情境是將byte[16]轉為較短,不分大小寫且不需UrlEncode的純文字表示法,與接收端約定好編碼格式可100%還原,應不存在碰撞問題,或者可否請你再補充可能發生碰撞的情境?

Post a comment