網關never_host設計

来源:https://www.cnblogs.com/shelldudu/archive/2019/07/02/11115761.html
-Advertisement-
Play Games

never下app的host與api Never是純c#語言開發的一個框架。host則是使用該框架開發出來的API網關,它包括了:路由、認證、鑒權、熔斷,內置了負載均衡器Deployment;並且只需要簡單的配置即可完成。 設計的核心思路:host負責轉發 + 身份識別 + 熔斷,api提供業務處理 ...


never下app的host與api

Never是純c#語言開發的一個框架。host則是使用該框架開發出來的API網關,它包括了:路由、認證、鑒權、熔斷,內置了負載均衡器Deployment;並且只需要簡單的配置即可完成。

設計的核心思路:host負責轉發 + 身份識別 + 熔斷,api提供業務處理(類似一個編排)

1、基本使用

用一臺機器來運行host,配置文件配置程式埠,api地址,限流次數信息等。

(1)程式host啟動的時候去配置中心讀取文件,讀取成功後IConfiguration介面就可以讀取相關配置;

(2)程式host會監聽客戶端請求,對header、body等進行包裝,並且會進行身份認識,將請求下發到api伺服器進行處理,再將請求結果返回;

(3)程式host設置一個健康檢查,api配置的地址如果不可用,則返回不可處理結果。由於讀取api的配置信息是從配置中心的,所以配置中心也可以使用熔斷設計。

 

2、集成identity service

當我們說到的identity,就是你有沒有訪問這個api的資源,這裡可以分2種:第一種是有沒有許可權訪問這個系統(要求登陸),第二種是登陸了有沒有許可權訪問系統裡面某一個資源。對於第一種,我們可以採用AOP的統一處理方式;比如只要驗證token就可以,第二種則是獲取 到用戶標識了,用戶會在我們後臺分配一定的許可權資源,許可權資源 + 身份標識 + 請求信息結合驗證就可以了。

為了業務劃分清楚,我們將host與api的分工要特別說明

  1. host,這個可以對我們的請求做路由轉發,健康檢查,身份驗證,數據加密,負載均衡。
  2. api,我們的業務所在地。有些情況是前端請求從host轉發到api裡面的時候會帶上身份,在api裡面我們可以通過Mvc一些Aop做法得到用戶信息,比IAuthorizationFilter介面,Never.Web.WebApi.Security.UserPrincipalAttribute特性等

3、服務發現

我們統一使用配置中心去獲取服務,配置中心在更新配置的時候會非同步下發當前配置請求,host程式的健康檢查會發現對服務不可用的時候做熔斷處理,這個配置中心裡面的服務配置可以從db管理(可以擴展為服務主動註冊),可以手動編寫。

配置host

下載demo,github地址:https://github.com/shelldudu/never_application

在host項目中,我們多加個配置文件appsettings.app.json,還有一個是系統的appsettings.json配置文件,為什麼會配置2個文件?appsettings.json文件是配置程式啟動的埠 + 配置中心的訪問地址,通常是比較固定的;而appsettings.app.json則是其實動態獲取的配置,比如分api的地址,限流的信息,這些都是通過配置中心管理,而配置中心可以通過後臺管理。

//統一使用配置中心,方便管理
e.Startup.UseConfigClient(new IPEndPoint(IPAddress.Parse(configReader["config_host"]), configReader.IntInAppConfig("config_port")), out var configFileClient);
//啟動配置中心,每10秒的心跳,並且指定當前讀取配置中心下麵的app_host文件內容。
configFileClient.Startup(TimeSpan.FromMinutes(10), new[] { new ConfigFileClientRequest { FileName = "app_host" } }, (c, t) =>
{
    var content = t;
    if (c != null && c.FileName == "app_host")
    {
        System.IO.File.WriteAllText(System.IO.Path.Combine(this.Environment.ContentRootPath, "appsettings.app.json"), content);
    }
}).Push("app_host").GetAwaiter().GetResult();

netcore系統新加appsettings.app.json監聽文件則是通過下麵的代碼實現

//程式名字
var pathToExe = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
//程式所在位置
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
return WebHost.CreateDefaultBuilder(args)
//監聽2個文件
    .UseJsonFileConfig(Never.Web.WebApi.StartupExtension.ConfigFileBuilder(new[] { "appsettings.json", "appsettings.app.json" }))
//使用kestrel
    .UseKestrel((builder, option) =>
    {
        //主要是重寫監聽url
        var ports = string.Empty;option.Listen(System.Net.IPAddress.Any, ports);
}) .UseContentRoot(pathToContentRoot) .UseStartup<Startup>()

UseJsonFileConfig這個擴展是在IConfigurationBuilder裡面使用AddConfiguration方法加配置文件的讀取與監聽,這個AddConfiguration方法是系統提供的。

//config builder
builder.ConfigureAppConfiguration((h, g) =>
{
    var files = jsonConfigFiles?.Invoke(h);
    if (files.IsNotNullOrEmpty())
    {
        foreach (var file in files)
        {
            if (file.Exists)
                g.AddConfiguration(new ConfigurationBuilder().SetBasePath(h.HostingEnvironment.ContentRootPath).AddJsonFile(file.FullName, true, true).Build());
            else
                throw new System.IO.FileNotFoundException(string.Format("找不到文件{0}", file.FullName));
        }
    }
});

host發現服務

由於有配置中心的存在,我們可以讀取api裡面的服務地址(也可以擴展為服務主動註冊),但是我們並不知道該地址是否為可用的,於是我們就有必要做一個對地址的迴圈檢查。我們約定請求服務地址裡面的A10Url項,去請求這個A10Url的地址內容,如果返回是work內容的表明可使用,其他表示不可用。這個work內容可以由自己內容約定(可在ProxyRouteDispatcher構造函數裡面傳遞),只是never下的deplyment約定請求的是a10路由是否可用。

圖中A10的健康非同步檢查,開戶一個Timer或Thread定時去拿到服務地址信息元素A10Url的內容,只有返回了work表明該元素的ApiUrl是可用的。

//讀取服務地址,構造函數可以傳遞如何匹配A10Url內容的回調
private class ProxyRouteDispatcher : DefaultApiRouteProvider
{
    private readonly IConfigReader configReader = null;

    public ProxyRouteDispatcher(IConfigReader configReader)
    {
        this.configReader = configReader;
    }

    public override IEnumerable<ApiUrlA10Element> ApiUrlA10Elements
    {
        get
        {
            /*讀取AppA10:url:0,AppA10:url:.1這個配置信息,如下麵的配置
                * {
                    "application": "true",
                    "version": "1123",
                    "AppA10": {
                    "url": [ "http://127.0.0.1:8081/", "http://127.0.0.1:8081/" ],
                    "ping": [ "http://127.0.0.1:8081/a10", "http://127.0.0.1:8081/a10" ]
                    }
                }
                */
        }
    }
}

健康檢查

/// <summary>
/// 路由中間件
/// </summary>
private class ProxyMiddlewear : IMiddleware
{
    private readonly AuthenticationService authenticationService = null;
    private readonly IApiUriDispatcher proxyRouteDispatcher = null;

    public ProxyMiddlewear(AuthenticationService authenticationService, IConfigReader configReader)
    {
        this.authenticationService = authenticationService;
        var provider = new ProxyRouteDispatcher(configReader);
        //開戶一個健康檢查,表示60秒會檢查一遍,檢查地址為ProxyRouteDispatcher.ApiUrlA10Elements裡面的A10Url
        var a10 = Never.Deployment.StartupExtension.StartReport().Startup(60, new[] { provider });
        this.proxyRouteDispatcher = new ApiUriDispatcher<IApiRouteProvider>(provider, a10);
    }
}

host轉發路由

轉發路由,要包含請求的querystring,header,以及body這三者信息。首先我們通過發現服務裡面的ProxyRouteDispatcher對象我們可知道當前待轉發的ApiUrl,存在2個以上ApiUrl我們就要使用策略去選擇我們應該用哪一條,系統預設取條數[條數%請求Ascill碼]

//拿api地址,如果存在多條可用的api地址的話,則找出其中一條,這裡還要結合限流等策略
var host = new HostString(this.proxyRouteDispatcher.GetCurrentUrlHost((context.Request.ContentLength.HasValue ? context.Request.ContentLength.Value : segments[1].GetHashCode()).ToString()));
var url = UriHelper.BuildAbsolute("http", host, context.Request.PathBase, context.Request.Path, context.Request.QueryString, default(FragmentString));

1、querystring  上面可以知道我們通過”var url =“代碼知道整個url的完整地址

2、header  我們可以將HttpContext.Request對象裡面的Headers都加入到我們的請求中,當然,有些Header的key不一定全部都要,因此我們只選擇了幾個有用的放到了header

//客戶端地址
if (context.Connection.RemoteIpAddress != null)
{
    headers["ip"] = context.Connection.RemoteIpAddress.ToString();
}
if (context.Request.Headers != null)
{
    //通過X-Real-IP,X-Forwarded-For等nginx傳遞過來的客戶端ip地址
    headers["ip"] = context.GetContextIP();
}

//查詢身份認證,accesstoken不要傳遞到api,api根本不知道這個accesstoken是用來做什麼的
var user = this.authenticationService.GetUser(context, token);
if (user.HasValue && user.Value > 0)
{
    headers["userid"] = user.Value.ToString();
}
//查找platform關鍵信息
if (context.Request.Headers != null && context.Request.Headers.Keys.Any(ta=>ta.IsEquals("platform")))
{
    var value = context.Request.Headers["platform"];
    headers["platform"] = value.ToString();
}

3、body 由於我們在這裡對數據加了密,所以我們要對body進行解密處理,如果沒有加密的,直接使用Context.Request.Body對象就可以了。下麵的模擬post請求

//開始請求
using (var body = this.ConvertContentFromBodyByteArray(context, enctryptor))
{
    using (var method = new Never.Utils.MethodTickCount(""))
    {
        var task = new HttpRequestDownloader().PostString(new Uri(url), body, header, "application/json");
        var content = task;// task.GetAwaiter().GetResult();
        return this.ConvertContentToBody(context, content, enctryptor);
    }
}

body數據的加解密

//請求的body讀取後進行3des解密
private Stream ConvertContentFromBodyByteArray(HttpContext context, IContentEncryptor enctryptor)
{
    using (var st = new MemoryStream())
    {
        context.Request.Body.CopyTo(st);
        st.Position = 0;
        var @byte = st.ToArray();
        return enctryptor.Decrypt(@byte, new[] { "utf-8" });
    }
}

//請求回來的內容將進行3desc加密
private Task ConvertContentToBody(HttpContext context, byte[] content, IContentEncryptor enctryptor)
{
    var @byte = enctryptor.Encrypt(content);
    return context.Response.Body.WriteAsync(@byte, 0, @byte.Length);
}

//請求回來的內容將進行3desc加密
private Task ConvertContentToBody(HttpContext context, string content, IContentEncryptor enctryptor)
{
    var @string = enctryptor.Encrypt(content);
    return context.Response.WriteAsync(@string);
}

有同學會問如果是get,delete等請求呢,這又怎麼做?實際也很好做,我們用httpclient來當例子,喜歡的同學可以研究一下

/// <summary>
/// 使用HTTPClient處理請求
/// </summary>
public Task ReverseInvokeAsync(HttpContext context, RequestDelegate next, ProxyRouteDispatcher dispatcher, Uri uri)
{
    var requestMessage = new System.Net.Http.HttpRequestMessage()
    {
        RequestUri = uri,
        Method = new System.Net.Http.HttpMethod(context.Request.Method),
    };

    //沒有body內容的請求
    var requestMethod = context.Request.Method;
    if (!(HttpMethods.IsGet(requestMethod) || HttpMethods.IsHead(requestMethod) || HttpMethods.IsDelete(requestMethod) || HttpMethods.IsTrace(requestMethod)))
    {
        var content = new System.Net.Http.StreamContent(context.Request.Body);
        requestMessage.Content = content;
    }

    //加入所有的header
    if (requestMessage.Content != null && requestMessage.Content.Headers != null)
    {
        foreach (var header in context.Request.Headers)
        {
            requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
        }
    }

    //開始請求
    using (var httpClient = new System.Net.Http.HttpClient(new System.Net.Http.HttpClientHandler() { AutomaticDecompression = System.Net.DecompressionMethods.GZip }) { })
    using (var responseMessage = httpClient.SendAsync(requestMessage, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, context.RequestAborted).GetAwaiter().GetResult())
    {
        context.Response.StatusCode = (int)responseMessage.StatusCode;
        foreach (var header in responseMessage.Headers)
            context.Response.Headers[header.Key] = header.Value.ToArray();

        foreach (var header in responseMessage.Content.Headers)
            context.Response.Headers[header.Key] = header.Value.ToArray();

        //表示輸出的內容長度不能確定
        context.Response.Headers.Remove("transfer-encoding");
        //copy到body裡面去了
        responseMessage.Content.CopyToAsync(context.Response.Body);
    }

    return Task.CompletedTask;
}
View Code

host的身份認證

在使用netcore做demo。先回顧我們上面說到的“集成identity service”,同時我們要自問一下什麼身份認證?是跟鑒權一樣的功能?基本上扯上鑒權,又要說到許可權,而許可權的理解,做CRM的同學會比較清楚。而傳統鑒權基本流程就是如下

上面是傳的鑒權流程;

(1)對於AccessToken的使用還是比較簡單的,只要驗證這個AccessToken是否合法便行,合法的條件如下:該AccessToken是本程式生成的,不能使用別的程式生成,AccessToken可以在本程式內找到,比如使用memcached技術實現,當前我們的程式還加了特比的條件:AccessToken可以加解密。如下麵的代碼

/// <summary>
/// 獲取從header中Token
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Token GetToken(HttpContext context)
{
    //查詢accesstoken
    var token = context.Request.Headers.ContainsKey("accesstoken") ? context.Request.Headers["accesstoken"].FirstOrDefault() : string.Empty;
    //空的話返回預設的token
    if (string.IsNullOrEmpty(token))
    {
        return new Token() { CryptToken = "56dc54a07f3d15a400000155" };
    }
    //嘗試對accesstoken使用加解密
    try
    {
        var splits = token.From3DES("56dc54a07f3d15a400000155").Split('|');
        if (splits != null && splits.Length == 2)
        {
            return new Token()
            {
                AccessToken = token,
                CryptToken = splits[0],
                UserToken = splits[1]
            };
        }
    }
    catch
    {
        //異常的話返回預設的token
        return new Token() { CryptToken = "56dc54a07f3d15a400000155" };
    }
    //空的話返回預設的token
    return new Token() { CryptToken = "56dc54a07f3d15a400000155" };
}

(2)這個AccessToken是怎麼生成的?這必然要求用戶先登陸了才可以生成。用戶登陸,是不是意味著要輸入賬號與密碼信息,要求後端提供的這個login介面服務,如果這個host是承載多個業務api的,不同的業務api有不同的host,AccessToken怎麼根據業務api生成不同的標識,系統A的AccessToken是否可用在系統B?這樣是否會出現串號?

引發這樣的一系列問題,我們首先確定這個host是否承載多個業務api?如果是承載多種業務api,那麼必然要求所有的生成AccessToken是符合當前host程式的要求的:

  • 多種業務api不可能說我要根據你當前使用的技術去生成AccessToken吧,這樣你後面一改這種host技術那我們的業務api豈不是全部都要改,造成天下大亂了;因此如果業務api生成Token的就要求host要使用業務api的一些標準:不能修改Token。假如我想實現對數據加解密,這是否意味著加解密的演算法只能放在業務api那裡了?不可能說我整個服務提供了AccessToken又提供了SecurityToken給到客戶端吧,要解決這個方面,我們設定有2種方案:放在host那裡,則host要求業務api在生成這個AccessToken的時候加上加解密的信息;放在api那裡生成,如果host處理報文,這樣好明顯與單一設計原則違背,整個加解密應該是個統一方案,不可能說業務api提供一半實現而host又要提供一半實現;如果api處理報文,報文的複雜度,加密的服務等整個業務api做成了功能太大太多的膨脹方式,即便這種問題是可以通過aop+中間件去處理,至少業務api做加解密的時候開發調試找bug難度加大,報文服務配置文件也會到處都存在,同時還有鑒權的問題去解決呢。這樣有沒有人想過為什麼要分host與api2個項目?
  • 當前host程式如果提供了login服務,那麼後面每加一種服務,這個host就要重新更新,最後會造成類似單點故障的問題了,並且host不能涉及具體業務的代碼處理。所以明確了這個host只能為某種業務api提供服務,不能承接多種業務api服務
  • 程式host不提供login服務介面,而業務api又不能生成AccessToken,那麼可以分解為:api提供login服務,host提供生成AccessToken,那麼就要解決host什麼時候生成AccessToken了,所以host與api應該有一定的契約約定

當業務api提供了login服務介面後,我們的host轉發的時候要知道這個路由等下是要生成AccessToken的,這樣當login服務介面返回了正確的驗證信息後,host就生成AccessToken了

//host與api約定處理方案生成的AccessToken
using (var body = this.ConvertContentFromBodyByteArray(context, enctryptor))
{
    //註冊與登陸,由於在這裡做identity servie
    switch (segments[2])
    {
        //註冊
        case "Register":
        //登陸
        case "Login":
            {
                var loginTask = new HttpRequestDownloader().PostString(new Uri(url), body, header, "application/json", 0);
                var loginContent = loginTask;
                var target = EasyJsonSerializer.Deserialize<Never.Web.WebApi.Controllers.BasicController.ResponseResult<UserIdToken>>(loginContent);
                //驗證成功,此時要生成AccessToken信息
                if (target != null && target.Code == "0000" && target.Data.UserId > 0)
                {
                    var token2 = this.authenticationService.SignIn(context, target.Data.UserId).GetAwaiter().GetResult();
                    var appresult = new Never.Web.WebApi.Controllers.BasicController.ResponseResult<AppToken>(target.Code, new AppToken { @accesstoken = token2.AccessToken }, target.Message);
                    return this.ConvertContentToBody(context, EasyJsonSerializer.Serialize(appresult), enctryptor);
                }
                //驗證不成功,返回驗證信息
                var appresult2 = new Never.Web.WebApi.Controllers.BasicController.ResponseResult<AppToken>(target.Code, new AppToken { @accesstoken = string.Empty }, target.Message);
                return this.ConvertContentToBody(context, EasyJsonSerializer.Serialize(appresult2), enctryptor);
            }
    }
}

AccessToken是用戶身份標識,這裡都已經可以拿到了用戶了,想要實現傳統的鑒權,應該不難了吧。

上面用的路由方式去表述了host與api之間的約定,還有很多方案的,舉個慄子:api在登陸與註冊的處理中在header返回個標識,或者返回個特定的status。

host的限流

從上面我們可以拿到了apiurl元素,每個apiurl正在處理的請求有多少都是可以統計出來的,只要這個統計數達到限流後便可以達到限流作用。當然限流目前會有2種處理方式:等待,放棄。

1、放棄 通常我們不要先選擇放棄,我們可以嘗試使用其他的api,因為上面說到"首先我們通過發現服務裡面的ProxyRouteDispatcher對象我們可知道當前待轉發的ApiUrl,存在2個以上的我們就要使用策略去選擇我們應該用哪一條",所以應該儘可能遍歷所有可用的ApiUrl,實在找不到可用的再放棄,response直接返回,比如返回503。

2、等待,可以使用讓重試,線程睡眠,自旋等技術,感興趣的去看看文章:熔斷,限流,降級

程式中沒有做限流技術,目前最快也只是載入放棄,重試幾次手段。

關於集群

大家可以發現這裡的沒有集群信息的,由於host對api有健康檢查,集群不會放到api;配置中心又會做心跳與重連接,host有可能掛,因此集群應該是放到host + 配置中心。我們後面可以嘗試實現一些,期待後面的更新吧!


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

-Advertisement-
Play Games
更多相關文章
  • ARR是應用級別的負載均衡方案,ARR只能做請求入口的分發服務,而NLB則是伺服器級別的負載均衡方案。我們將二者結合起來,形成高可用最佳方案。 ...
  • C#中的這個幾個關鍵字:explicit、implicit與operator,估計好多人的用不上,什麼情況,這是什麼?字面解釋:explicit:清楚明白的;易於理解的;(說話)清晰的,明確的;直言的;坦率的;直截了當的;不隱晦的;不含糊的。implicit:含蓄的;不直接言明的;成為一部分的;內含 ...
  • 使用ItemContainerGenerator.ContainerFromItem方法可以獲取對應數據的UIElement 。 但是如果使用了虛擬化技術,超出可見區域的UIElement就獲取不到了。 參考微軟的文檔《如何:在 TreeView 中查找 TreeViewItem》,去掉一些不必要的 ...
  • 本文分別說明.NET CORE與Spring Boot 編寫控制台程式應有的“正確”方法,以便.NET程式員、JAVA程式員可以相互學習與加深瞭解,註意本文只介紹用法,不會刻意強調哪種語言或哪種框架寫的控制台程式要好。 本文所說的編寫控制台程式應有的“正確”方法,我把正確二字加上引號,因為沒有絕對的 ...
  • C# 單例模式 1、定義:單例模式就是保證在整個應用程式的生命周期中,在任何時刻,被指定的類只有一個實例,併為客戶程式提供一個獲取該實例的全局訪問點。 2、單例模式的優點有: (1)實例控制:單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。 (2)靈活性:因為類控 ...
  • DES(Data Encryption Standard)的加密與MD5不同,DES可以解密,而MD5的加密是不可逆的;用於數字簽名和數據加密,對稱加密-即加密秘鑰和解密秘鑰相同。標準的DES密鑰長度為64bit,密鑰每個字元占7bit,外加1bit的奇偶校驗,64/(7+1)=8;所以必須是8個字 ...
  • 原文:https://www.stevejgordon.co.uk/httpclientfactory-named-typed-clients-aspnetcore 發表於:2018年1月 原文:https://www.stevejgordon.co.uk/httpclientfactory-nam ...
  • 最近利用周末時間,完成了線上轉換服務的各個功能模塊。 網站地址:http://101.201.64.215:8088 前臺網站採用MVC結構。文件支持本地文件和網路文件。 後臺服務採用WCF服務。後臺支持多個服務端同時運行,高效負載。 主要功能: 1.Word,Excel,PPT文件轉PDF文件。 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...