九哥聊Kestrel網絡編程第二章:開發一個Fiddler

推薦序

之前在.NET 性能優化群內交流時,我們發現很多朋友對于高性能網絡框架有需求,需要創建自己的消息服務器、游戲服務器或者物聯網網關。但是大多數小伙伴只知道 DotNetty,雖然 DotNetty 是一個非常優秀的網絡框架,廣泛應用于各種網絡服務器中,不過因為各種原因它已經不再有新的特性支持和更新,很多小伙伴都在尋找替代品。

這一切都不用擔心,在.NET Core 以后的時代,我們有了更快、更強、更好的 Kestrel 網絡框架,正如其名,Kestrel 中文翻譯為紅隼(hóng sǔn) 封面就是紅隼的樣子,是一種飛行速度極快的猛禽。Kestrel 是 ASPNET Core 成為.NET 平臺性能最強 Web 服務框架的原因之一,但是很多人還覺得 Kestrel 只是用于 ASPNET Core 的網絡框架,但是其實它是一個高性能的通用網絡框架。

為了讓更多的人了解 Kestrel,和多個千星.NET 開源項目作者九哥[1]一拍即合,計劃寫一系列的文章來介紹它;本文是第二篇,通過 kestrel 實現一個類似 Fiddler 的抓包軟件。

由于公眾號排版問題,在 PC 端瀏覽更佳

1 文章目的

本文講解基于 kestrel 開發類似 Fiddler 應用的過程,讓讀者了解 kestrel 網絡編程里面的 kestrel 中間件和 http 應用中間件。由于最終目的不是輸出完整功能的產品,所以這里只實現 Fiddler 最核心的 http 請求和響應內容查看的功能。本文章是KestrelApp 項目[2]里面的一個 demo 的講解,希望對您有用。

2 開發順序

  1. 代理協議 kestrel 中間件

  2. tls 協議偵測 kestrel 中間件

  3. 隧道和 http 協議偵測 kestrel 中間件

  4. 請求響應分析 http 中間件

  5. 反向代理 http 中間件

  6. 編排中間件創建服務器和應用

3 傳輸層與 kestrel 中間件

所謂傳輸層,其目的是為了讓應用協議數據安全、可靠、快速等傳輸而存在的一種協議,其特征是把應用協議的報文做為自己的負載,常見的 tcp、udp、quic、tls 等都可以理解為傳輸層協議。 比如 http 協議,常見有如下的傳輸方式:

  1. http over tcp

  2. http over tls over tcp

  3. http over quic over udp

3.1 Fiddler 的傳輸層

Fiddler 要處理以下三種 http 傳輸情況:

  1. http over tcp:直接 http 請求首頁

  2. http over proxy over tcp:代理 http 流量

  3. http over tls over proxy over tcp:代理 https 流量

3.2 Kestrel 的中間件

kestrel 目前的傳輸層基于 tcp 或 quic 兩種,同時內置了 tls 中間件,需要調用ListenOptions.UseHttps()來使用 tls 中間件。kestrel 的中間件的表現形式為:Func<ConnectionDelegate, ConnectionDelegate>,為了使用讀者能夠簡單理解中間件,我在KestrelFramework里定義了 kestrel 中間件的變種接口,大家基于此接口來實現更多的中間件就方便很多:

///?<summary>
///?Kestrel的中間件接口
///?</summary>
public?interface?IKestrelMiddleware
{///?<summary>///?執行///?</summary>///?<param?name="next"></param>///?<param?name="context"></param>///?<returns></returns>Task?InvokeAsync(ConnectionDelegate?next,?ConnectionContext?context);
}

4 代理協議 kestrel 中間件

Filddler 最基礎的功能是它是一個 http 代理服務器, 我們需要為 kestrel 編寫代理中間件,用于處理代理傳輸層。http 代理協議分兩種:普通的 http 代理和 Connect 隧道代理。兩種的報文者是遵循 http1.0 或 1.1 的文本格式,我們可以使用 kestrel 自帶的HttpParser<>來解析這些復雜的 http 文本協議。

4.1 代理特征

在中間件編程模式中,Feature是一個很重要的中間件溝通橋梁,它往往是某個中間件工作之后,留下的財產,讓之后的中間件來獲取并受益。我們的代理中間件,也設計了 IProxyFeature,告訴之后的中間件一些代理特征。

///?<summary>
///?代理Feature
///?</summary>
public?interface?IProxyFeature
{///?<summary>///?代理主機///?</summary>HostString?ProxyHost?{?get;?}///?<summary>///?代理協議///?</summary>ProxyProtocol?ProxyProtocol?{?get;?}
}///?<summary>
///?代理協議
///?</summary>
public?enum?ProxyProtocol
{///?<summary>///?無代理///?</summary>None,///?<summary>///?http代理///?</summary>HttpProxy,///?<summary>///?隧道代理///?</summary>TunnelProxy
}

4.2 代理中間件的實現

///?<summary>
///?代理中間件
///?</summary>
sealed?class?KestrelProxyMiddleware?:?IKestrelMiddleware
{private?static?readonly?HttpParser<HttpRequestHandler>?httpParser?=?new();private?static?readonly?byte[]?http200?=?Encoding.ASCII.GetBytes("HTTP/1.1?200?Connection?Established\r\n\r\n");private?static?readonly?byte[]?http400?=?Encoding.ASCII.GetBytes("HTTP/1.1?400?Bad?Request\r\n\r\n");///?<summary>///?解析代理///?</summary>///?<param?name="next"></param>///?<param?name="context"></param>///?<returns></returns>public?async?Task?InvokeAsync(ConnectionDelegate?next,?ConnectionContext?context){var?input?=?context.Transport.Input;var?output?=?context.Transport.Output;var?request?=?new?HttpRequestHandler();while?(context.ConnectionClosed.IsCancellationRequested?==?false){var?result?=?await?input.ReadAsync();if?(result.IsCanceled){break;}try{if?(ParseRequest(result,?request,?out?var?consumed)){if?(request.ProxyProtocol?==?ProxyProtocol.TunnelProxy){input.AdvanceTo(consumed);await?output.WriteAsync(http200);}else{input.AdvanceTo(result.Buffer.Start);}context.Features.Set<IProxyFeature>(request);await?next(context);break;}else{input.AdvanceTo(result.Buffer.Start,?result.Buffer.End);}if?(result.IsCompleted){break;}}catch?(Exception){await?output.WriteAsync(http400);break;}}}///?<summary>///?解析http請求///?</summary>///?<param?name="result"></param>///?<param?name="request"></param>///?<param?name="consumed"></param>///?<returns></returns>private?static?bool?ParseRequest(ReadResult?result,?HttpRequestHandler?request,?out?SequencePosition?consumed){var?reader?=?new?SequenceReader<byte>(result.Buffer);if?(httpParser.ParseRequestLine(request,?ref?reader)?&&httpParser.ParseHeaders(request,?ref?reader)){consumed?=?reader.Position;return?true;}else{consumed?=?default;return?false;}}///?<summary>///?代理請求處理器///?</summary>private?class?HttpRequestHandler?:?IHttpRequestLineHandler,?IHttpHeadersHandler,?IProxyFeature{private?HttpMethod?method;public?HostString?ProxyHost?{?get;?private?set;?}public?ProxyProtocol?ProxyProtocol{get{if?(ProxyHost.HasValue?==?false){return?ProxyProtocol.None;}if?(method?==?HttpMethod.Connect){return?ProxyProtocol.TunnelProxy;}return?ProxyProtocol.HttpProxy;}}void?IHttpRequestLineHandler.OnStartLine(HttpVersionAndMethod?versionAndMethod,?TargetOffsetPathLength?targetPath,?Span<byte>?startLine){method?=?versionAndMethod.Method;var?host?=?Encoding.ASCII.GetString(startLine.Slice(targetPath.Offset,?targetPath.Length));if?(versionAndMethod.Method?==?HttpMethod.Connect){ProxyHost?=?HostString.FromUriComponent(host);}else?if?(Uri.TryCreate(host,?UriKind.Absolute,?out?var?uri)){ProxyHost?=?HostString.FromUriComponent(uri);}}void?IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte>?name,?ReadOnlySpan<byte>?value){}void?IHttpHeadersHandler.OnHeadersComplete(bool?endStream){}void?IHttpHeadersHandler.OnStaticIndexedHeader(int?index){}void?IHttpHeadersHandler.OnStaticIndexedHeader(int?index,?ReadOnlySpan<byte>?value){}}
}

5 tls 協議偵測 kestrel 中間件

Fiddler 只監聽了一個端口,要同時支持非加密和加密兩種流量,如果不調用調用ListenOptions.UseHttps(),我們的程序就不支持 https 的分析;如果直接調用ListenOptions.UseHttps(),會讓我們的程序不支持非加密的 http 的分析,這就要求我們有條件的根據客戶端發來的流量分析是否需要開啟。

我已經在KestrelFramework內置了TlsDetection中間件,這個中間件可以根據客戶端的實際流量類型來選擇是否使用 tls。在 Fiddler 中,我們還需要根據客戶端的tls握手中的sni使用 ca 證書來動態生成服務器證書用于 tls 加密傳輸。

///?<summary>
///?證書服務
///?</summary>
sealed?class?CertService
{private?const?string?CACERT_PATH?=?"cacert";private?readonly?IMemoryCache?serverCertCache;private?readonly?IEnumerable<ICaCertInstaller>?certInstallers;private?readonly?ILogger<CertService>?logger;private?X509Certificate2??caCert;///?<summary>///?獲取證書文件路徑///?</summary>public?string?CaCerFilePath?{?get;?}?=?OperatingSystem.IsLinux()???$"{CACERT_PATH}/fiddler.crt"?:?$"{CACERT_PATH}/fiddler.cer";///?<summary>///?獲取私鑰文件路徑///?</summary>public?string?CaKeyFilePath?{?get;?}?=?$"{CACERT_PATH}/fiddler.key";///?<summary>///?證書服務///?</summary>///?<param?name="serverCertCache"></param>///?<param?name="certInstallers"></param>///?<param?name="logger"></param>public?CertService(IMemoryCache?serverCertCache,IEnumerable<ICaCertInstaller>?certInstallers,ILogger<CertService>?logger){this.serverCertCache?=?serverCertCache;this.certInstallers?=?certInstallers;this.logger?=?logger;Directory.CreateDirectory(CACERT_PATH);}///?<summary>///?生成CA證書///?</summary>public?bool?CreateCaCertIfNotExists(){if?(File.Exists(this.CaCerFilePath)?&&?File.Exists(this.CaKeyFilePath)){return?false;}File.Delete(this.CaCerFilePath);File.Delete(this.CaKeyFilePath);var?notBefore?=?DateTimeOffset.Now.AddDays(-1);var?notAfter?=?DateTimeOffset.Now.AddYears(10);var?subjectName?=?new?X500DistinguishedName($"CN={nameof(Fiddler)}");this.caCert?=?CertGenerator.CreateCACertificate(subjectName,?notBefore,?notAfter);var?privateKeyPem?=?this.caCert.GetRSAPrivateKey()?.ExportRSAPrivateKeyPem();File.WriteAllText(this.CaKeyFilePath,?new?string(privateKeyPem),?Encoding.ASCII);var?certPem?=?this.caCert.ExportCertificatePem();File.WriteAllText(this.CaCerFilePath,?new?string(certPem),?Encoding.ASCII);return?true;}///?<summary>///?安裝和信任CA證書///?</summary>public?void?InstallAndTrustCaCert(){var?installer?=?this.certInstallers.FirstOrDefault(item?=>?item.IsSupported());if?(installer?!=?null){installer.Install(this.CaCerFilePath);}else{this.logger.LogWarning($"請根據你的系統平臺手動安裝和信任CA證書{this.CaCerFilePath}");}}///?<summary>///?獲取頒發給指定域名的證書///?</summary>///?<param?name="domain"></param>///?<returns></returns>public?X509Certificate2?GetOrCreateServerCert(string??domain){if?(this.caCert?==?null){using?var?rsa?=?RSA.Create();rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));this.caCert?=?new?X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);}var?key?=?$"{nameof(CertService)}:{domain}";var?endCert?=?this.serverCertCache.GetOrCreate(key,?GetOrCreateCert);return?endCert!;//?生成域名的1年證書X509Certificate2?GetOrCreateCert(ICacheEntry?entry){var?notBefore?=?DateTimeOffset.Now.AddDays(-1);var?notAfter?=?DateTimeOffset.Now.AddYears(1);entry.SetAbsoluteExpiration(notAfter);var?extraDomains?=?GetExtraDomains();var?subjectName?=?new?X500DistinguishedName($"CN={domain}");var?endCert?=?CertGenerator.CreateEndCertificate(this.caCert,?subjectName,?extraDomains,?notBefore,?notAfter);//?重新初始化證書,以兼容win平臺不能使用內存證書return?new?X509Certificate2(endCert.Export(X509ContentType.Pfx));}}///?<summary>///?獲取域名///?</summary>///?<param?name="domain"></param>///?<returns></returns>private?static?IEnumerable<string>?GetExtraDomains(){yield?return?Environment.MachineName;yield?return?IPAddress.Loopback.ToString();yield?return?IPAddress.IPv6Loopback.ToString();}
}

6 隧道和 http 協議偵測 kestrel 中間件

經過KestrelProxyMiddleware后的流量,在 tls 解密(如果可能)之后,一般情況下都是 http 流量了,但如果你在 qq 設置代理到我們這個偽 Fildder 之后,會發現部分流量流量不是 http 流量,原因是 http 隧道也是一個通用傳輸層,可以傳輸任意 tcp 或 tcp 之上的流量。所以我們需要新的中間件來檢測當前流量,如果不是 http 流量就回退到隧道代理的流程,即我們不跟蹤不分析這部分非 http 流量。

6.1 http 流量偵測

///?<summary>
///?流量偵測器
///?</summary>
private?static?class?FlowDetector
{private?static?readonly?byte[]?crlf?=?Encoding.ASCII.GetBytes("\r\n");private?static?readonly?byte[]?http10?=?Encoding.ASCII.GetBytes("?HTTP/1.0");private?static?readonly?byte[]?http11?=?Encoding.ASCII.GetBytes("?HTTP/1.1");private?static?readonly?byte[]?http20?=?Encoding.ASCII.GetBytes("?HTTP/2.0");///?<summary>///?傳輸內容是否為http///?</summary>///?<param?name="context"></param>///?<returns></returns>public?static?async?ValueTask<bool>?IsHttpAsync(ConnectionContext?context){var?input?=?context.Transport.Input;var?result?=?await?input.ReadAtLeastAsync(1);var?isHttp?=?IsHttp(result);input.AdvanceTo(result.Buffer.Start);return?isHttp;}private?static?bool?IsHttp(ReadResult?result){var?reader?=?new?SequenceReader<byte>(result.Buffer);if?(reader.TryReadToAny(out?ReadOnlySpan<byte>?line,?crlf)){return?line.EndsWith(http11)?||?line.EndsWith(http20)?||?line.EndsWith(http10);}return?false;}
}

6.2 隧道回退中間件

///?<summary>
///?隧道傳輸中間件
///?</summary>
sealed?class?KestrelTunnelMiddleware?:?IKestrelMiddleware
{private?readonly?ILogger<KestrelTunnelMiddleware>?logger;///?<summary>///?隧道傳輸中間件///?</summary>///?<param?name="logger"></param>public?KestrelTunnelMiddleware(ILogger<KestrelTunnelMiddleware>?logger){this.logger?=?logger;}///?<summary>///?執行中間你件///?</summary>///?<param?name="next"></param>///?<param?name="context"></param>///?<returns></returns>public?async?Task?InvokeAsync(ConnectionDelegate?next,?ConnectionContext?context){var?feature?=?context.Features.Get<IProxyFeature>();if?(feature?==?null?||?feature.ProxyProtocol?==?ProxyProtocol.None){this.logger.LogInformation($"偵測到http直接請求");await?next(context);}else?if?(feature.ProxyProtocol?==?ProxyProtocol.HttpProxy){this.logger.LogInformation($"偵測到普通http代理流量");await?next(context);}else?if?(await?FlowDetector.IsHttpAsync(context)){this.logger.LogInformation($"偵測到隧道傳輸http流量");await?next(context);}else{this.logger.LogInformation($"跳過隧道傳輸非http流量{feature.ProxyHost}的攔截");await?TunnelAsync(context,?feature);}}///?<summary>///?隧道傳輸其它協議的數據///?</summary>///?<param?name="context"></param>///?<param?name="feature"></param>///?<returns></returns>private?async?ValueTask?TunnelAsync(ConnectionContext?context,?IProxyFeature?feature){var?port?=?feature.ProxyHost.Port;if?(port?==?null){return;}try{var?host?=?feature.ProxyHost.Host;using?var?socket?=?new?Socket(SocketType.Stream,?ProtocolType.Tcp);await?socket.ConnectAsync(host,?port.Value,?context.ConnectionClosed);Stream?stream?=?new?NetworkStream(socket,?ownsSocket:?false);//?如果有tls中間件,則反回來加密隧道if?(context.Features.Get<ITlsConnectionFeature>()?!=?null){var?sslStream?=?new?SslStream(stream,?leaveInnerStreamOpen:?true);await?sslStream.AuthenticateAsClientAsync(feature.ProxyHost.Host);stream?=?sslStream;}var?task1?=?stream.CopyToAsync(context.Transport.Output);var?task2?=?context.Transport.Input.CopyToAsync(stream);await?Task.WhenAny(task1,?task2);}catch?(Exception?ex){this.logger.LogError(ex,?$"連接到{feature.ProxyHost}異常");}}
}

7 請求響應分析 http 中間件

這部分屬于 asp.netcore 應用層內容,關鍵點是制作可多次讀取的 http 請求 body 流和 http 響應 body 流,因為每個分析器實例都可以會重頭讀取一次請求內容和響應內容。

7.1 http 分析器

為了方便各種分析器的獨立實現,我們定義 http 分析器的接口

///?<summary>
///?http分析器
///?支持多個實例
///?</summary>
public?interface?IHttpAnalyzer
{///?<summary>///?分析http///?</summary>///?<param?name="context"></param>///?<returns></returns>ValueTask?AnalyzeAsync(HttpContext?context);
}

這是輸到日志的 http 分析器

public?class?LoggingHttpAnalyzer?:?IHttpAnalyzer
{private?readonly?ILogger<LoggingHttpAnalyzer>?logger;public?LoggingHttpAnalyzer(ILogger<LoggingHttpAnalyzer>?logger){this.logger?=?logger;}public?async?ValueTask?AnalyzeAsync(HttpContext?context){var?builder?=?new?StringBuilder();var?writer?=?new?StringWriter(builder);writer.WriteLine("[REQUEST]");await?context.SerializeRequestAsync(writer);writer.WriteLine("[RESPONSE]");await?context.SerializeResponseAsync(writer);this.logger.LogInformation(builder.ToString());}
}

7.2 分析 http 中間件

我們把請求 body 流和響應 body 流保存到臨時文件,在所有分析器工作之后再刪除。

///?<summary>
///?http分析中間件
///?</summary>
sealed?class?HttpAnalyzeMiddleware
{private?readonly?RequestDelegate?next;private?readonly?IEnumerable<IHttpAnalyzer>?analyzers;///?<summary>///?http分析中間件///?</summary>///?<param?name="next"></param>///?<param?name="analyzers"></param>public?HttpAnalyzeMiddleware(RequestDelegate?next,IEnumerable<IHttpAnalyzer>?analyzers){this.next?=?next;this.analyzers?=?analyzers;}///?<summary>///?分析代理的http流量///?</summary>///?<param?name="context"></param>///?<returns></returns>public?async?Task?InvokeAsync(HttpContext?context){var?feature?=?context.Features.Get<IProxyFeature>();if?(feature?==?null?||?feature.ProxyProtocol?==?ProxyProtocol.None){await?next(context);return;}context.Request.EnableBuffering();var?oldBody?=?context.Response.Body;using?var?response?=?new?FileResponse();try{//?替換response的bodycontext.Response.Body?=?response.Body;//?請求下個中間件await?next(context);//?處理分析await?this.AnalyzeAsync(context);}finally{response.Body.Position?=?0L;await?response.Body.CopyToAsync(oldBody);context.Response.Body?=?oldBody;}}private?async?ValueTask?AnalyzeAsync(HttpContext?context){foreach?(var?item?in?this.analyzers){context.Request.Body.Position?=?0L;context.Response.Body.Position?=?0L;await?item.AnalyzeAsync(context);}}private?class?FileResponse?:?IDisposable{private?readonly?string?filePath?=?Path.GetTempFileName();public?Stream?Body?{?get;?}public?FileResponse(){this.Body?=?new?FileStream(filePath,?FileMode.Open,?FileAccess.ReadWrite);}public?void?Dispose(){this.Body.Dispose();File.Delete(filePath);}}
}

8 反向代理 http 中間件

我們需要把請求轉發到真實的目標服務器,這時我們的應用程序是一個 http 客戶端角色,這個過程與 nginx 的反向代理是一致的。具體的實現上,我們直接使用 yarp 庫來完成即可。

///?<summary>
///?http代理執行中間件
///?</summary>
sealed?class?HttpForwardMiddleware
{private?readonly?RequestDelegate?next;private?readonly?IHttpForwarder?httpForwarder;private?readonly?HttpMessageInvoker?httpClient?=?new(CreateSocketsHttpHandler());///?<summary>///?http代理執行中間件///?</summary>///?<param?name="next"></param>///?<param?name="httpForwarder"></param>public?HttpForwardMiddleware(RequestDelegate?next,IHttpForwarder?httpForwarder){this.next?=?next;this.httpForwarder?=?httpForwarder;}///?<summary>///?轉發http流量///?</summary>///?<param?name="context"></param>///?<returns></returns>public?async?Task?InvokeAsync(HttpContext?context){var?feature?=?context.Features.Get<IProxyFeature>();if?(feature?==?null?||?feature.ProxyProtocol?==?ProxyProtocol.None){await?next(context);}else{var?scheme?=?context.Request.Scheme;var?destinationPrefix?=?$"{scheme}://{feature.ProxyHost}";await?httpForwarder.SendAsync(context,?destinationPrefix,?httpClient,?ForwarderRequestConfig.Empty,?HttpTransformer.Empty);}}private?static?SocketsHttpHandler?CreateSocketsHttpHandler(){return?new?SocketsHttpHandler{Proxy?=?null,UseProxy?=?false,UseCookies?=?false,AllowAutoRedirect?=?false,AutomaticDecompression?=?DecompressionMethods.None,};}
}

9 編排中間件創建服務器和應用

9.1 kestrel 中間件編排

這里要特別注意順序,傳輸層套娃。

///?<summary>
///??ListenOptions擴展
///?</summary>
public?static?partial?class?ListenOptionsExtensions
{///?<summary>///?使用Fiddler的kestrel中間件///?</summary>///?<param?name="listen"></param>public?static?ListenOptions?UseFiddler(this?ListenOptions?listen){//?代理協議中間件listen.Use<KestrelProxyMiddleware>();//?tls偵測中間件listen.UseTlsDetection(tls?=>{var?certService?=?listen.ApplicationServices.GetRequiredService<CertService>();certService.CreateCaCertIfNotExists();certService.InstallAndTrustCaCert();tls.ServerCertificateSelector?=?(context,?domain)?=>?certService.GetOrCreateServerCert(domain);});//?隧道代理處理中間件listen.Use<KestrelTunnelMiddleware>();return?listen;}
}

9.2 http 中間件的編排

public?static?class?ApplicationBuilderExtensions
{///?<summary>///?使用Fiddler的http中間件///?</summary>///?<param?name="app"></param>public?static?void?UseFiddler(this?IApplicationBuilder?app){app.UseMiddleware<HttpAnalyzeMiddleware>();app.UseMiddleware<HttpForwardMiddleware>();}
}

9.3 創建應用

我們可以在傳統的 MVC 里創建偽 fiddler 的首頁、下載證書等 http 交互頁面。

public?static?void?Main(string[]?args)
{var?builder?=?WebApplication.CreateBuilder(args);builder.Services.AddFiddler().AddControllers();builder.WebHost.ConfigureKestrel((context,?kestrel)?=>{var?section?=?context.Configuration.GetSection("Kestrel");kestrel.Configure(section).Endpoint("Fiddler",?endpoint?=>?endpoint.ListenOptions.UseFiddler());});var?app?=?builder.Build();app.UseRouting();app.UseFiddler();app.MapControllerRoute(name:?"default",pattern:?"{controller=Home}/{action=Index}/{id?}");app.Run();
}

10 留給讀者

如果讓您來開發個偽 Fiddler,除了本文的方法,您會使用什么方式來開發呢?

參考資料

[1]

九哥: https://www.cnblogs.com/kewei/

[2]

KestrelApp項目: https://github.com/xljiulang/KestrelApp

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/280319.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/280319.shtml
英文地址,請注明出處:http://en.pswp.cn/news/280319.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

apple tv 開發_如何跨多臺Apple TV同步Apple TV的主屏幕

apple tv 開發If you have more than one Apple TV in your household, you probably know how annoying it is when you have to install Apple TV apps multiple times on each device. However, with the release of tvOS 11, that’s no longer the case. 如果您的家庭中有…

這些故事說的都是你——譯者帶你讀《硅谷革命》

作者 | 薛命燈 作為《硅谷革命》的譯者之一&#xff0c;同時也是一個擁有十余年軟件開發和架構經驗的工程師&#xff0c;當時我在決定是否接受重譯這本書的時候&#xff0c;幾乎是不假思索地答應了郭蕾&#xff08;本書重啟版發起人之一&#xff09;的提議&#xff0c;只因他的…

runc容器逃逸漏洞最強后續:應對之策匯總與熱點疑問解答

美國時間2019年2月11日晚&#xff0c;runc通過oss-security郵件列表披露了runc容器逃逸漏洞CVE-2019-5736的詳情。runc是Docker、CRI-O、Containerd、Kubernetes等底層的容器運行時&#xff0c;此次安全漏洞無可避免地會影響大多數Docker與Kubernetes用戶&#xff0c;也因此為整…

OOD之問題空間到解空間—附FP的建模

通常會被問到&#xff0c;什么事OOD&#xff0c;然后大部分人期待的答案比較死板&#xff0c;繼承、封裝、多態&#xff01;懂這個的人多的去了&#xff0c;有什么好問&#xff1f;回答出來的人是否拿著Java又去做一些面向過程的勾當&#xff1f; 計算機革命起源于機器&#xf…

com surrogate_什么是“ COM Surrogate”(dllhost.exe),為什么它在我的PC上運行?

com surrogateIf you poke around in your Task Manager, there’s a good chance you’ll see one or more “COM Surrogate” processes running on a Windows PC. These processes have the file name “dllhost.exe”, and are part of the Windows operating system. You’…

云計算時代,互聯網金融背后的想象空間

本文講的是云計算時代&#xff0c;互聯網金融背后的想象空間&#xff0c;【IT168評論】阿里巴巴在紐交所的開市鐘史無前例的由八位合作伙伴敲響&#xff0c;可見阿里對互聯網時代構筑起生態系統的堅持。這其中&#xff0c;由余額寶所敲開的互聯網金融熱潮的熱度持續不減&#x…

JavaScript數據結構與算法——集合

1.集合數據結構 集合是一組無序且唯一&#xff08;不能重復&#xff09;的項組成的。這個數據結構使用了和有限集合相同的數學概念。 2.創建集合 function Set() {// 這里使用對象而不是數組來表示集合 // js對象中不允許一個鍵值指向兩個不同屬性&#xff0c;也保證了集合中的…

php用兩個棧來實現隊列

php用兩個棧來實現隊列 一、總結 我主要的問題是不知道的是題目描述&#xff0c;題目和貴的代碼之間的關系&#xff0c;以及返回值 思路&#xff1a;A棧做入隊操作&#xff0c;B棧做出隊操作&#xff0c;入隊的時候元素直接入A&#xff0c;出隊的時候判斷B棧是否為空&#xff0…

facebook 邀請好友_如何查看緊急情況下您的Facebook朋友是否安全

facebook 邀請好友Facebook’s Safety Check feature lets you check in during an emergency to confirm you’re safe. If you have friends or family in an area that you haven’t heard from, though, you may want to ask them directly. Here’s how to ask someone to…

【您有一封來自阿里云的邀請函】阿里云成都客戶服務中心20+職位虛席以待,來吧,成就最好的自己!...

如果你不想辜負這個科技的時代&#xff0c;相信它會因你而不同。如果你不想僅做年度大戲的觀眾&#xff0c;相信自己會成為主角。如果你不想淹沒在枯燥與茍且中&#xff0c;相信工作有詩和遠方。那么&#xff0c;不要猶豫&#xff0c;加入我們&#xff01;在這&#xff0c;你已…

A - A Secret -擴展KMP

題目大意&#xff1a;給你兩個字符串A,B&#xff0c;現在要你求B串的后綴在A串中出現的次數和后綴長度的乘積和為多少。題解&#xff1a;擴展KMP模板題&#xff0c;將A和B串都逆序以后就變成了求前綴的問題了&#xff0c;擴展KMP求處從i位置開始的最長公共前綴存于數組。最后通…

.NET 代碼優化 聊聊邏輯圈復雜度

本文屬于 dotnet 代碼優化系列博客。相信大家都對圈復雜度這個概念很是熟悉&#xff0c;本文來和大家聊聊邏輯的圈復雜度。代碼優化里面&#xff0c;一個關注的重點在于代碼的邏輯復雜度。一段代碼的邏輯復雜度越高&#xff0c;那么維護起來的難度也就越大。衡量代碼的邏輯復雜…

GO語言基礎條件、跳轉、Array和Slice

1. 判斷語句if 1. 條件表達式沒有括號&#xff08;這點其他語言轉過來的需要注意&#xff09; 2. 支持一個初始化表達式&#xff08;可以是并行方式&#xff0c;即&#xff1a;a, b, c : 1, 2, 3) 3. 左大括號必須和條件語句或 else 在同一行 4. 支持單行模式 5. 初始化語句中的…

干式真空泵原理_如何安裝干式墻錨在墻壁上懸掛重物

干式真空泵原理If you ever plan to mount something to the wall that’s even remotely heavy, you’ll need to use drywall anchors if a stud isn’t available. Here are the different types of drywall anchors, and how to use each one. 如果您打算將甚至更重的東西安…

sharding-jdbc學習

sharding-jdbc的全局id生成策略是通過雪花算法來實現的。 sharding-jdbc也是一個數據的中間件&#xff0c;可實現讀寫分離和分庫分表&#xff0c;比mycat要簡單些。 nginx與ribbon實現負載均衡的區別&#xff1a;nginx是實現服務器端的負載均衡&#xff0c;ribbon是實現客戶端即…

像go 一樣 打造.NET 單文件應用程序的編譯器項目bflat 發布 7.0版本

現代.NET和C#在低級/系統程序以及與C/C/Rust等互操作方面的能力完全令各位刮目相看了&#xff0c;有人用C#開發的64位操作系統: GitHub - nifanfa/MOOS: C# x64 operating system pro...&#xff0c;截圖要介紹的是一個結合Roslyn和NativeAOT的實驗性編譯器bflat &#xff1a;h…

添加dubbo.xsd的方法

整合dubbo-spring的時候&#xff0c;配置文件會報錯 因為 阿里關閉在線的域名了.需要本地下載xsd文件 所以&#xff0c;需要下載本地引入。 解決方式&#xff1a; 在dubbo的開源項目上找到xsd文件&#xff1a; https://github.com/alibaba/dubbo Idea使用本地xsd Setting…

Spring Cloud Feign注意點

2019獨角獸企業重金招聘Python工程師標準>>> 1、只要在啟動類中加入EnableFeignClients注解&#xff0c;才會掃描FeignClient注解 2、Feign主要是通過接口調用&#xff0c;底層其實也是HttpClient/OkHttp 1&#xff09;提供一個Feign接口&#xff0c;加入對應的rest…

.gitkeep是什么? .gitignore和.gitkeep之間的區別(譯)

你是不是在git工程里遇到過.gitkeep文件&#xff1f;如果你通過angular腳手架來生成angular2或者angular4工程&#xff0c;你會發現.gitkeep文件在./src/app/assets文件夾里。你對著個文件感到奇怪嗎&#xff1f;我們都知道我們的老朋友.gitignore。你也許會覺得它是.gitignore…

掃描PDF417崩潰的原因找到:手機攝像頭分辨率低

換孩子姥姥華為手機解決了。 能掃pdf417碼了轉載于:https://www.cnblogs.com/strongdady/p/9049155.html