完美的.net泛型也有特定的性能黑點?追根問底並且改善這個性能問題

来源:http://www.cnblogs.com/life2009/archive/2016/07/12/generic-performance.html
-Advertisement-
Play Games

完美的.net真泛型真的完美嗎 碼C#多年,不求甚解覺得泛型就是傳說中那麼完美,性能也是超級好,不錯,在絕大部分場景下泛型表現簡直可以用完美來形容,不過隨著前一陣重做IOC時,才發現與自己預想中不一樣,覺得自己還是圖樣圖森破,太過拿衣服了 在前面一篇文章(一步一步造個IoC輪子(二),詳解泛型工廠) ...


完美的.net真泛型真的完美嗎

碼C#多年,不求甚解覺得泛型就是傳說中那麼完美,性能也是超級好,不錯,在絕大部分場景下泛型表現簡直可以用完美來形容,不過隨著前一陣重做IOC時,才發現與自己預想中不一樣,覺得自己還是圖樣圖森破,太過拿衣服了

在前面一篇文章(一步一步造個IoC輪子(二),詳解泛型工廠)中,我說了泛型工廠帶來"接近new的性能",是錯誤的,我要道歉,其實是完全達不到直接new的性能,差了兩個數量級,當然還是比反射速度強很多很多很多


 

性能黑點出在哪裡?

我來來演示一下普通類型和泛型的實際測試吧

先來做兩個類,一個普通一個泛型

    public class NormalClass
    {

    }
    public class GenericClass<T>
    {

    }

再來寫個迴圈測試

            var sw = new Stopwatch();
            Console.WriteLine("請輸入迴圈次數");
            int max = int.Parse(Console.ReadLine());

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = new NormalClass();
            }
            sw.Stop();
            Console.WriteLine("直接創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = new GenericClass<int>();
            }
            sw.Stop();
            Console.WriteLine("泛型創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            Console.ReadLine();

好了,E3CPU,dotnet core 1.0 Release下測試結果(本篇全部測試結果均是E3CPU,dotnet core 1.0 Release模式下測試)

一千萬次迴圈

直接創建耗時3ms,平均每次0.3ns
泛型創建耗時3ms,平均每次0.3ns

表現簡直完美啊,順便一提.net core速度提高了很多,像這樣的測試如果在.net 2.0-4.6直接new簡單對象一千萬次下表現都是30-50ms左右,.net core這個真是提升了一個數量級.

那麼我說的性能黑點在哪裡了?

問題就在於像泛型工廠這樣的代碼中,在泛型方法里new 泛型對象,我們繼續來段代碼測試一下

        public static ISMS Create()
        {
            return new XSMS();
        }

        public static ISMS Create<T>() where T : class, ISMS, new()
        {
            return new T();
        }

        public static void Main(string[] args)
        {
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            var sw = new Stopwatch();
            Console.WriteLine("請輸入迴圈次數");
            int max = int.Parse(Console.ReadLine());

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Create();
            }
            sw.Stop();
            Console.WriteLine("直接創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Create<XSMS>();
            }
            sw.Stop();
            Console.WriteLine("泛型方法創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            Console.ReadLine();
        }

以上代碼,如果泛型真表現是像我們所預期那麼完美,兩個測試的時間應該是基本相等才對,那來我們來實際測試一下看吧

一千萬次結果

直接創建耗時3ms,平均每次0.3ns
泛型方法創建耗時619ms,平均每次61.9ns

WTF,差異為什麼這麼大,這是什麼回事,200倍啊,傳說中泛型不是幾乎沒有性能損失的麽

考慮到這麼簡單的代碼,身經百碼的我是不可能寫錯的,難道這個是泛型實現的問題?看看實際編譯出什麼鬼再說吧

我們打開一下ILSPY看看IL代碼是什麼樣的,這東西比ildasm用著方便,畢竟我是懶人

原來.net實現這個泛型方法new泛型對象時偷了個懶,直接利用編譯器加上一句System.Activator.CreateInstance<T>()的方法完事,這個打破了我一直美好的幻想,我以為泛型真的表現得像模板一樣完美,JIT時才完全膨脹代碼,都是不求甚解導致我的曲解

追根問底,我們再來把new泛型放到泛型內部看看編譯後的IL

噢NO,跟泛型方法一樣,System.Activator.CreateInstance<T>(),至此我們可以得出結論,new泛型對象都是編譯器利用System.Activator.CreateInstance<T>()來做的

性能也就降到跟System.Activator.CreateInstance<T>()的水平了


改善性能黑點

雖然Activator.CreateInstance已經很快了,但本著鑽研的精神,我們來嘗試加速一下這個創建,至少在泛型中的創建性能,最直接的方法當然是模擬編譯後IL代碼里直接new普通對象的方法了,怎麼處理呢,造一個方法,調用這個方法返回要創建的對象

上代碼再說吧

    public class FastActivator<T> where T : class, new()
    {
        private static readonly Func<T> createFunc = BuildFunc();
        private static Func<T> BuildFunc()
        {
            var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true);
            var il = newMethod.GetILGenerator();
            il.DeclareLocal(typeof(T));
            il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Stloc_0);
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ret);
                        
            return newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;
        }
        public static T CreateInstance()
        {
            return createFunc();
        }
    }

在上面的代碼中,我們創建一個FastActivator<T>的類,T的約束為class而且有空的構造器方法

static readonly Func<T>這裡當訪問到這個類時就調用BuildFunc的方法,還記得前面提到的static readonly魔法嗎,僅僅調用一次,線程安全

CreateInstance()方法里返回createFunc創建的對象

對於IL代碼不瞭解的同學,我來簡單解釋一下這段IL Emit的代碼吧

var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true); //<-創建一個DynamicMethod 動態方法

var il = newMethod.GetILGenerator();//<-取出ILGenerator對象

il.DeclareLocal(typeof(T));//<-接一來定義一個臨時本地變數,類型為T

----------------------------------分隔一下----------------------------------------------------

接下來到IL最核心的代碼構建了,如下代碼

il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));

OpCodes.Newobj是調用構造方法 等同代碼里的new關鍵字,後面 typeof(T).GetConstructor(Type.EmptyTypes)是取出T的空構造方法,整句IL代碼意思等於代碼  new XX類型() ,這裡的XX是T實際的類型

il.Emit(OpCodes.Stloc_0);

OpCodes.Stloc從MSDN里的解釋是:“從計算堆棧的頂部彈出當前值並將其存儲到指定索引處的局部變數列表中。”,意思是把值存入局部變數,Stloc_0中的0就是第0個變數,即我們剛纔在上面定義的那個變數

il.Emit(OpCodes.Ldloc_0);

OpCodes.Ldloc從MSDN里的解釋是:“將指定索引處的局部變數載入到計算堆棧上。”,意思是把變數載入到棧上,這裡是把索引為0的變數加入棧,在IL代碼里基本上都是把參數結果等對載入到棧上做相應操作,寫IL代碼是腦中要有一個棧的表,臨時調用的數據都是存到棧上,然後調用方法時就會把棧的參數一一傳給方法,當然這個我說不清楚,加深瞭解直接用ILSPY和代碼相互參照就是了

il.Emit(OpCodes.Ret);

OpCodes.Ret就是最後一步就是返回了等同代碼里的Return,即使void類型的方法最後一樣也是有個OpCodes.Ret表示當前方法完成並返回,如果棧上有值當然就相當於Return xx了

在上面的代碼里new出來的對象(指針引用)先存在了棧頂部,然後我們又取出來存入變數[0]然後又從變數[0]取出來壓入棧再返回,是否就表示我直接new了就return也行呢

不錯,真的行,把il.Emit(OpCodes.Stloc_0);il.Emit(OpCodes.Ldloc_0);這兩句及變數聲明il.DeclareLocal(typeof(T));去掉實測完全沒有影響,我不知編譯器為何都要加上這兩句,是不夠智能還是相容,不清楚,反正IL代碼執行相當快,加上去掉這兩句千萬次調用基本上時間表現是一致的

最後一個是newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;是利用方法創建一個泛型委托,讓我們可以直接調用委托而不用反射來調用方法

好了,代碼準備好了,是驢是馬拉出來溜一溜就知道了

測試代碼如下

            var sw = new Stopwatch();
            Console.WriteLine("請輸入迴圈次數");
            int max = int.Parse(Console.ReadLine());

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Create();
            }
            sw.Stop();
            Console.WriteLine("直接創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Create<XSMS>();
            }
            sw.Stop();
            Console.WriteLine("泛型方法創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Activator.CreateInstance<XSMS>();
            }
            sw.Stop();
            Console.WriteLine("Activator創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = FastCreate<XSMS>();
            }
            sw.Stop();
            Console.WriteLine("FastActivator創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);
            Console.ReadLine();

測試結果

還是一千萬次

直接創建耗時3ms,平均每次0.3ns
泛型方法創建耗時582ms,平均每次58.2ns
Activator創建耗時552ms,平均每次55.2ns
FastActivator創建耗時130ms,平均每次13ns

雖然比Activator快了近5倍,比預期直接new的速度還是差了兩個數量級,當然在.net2.0-4.6里是一個數量級,WTF究竟慢在哪裡了

好吧,參考泛型工廠里,我們用個靜態的代理對象,代理對象裡面包含個Create方法來創建需要的對象來試試能不能再快點,直接上代碼吧

    internal class FastActivatorModuleBuilder
    {
        public static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DynamicFastTypeCreaterAssembly"), AssemblyBuilderAccess.Run).DefineDynamicModule("DynamicFastTypeCreaterModuleBuilder");
        public static int CurrId;
    }
    public class FastActivator<T> where T : class, new()
    {
        /*//委托方法
        public static readonly Func<T> createFunc = BuildFunc();
        private static Func<T> BuildFunc()
        {
            var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true);
            var il = newMethod.GetILGenerator();
            //il.DeclareLocal(typeof(T));
            il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
            //il.Emit(OpCodes.Stloc_0);
            //il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ret);
                        
            return newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;
        }*/
        public static T CreateInstance()
        {
            //return createFunc();
            return Creater.Create();//調用Creater對象的Create創造T對象
        }

        private static readonly ICreater Creater = BuildCreater();
        public interface ICreater
        {
            T Create();
        }
        private static ICreater BuildCreater()
        {
            var type = typeof(T);
            var typeBuilder = FastActivatorModuleBuilder.ModuleBuilder.DefineType("FastTypeCreater_" + Interlocked.Increment(ref FastActivatorModuleBuilder.CurrId),
                TypeAttributes.Class | TypeAttributes.Public, null, new Type[] { typeof(ICreater) });//創建類型,繼承ICreater介面

            var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);//創建類型的構造方法
            var il = ctor.GetILGenerator();//從構造方法取出ILGenerator
            il.Emit(OpCodes.Ret);//給構造方法加上最基本的代碼(空)

            var createMethod = typeBuilder.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.HideBySig |
                MethodAttributes.NewSlot | MethodAttributes.Virtual |
                MethodAttributes.Final, type, Type.EmptyTypes);//創建介面同名方法
            il = createMethod.GetILGenerator();//從方法取出ILGenerator
            il.DeclareLocal(type);//定義臨時本地變數

            il.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));//調用當前新建類型的構造方法
            il.Emit(OpCodes.Stloc_0);//棧入變數
            il.Emit(OpCodes.Ldloc_0);//變數壓棧
            il.Emit(OpCodes.Ret);//返回棧頂值,方法完成

            typeBuilder.DefineMethodOverride(createMethod, typeof(ICreater).GetMethod("Create"));//跟介面方法根據簽名進行綁定

            var createrType = typeBuilder.CreateTypeInfo().AsType();//創建類型

            return (ICreater)Activator.CreateInstance(createrType);//偷懶用Activator.CreateInstance創造剛剛IL代碼搞的ICreater對象,有了這個對象就可以調用對象的Create方法調用我們自己搞的IL代碼了
        }
    }

老規矩,一千萬次迴圈

直接創建耗時3ms,平均每次0.3ns

泛型方法創建耗時596ms,平均每次59.6ns
Activator創建耗時552ms,平均每次55.2ns
FastActivator創建耗時79ms,平均每次7.9ns

一千萬次,性能繼續有提升,幾乎是泛型方法的8倍,算是提高了一個數量級了,實際上在 .net2.0-4.6里已經是同數量級的速度了,不過.net core狠啊,直接new夠快,這裡性能不如預期的原因,我想了好久,百撕不得騎姐的時候,只能夠再碼點基礎代碼來測試了

        public class TestCreater
        {
            /// <summary>
            /// 直接創建
            /// </summary>
            /// <returns></returns>
            public static ISMS Driect()
            {
                return new XSMS();
            }
            private interface ICreater
            {
                ISMS Create();
            }
            private static readonly ICreater creater = new Creater();
            private class Creater : ICreater
            {
                public ISMS Create()
                {
                    return new XSMS();
                }
            }
            /// <summary>
            /// 每次都創建Creater對象用Creater對象來創建
            /// </summary>
            /// <returns></returns>
            public static ISMS InternalCreaterCreater()
            {
                return new Creater().Create();
            }
            /// <summary>
            /// 使用靜態緩存的Creater創建
            /// </summary>
            /// <returns></returns>
            public static ISMS StaticCreaterCreate()
            {
                return creater.Create();
            }
        }

        public static void Main(string[] args)
        {
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            var sw = new Stopwatch();
            Console.WriteLine("請輸入迴圈次數");
            int max = int.Parse(Console.ReadLine());

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = Create();
            }
            sw.Stop();
            Console.WriteLine("直接創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = TestCreater.Driect();
            }
            sw.Stop();
            Console.WriteLine("TestCreater.Driect方法創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = TestCreater.InternalCreaterCreater();
            }
            sw.Stop();
            Console.WriteLine("TestCreater.InternalCreaterCreater方法創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            sw.Restart();
            for (var i = 0; i < max; i++)
            {
                var x = TestCreater.StaticCreaterCreate();
            }
            sw.Stop();
            Console.WriteLine("TestCreater.StaticCreaterCreate方法創建耗時{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);

            Console.ReadLine();

        }

在上面的測試代碼中,我造了個TestCreater的類分別來測試不同的方法,分別有直接new對象的,用ICreater代理對象來new 對象的及緩存了ICreater代理對象來new對象的

來跑一跑性能表現吧

老規矩,一千萬次迴圈

直接創建耗時3ms,平均每次0.3ns
TestCreater.Driect方法創建耗時3ms,平均每次0.3ns
TestCreater.InternalCreaterCreater方法創建耗時3ms,平均每次0.3ns
TestCreater.StaticCreaterCreate方法創建耗時89ms,平均每次8.9ns

前面兩個方法跟直接new時間完全一致,分不出什麼勝負,最後一個和FastActivator吻合,性能表現完全一致,到這裡我們可以得出結論了,性能下降的原因是由於引用了代理對象,畢竟要訪問堆記憶體,所以這個下降也是理所當然的


 

優化結論

到此,泛型這個性能黑點優化算是完成了,如果要近乎直接new的性能,估計只能熱更新掉運行時已經JIT過的代碼,參考http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time

用這種魔法去提升微乎其微的性能,或者祈求官方在泛型里new不要偷懶在編譯期實現,而是放到JIT的時候再去實現,不知會不會引起迴圈引用的問題

如果對於上面所有的測試代碼認為有編譯器優化的其實可以用ILSPY看一下IL代碼或者最簡單的就是在XSMS構造方法裡加上計數或者控制台輸出就知道這些測試代碼是可靠的,沒有給編譯器優化忽略掉

代碼就不附了,上面有,會加入前面的IOC里改善性能

有更理想方法的同學可以留言討論一下


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要 雖然ASP.NET的伺服器控制項一直被大家所詬病,但是用戶控制項(ACSX)在某些場景下還是非常有用的。 在一些極特珠的情況下,我們會使用JavaScript動態的構建頁面中的控制項,但假設遇到了我要用JavaScript構建一個服務端控制項、用戶控制項時,該怎麼辦? 我們常常說,服務端控制項運行在服端器 ...
  • 根據公司目前的業務情況,進行分散式雲平臺基礎服務建設的架構,現狀,取捨,概述以及展望。 包含資料庫中間件,TCP服務框架,認證中心,服務中心,統一監控,配置中心,消息隊列,任務調度平臺,分散式緩存,文件服務,日誌平臺,開發介面平臺,分散式部署平臺,開發Api網關相關內容。 ...
  • 個人網站地址:nee32.com 一、實體框架(EF)簡介 EF框架是一個數據持久層框架,它的全稱是ADO.NET Entity Framework,是微軟開發的基於ADO.NET的ORM(Object Relational Mapping,對象關係映射)框架,常見的數據持久層框架有還有Nhiber ...
  • 其實完成這個功能之前,我就在思考:是先把想法寫了來,和大伙討論討論後再實現,還是實現後再寫文論述自己的思維。忽然腦後傳來一個聲音說:你發文後會進入發呆階段。所以還是靜下心,讓我輕輕地把代碼擼完再說。最近這幾天,自己在大腦里演練過各種技術難點,解決方案,推敲了各種該解決的問題,覺的差不多了,才決定擼碼... ...
  • Jquery的代碼是這樣實現的:(參考某大神EdieLei的回覆 博客鏈接 http://www.cnblogs.com/edielei) 個人覺得這種方法比之each好,所以拿來記錄: 當然each的方法也可以: ///參考狼牙曼巴的博客 ...
  • ...
  • 代碼很簡單,不多做解釋,如果有疑問和建議請留言,回第一時間回覆 C#代碼first class Program { static void Main(string[] args) { MyCallback mc = new MyCallback(); mc.Callback(new Action(s ...
  • 重寫模板查找方式: Application_Start()註冊 _ViewStart.cshtml @{ Layout = "~/Views/Shared/_Layout.cshtml".Replace("~/Views/", MyRazorViewEngine.GetView(Request)); ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...