關于dotnet?替換?ASP.NET?Core?的底層通訊為命名管道的?IPC?庫的問題
這是一個用于本機多進程進行 IPC 通訊的庫,此庫的頂層 API 是采用 ASP.NET Core 的 MVC 框架,其底層通訊不是傳統(tǒng)的走網(wǎng)絡的方式,而是通過 dotnetCampus.Ipc 開源項目提供的基于 NamedPipeStream 命名管道的方式進行通訊。相當于替換掉 ASP.NET Core 的底層通訊方式,從走網(wǎng)絡換成命名管道的方式。本庫的優(yōu)勢是可以使用設計非常好的 ASP.NET Core 的 MVC 框架作為頂層調(diào)用 API 層,底層通訊采用可提升傳輸性能的命名管道,如此可以做到不走網(wǎng)絡通訊從而極大減少網(wǎng)絡端口占用問題和減少用戶端網(wǎng)絡環(huán)境帶來的問題
這是一個用于本機多進程進行 IPC 通訊的庫,此庫的頂層 API 是采用 ASP.NET Core 的 MVC 框架,其底層通訊不是傳統(tǒng)的走網(wǎng)絡的方式,而是通過 dotnetCampus.Ipc 開源項目提供的基于 NamedPipeStream 命名管道的方式進行通訊。相當于替換掉 ASP.NET Core 的底層通訊方式,從走網(wǎng)絡換成命名管道的方式。本庫的優(yōu)勢是可以使用設計非常好的 ASP.NET Core 的 MVC 框架作為頂層調(diào)用 API 層,底層通訊采用可提升傳輸性能的命名管道,如此可以做到不走網(wǎng)絡通訊從而極大減少網(wǎng)絡端口占用問題和減少用戶端網(wǎng)絡環(huán)境帶來的問題
背景
本機內(nèi)多進程通訊 IPC 不同于跨設備系統(tǒng)的 RPC 通訊方式,大多數(shù)的 IPC 通訊都需要處理復雜的用戶端環(huán)境問題。對于 RPC 通訊來說,大部分時候,服務端都在開發(fā)者完全管控的環(huán)境下運行。但 IPC 通訊則無論是服務端還是客戶端都可能是在用戶端運行的。然而用戶端上,無論是系統(tǒng)還是其他環(huán)境都是十分復雜的,特別是在國內(nèi)的,魔改的系統(tǒng),兇狠的殺毒軟件,這些都會讓 IPC 通訊受到非預期的打斷
傳統(tǒng)的 dotnet 系的 IPC 手段有很多個,提供給開發(fā)使用的頂層框架也有很多,如 .NET Remoting 和 WCF 等。但是在遷移到 dotnet core 時,由于底層運行時機制的變更,如透明代理不再支持類對象只能支持接口的行為變更,就讓 .NET Remoting 從機制性不受支持。為了方便將應用遷移到 dotnet core 框架上,可采用 dotnet campus 組織基于最友好的 MIT 協(xié)議開源的 dotnetCampus.Ipc 開源庫進行本機內(nèi)多進程通訊
此 dotnetCampus.Ipc 開源庫底層可基于命名管道進行通訊,經(jīng)過了約 600 萬臺設備近半年的測試,發(fā)現(xiàn)通過此方式的通訊穩(wěn)定性極高。開源倉庫地址:https://github.com/dotnet-campus/dotnetCampus.Ipc
無論是 RPC 還是 IPC 通訊,其頂層提供給開發(fā)者使用的 API 層,主流上有兩個設計陣營。一個是如 .NET Remoting 一樣的傳輸類對象的方式,此方法可以極大隱藏 RPC 或 IPC 的細節(jié),調(diào)用遠程進程的對象就和調(diào)用本機進程一樣。另一個陣營是本文的主角,如 ASP.NET Core 的 MVC 模式,通過路由配合參數(shù)傳遞,進行控制器處理的模式,此方式的優(yōu)良設計已被 ASP.NET Core 所證明,本文也就不多說了
默認下,如此妙的 ASP.NET Core 的 MVC 層框架是僅提供網(wǎng)絡傳輸?shù)姆绞?。然而在詭異的用戶端環(huán)境下,將有層出不窮的網(wǎng)絡通訊問題,如端口被占用,特殊的軟件阻止上網(wǎng)等等。讓 ASP.NET Core 從走網(wǎng)絡的方式,替換為走命名管道的方式,可以極大提升在用戶端的穩(wěn)定性
再次表揚 ASP.NET Core 的優(yōu)秀設計,在 ASP.NET Core 里,各個模塊分層明確,這也就讓更換 ASP.NET Core 里的“通訊傳輸”(其實本意是 IServer 層)這個工作十分簡單
在采用 ASP.NET Core 作為 IPC 的頂層調(diào)用時,那此時的通訊方式一定就是 服務端-客戶端 的形式。服務端可以采用替換 ASP.NET Core 的“通訊傳輸”為 dotnetCampus.Ipc 的基于命名管道的傳輸方式??蛻舳四??對 ASP.NET Core 來說,最期望客戶端的行為是通過 HttpClient 來進行發(fā)起調(diào)用。剛好 dotnet 下默認的 HttpClient 是支持注入具體的消息傳輸實現(xiàn),通過將 dotnetCampus.Ipc 封裝為 HttpClient 的消息傳輸 HttpMessageHandler 就可以讓客戶端也走 dotnetCampus.Ipc 的傳輸。如此封裝,相當于在 服務端和客戶端 的底層傳輸,全部都在 dotnetCampus.Ipc 層內(nèi),分層圖如下,通過 dotnetCampus.Ipc 維持穩(wěn)定的傳輸從而隱藏具體的 IPC 細節(jié),業(yè)務端可以完全復用原有的知識,無須引入額外的 IPC 知識
充當 IPC 里的服務端和客戶端的業(yè)務代碼將分別與 ASP.NET Core 和 HttpClient 對接。而 ASP.NET Core 和 HttpClient 又與 dotnetCampus.Ipc 層對接,一切的跨進程通訊邏輯都在 dotnetCampus.Ipc 這一層內(nèi)完成,由 dotnetCampus.Ipc 層維持穩(wěn)定的 IPC 傳輸。下面來看看如何使用此方式開發(fā)應用
使用方法
接下來將使用 PipeMvcServerDemo 和 PipeMvcClientDemo 這兩個例子項目來演示如何使用 ASP.NET Core 的 MVC 層框架加命名管道 NamedPipeStream 做通訊傳輸?shù)谋緳C內(nèi)多進程的跨進程通訊 IPC 方式
按照慣例,在 dotnet 系的應用上使用庫之前,先通過 NuGet 進行安裝。從業(yè)務上人為分為服務端和業(yè)務端的兩個項目,分別安裝給服務端用的 dotnetCampus.Ipc.PipeMvcServer 庫,和給客戶端用的 dotnetCampus.Ipc.PipeMvcClient 庫
新建的 PipeMvcServerDemo 和 PipeMvcClientDemo 這兩個基于 .NET 6 的例子項目都是先基于 WPF 的項目模板創(chuàng)建,從業(yè)務上人為分為服務端和業(yè)務端的兩個項目其實都是運行在相同的一個計算機內(nèi),只是為了方便敘述,強行將 PipeMvcServerDemo 稱為服務端項目,將 PipeMvcClientDemo 稱為客戶端項目
服務端
先從 PipeMvcServerDemo 服務端項目開始寫起,在安裝完成 dotnetCampus.Ipc.PipeMvcServer 庫之后,為了使用上 ASP.NET Core 的 MVC 框架,需要在此 WPF 應用里面初始化 ASP.NET Core 框架
初始化的邏輯,和純放在服務器上的 ASP.NET Core 服務應用只有一點點的差別,那就是在初始化時,需要調(diào)用 UsePipeIpcServer 擴展方法,注入 IPC 的服務替換掉默認的 ASP.NET Core 的“通訊傳輸”(IServer)層。代碼如下
using dotnetCampus.Ipc.PipeMvcServer; private static void RunMvc(string[] args) { var builder = WebApplication.CreateBuilder(args); // 下面一句是關鍵邏輯 builder.WebHost.UsePipeIpcServer("PipeMvcServerDemo"); builder.Services.AddControllers(); var app = builder.Build(); app.MapControllers(); app.Run(); }
調(diào)用 UsePipeIpcServer 擴展方法,需要額外加上 using dotnetCampus.Ipc.PipeMvcServer;
命名空間。在 UsePipeIpcServer 方法里面需要傳入一個參數(shù),此參數(shù)用于開啟的 IPC 服務所使用的服務名,也就是作為命名管道的管道名。服務名的字符串要求是在當前機器上唯一不重復,推薦采用屬性的命名法對其命名傳入。此后,客戶端的代碼即可采用此服務名連接上服務端
也僅僅只需加上 UsePipeIpcServer 擴展方法即可完成對服務端的 IPC 的所有配置
客戶端
完成服務端的配置之后,可以開始對客戶端的配置邏輯,客戶端只需要知道服務端的服務名,即如上例子的 "PipeMvcServerDemo"
字符串,即可建立和服務端的通訊。在此庫的設計上,可以認為服務端的服務名和傳統(tǒng)的 C/S 端應用的服務端地址是等同的,至少需要知道服務端的地址才能連接上
在客戶端的任意代碼里,可采用 IpcPipeMvcClientProvider 提供的 CreateIpcMvcClientAsync 靜態(tài)方法傳入服務名,拿到可以和服務端通訊的 HttpClient 對象,如以下代碼
using dotnetCampus.Ipc.PipeMvcClient; HttpClient ipcPipeMvcClient = await IpcPipeMvcClientProvider.CreateIpcMvcClientAsync("PipeMvcServerDemo");
以上代碼拿到的 ipcPipeMvcClient
對象即可和傳統(tǒng)的邏輯一樣,進行服務端的請求邏輯,如下文所演示的例子??梢钥吹娇蛻舳说呐渲眠壿?,也只有在初始化時,獲取 HttpClient 的邏輯不同
如上面演示的代碼,可以看到,無論是客戶端還是服務端,初始化的代碼都是一句話,沒有很多的細節(jié)邏輯,方便入手
調(diào)用
下面開始演示服務端和客戶端調(diào)用的例子。為了讓客戶端能調(diào)用到客戶端對應的服務內(nèi)容,需要先在服務端創(chuàng)建對應的服務邏輯。以下將演示 GET 和 POST 方法和對應的路由和參數(shù)調(diào)用方法
在服務端 PipeMvcServerDemo 項目上添加一個 FooController 控制器,代碼如下
[Route("api/[controller]")] [ApiController] public class FooController : ControllerBase { public FooController(ILogger<FooController> logger) { Logger = logger; } public ILogger<FooController> Logger { get; } }
在 FooController 添加 Get 方法,代碼如下
[HttpGet] public IActionResult Get() { Logger.LogInformation("FooController_Get"); return Ok(DateTime.Now.ToString()); }
根據(jù) ASP.NET Core 的路由知識,可以在客戶端通過 api/Foo
路徑訪問到以上的 Get 方法。接下來編寫客戶端的邏輯,先在客戶端上的 XAML 界面上添加按鈕,代碼如下
<Button x:Name="GetFooButton" Margin="10,10,10,10" Click="GetFooButton_Click">Get</Button>
在 GetFooButton_Click
方法里面,使用預先拿到的 HttpClient 進行通訊,代碼如下
using System.Net.Http; private async void MainWindow_Loaded(object sender, RoutedEventArgs e) { Log($"Start create PipeMvcClient."); var ipcPipeMvcClient = await IpcPipeMvcClientProvider.CreateIpcMvcClientAsync("PipeMvcServerDemo"); _ipcPipeMvcClient = ipcPipeMvcClient; Log($"Finish create PipeMvcClient."); } private HttpClient? _ipcPipeMvcClient; private async void GetFooButton_Click(object sender, RoutedEventArgs e) if (_ipcPipeMvcClient is null) { return; } Log($"[Request][Get] IpcPipeMvcServer://api/Foo"); var response = await _ipcPipeMvcClient.GetStringAsync("api/Foo"); Log($"[Response][Get] IpcPipeMvcServer://api/Foo {response}");
以上的 Log 方法將輸出日志到界面的 TextBlock 控件
以上代碼通過 await _ipcPipeMvcClient.GetStringAsync("api/Foo");
訪問到服務端的 Get 方法,運行效果如下
如上圖可以看到,客戶端成功調(diào)用了服務端,從服務端拿到了返回值
接下來的例子是在 GET 請求帶上參數(shù),如實現(xiàn)遠程調(diào)用計算服務功能,在客戶端發(fā)送兩個 int 數(shù)給服務端進行計算相加的值。服務端的代碼如下
public class FooController : ControllerBase { [HttpGet("Add")] public IActionResult Add(int a, int b) { Logger.LogInformation($"FooController_Add a={a};b="); return Ok(a + b); } }
客戶端在 XAML 界面添加對應按鈕的代碼省略,按鈕的事件里調(diào)用方法代碼如下
private async void GetFooWithArgumentButton_Click(object sender, RoutedEventArgs e) { Log($"[Request][Get] IpcPipeMvcServer://api/Foo/Add"); var response = await _ipcPipeMvcClient.GetStringAsync("api/Foo/Add?a=1&b=1"); Log($"[Response][Get] IpcPipeMvcServer://api/Foo/Add {response}"); }
運行效果如下
可以看到客戶端成功調(diào)用了服務端執(zhí)行了計算,拿到了返回值
通過以上的例子可以看到,即使底層更換為 IPC 通訊,對于上層業(yè)務代碼,調(diào)用服務端的邏輯,依然沒有引入任何新的 IPC 知識,都是對 HttpClient 的調(diào)用
接下來是 POST 調(diào)用的代碼,服務端在 FooController 類上添加 Post 方法,加上 HttpPostAttribute 特性,代碼如下
[HttpPost] public IActionResult Post() { Logger.LogInformation("FooController_Post"); return Ok($"POST {DateTime.Now}"); }
客戶端編寫 PostFooButton 按鈕,在按鈕點擊事件添加如下代碼用于請求服務端
private async void PostFooButton_Click(object sender, RoutedEventArgs e) { Log($"[Request][Post] IpcPipeMvcServer://api/Foo"); var response = await _ipcPipeMvcClient.PostAsync("api/Foo", new StringContent("")); var m = await response.Content.ReadAsStringAsync(); Log($"[Response][Post] IpcPipeMvcServer://api/Foo {response.StatusCode} {m}"); }
運行效果如下圖
如上圖可以看到客戶端成功采用 POST 方法請求到服務端
接下來將采用 POST 方法帶參數(shù)方式請求服務端,服務端處理客戶端請求過來的參數(shù)執(zhí)行實際的業(yè)務邏輯,服務端的代碼依然放在 FooController 類里
[HttpPost("PostFoo")] public IActionResult PostFooContent(FooContent foo) { Logger.LogInformation($"FooController_PostFooContent Foo1={foo.Foo1};Foo2={foo.Foo2 ?? "<NULL>"}"); return Ok($"PostFooContent Foo1={foo.Foo1};Foo2={foo.Foo2 ?? "<NULL>"}"); }
以上代碼采用 FooContent 作為參數(shù),類型定義如下
public class FooContent { public string? Foo1 { set; get; } public string? Foo2 { set; get; } }
客戶端代碼如下,為了給出更多細節(jié),我將不使用 PostAsJsonAsync 方法,而是先創(chuàng)建 FooContent 對象,將 FooContent 對象序列化為 json 字符串,再 POST 請求
private async void PostFooWithArgumentButton_Click(object sender, RoutedEventArgs e) { Log($"[Request][Post] IpcPipeMvcServer://api/Foo"); var json = JsonSerializer.Serialize(new FooContent { Foo1 = "Foo PostFooWithArgumentButton", Foo2 = null, }); StringContent content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _ipcPipeMvcClient.PostAsync("api/Foo/PostFoo", content); var m = await response.Content.ReadAsStringAsync(); Log($"[Response][Post] IpcPipeMvcServer://api/Foo/PostFoo {response.StatusCode} {m}"); }
運行效果如下圖
如上圖,客戶端成功將 FooContent 參數(shù)傳給服務端
以上就是 GET 和 POST 的例子,幾乎看不出來加上 IPC 前后對 ASP.NET Core 應用調(diào)用的差別,除了要求需要使用特定的 HttpClient 對象之外,其他的邏輯都相同。以上的例子項目,可以從本文末尾獲取
如關注此庫的實現(xiàn)原理,請繼續(xù)閱讀下文
原理
先從客戶端方向開始,在客戶端里使用的 HttpClient 是被注入了使用 IPC 底層框架通訊的 IpcNamedPipeClientHandler 對象,此 IpcNamedPipeClientHandler 對象是一個繼承 HttpMessageHandler 類型的對象
在 IpcNamedPipeClientHandler 重寫了 HttpMessageHandler 類型的 SendAsync 方法,可以讓所有使用 HttpClient 發(fā)送的請求,進入 IpcNamedPipeClientHandler 的邏輯。在此方法里面,將序列化請求,將請求通過 dotnetCampus.Ipc 發(fā)送到服務端,再通過 dotnetCampus.Ipc 提供的消息請求機制,等待收到服務端對此請求的返回值。等收到服務端的返回值之后,封裝成為 HttpResponseMessage 返回值,讓此返回值接入到 HttpClient 的機制框架,從而實現(xiàn)調(diào)用 HttpClient 發(fā)送的請求是通過 dotnetCampus.Ipc 層傳輸而不是走網(wǎng)絡。進入 dotnetCampus.Ipc 層是被設計為對等層,對客戶端來說,進入 dotnetCampus.Ipc 層具體是走到 ASP.NET Core 的 MVC 或者是其他框架都是不需要關注的。對客戶端來說,只需要知道進入 dotnetCampus.Ipc 層的請求,可以進行異步等待請求,細節(jié)邏輯不需要關注
以下是 IpcNamedPipeClientHandler 的實現(xiàn)代碼
class IpcNamedPipeClientHandler : HttpMessageHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 序列化請求消息,準備通過 IPC 層發(fā)送 var message = HttpMessageSerializer.Serialize(request); // 創(chuàng)建 IPC 消息的 Tag 內(nèi)容,此 Tag 內(nèi)容僅用來調(diào)試和記錄日志 var ipcMessageTag = request.RequestUri?.ToString() ?? request.Method.ToString(); // 在 dotnetCampus.Ipc 層,采用 P2P 模型,沒有具體的服務端和客戶端 // 但是 P2P 模型是可以模擬 C/S 模型的,只需要讓某個端(Peer)充當服務端,另外的端充當客戶端即可 // 在 dotnetCampus.Ipc 庫里,采用 PeerProxy 表示某個端 // 這里的端表示的是 IPC 的某個端,大部分時候可以認為是一個進程 // 以下的 ServerProxy 就是充當服務端的一個端,將在此框架內(nèi)被初始化創(chuàng)建 // 通過 PeerProxy 發(fā)送 IPC 請求,此時的 IPC 請求將會被 PipeMvcServer 處理 // 在 PipeMvcServer 里面,將通過 ASP.NET Core MVC 框架層進行調(diào)度,分發(fā)到對應的控制器處理 // 控制器處理完成之后,將由 MVC 框架層將控制器的輸出交給 PipeMvcServer 層 // 在 PipeMvcServer 層收到控制器的輸出之后,將通過 IPC 框架,將輸出返回給 PipeMvcClient 端 // 當 PipeMvcClient 收到輸出返回值后,以下的 await 方法將會返回 var response = await ServerProxy.GetResponseAsync(new IpcMessage(ipcMessageTag, message)); // 將 IPC 返回的消息反序列化為 HttpResponseMessage 用于接入 HttpClient 框架 return HttpMessageSerializer.DeserializeToResponse(response.Body); } private PeerProxy ServerProxy { get; } // 忽略其他代碼 }
這就是為什么客戶端需要通過 IpcPipeMvcClientProvider 的 CreateIpcMvcClientAsync 拿到 HttpClient 的原因。在 CreateIpcMvcClientAsync 方法,不僅需要創(chuàng)建 HttpClient 對象,還需要先嘗試連接服務端。盡管從 HttpClient 的設計上,應該是發(fā)起請求時才去連接服務端,但因為這是 IPC 通訊,且為了解決 IPC 初始化邏輯的多進程資源競爭,當前版本采用在獲取 HttpClient 也就是發(fā)起具體請求之前,連接服務端
/// <summary> /// 提供給客戶端調(diào)用 MVC 的 Ipc 服務的功能 /// </summary> public static class IpcPipeMvcClientProvider { /// <summary> /// 獲取訪問 Mvc 的 Ipc 服務的對象 /// </summary> /// <param name="ipcPipeMvcServerName">對方 Ipc 服務名</param> /// <param name="clientIpcProvider">可選,用來進行 Ipc 連接的本地服務。如不傳或是空,將創(chuàng)建新的 Ipc 連接服務</param> /// <returns></returns> public static async Task<HttpClient> CreateIpcMvcClientAsync(string ipcPipeMvcServerName, IpcProvider? clientIpcProvider = null) { if (clientIpcProvider == null) { clientIpcProvider = new IpcProvider(); clientIpcProvider.StartServer(); } var peer = await clientIpcProvider.GetAndConnectToPeerAsync(ipcPipeMvcServerName); return new HttpClient(new IpcNamedPipeClientHandler(peer, clientIpcProvider)) BaseAddress = new Uri(IpcPipeMvcContext.BaseAddressUrl), }; } }
在 dotnetCampus.Ipc 層是采用 P2P 方式設計的,因此客戶端也需要創(chuàng)建自己的 IpcProvider 對象。客戶端可選傳入已有的 IpcProvider 對象進行復用,就如 HttpClient 復用邏輯一樣。但創(chuàng)建 IpcProvider 對象是很便宜的,不會占用多少資源,是否復用在性能上沒有多少影響。但是支持傳入 IpcProvider 更多是可以方便開發(fā)者對 IpcProvider 進行的定制邏輯,例如注入自己的數(shù)組池和日志等
以上就是客戶端的邏輯。關于如何序列化請求消息等,這些就屬于細節(jié)了,無論采用什么方法,只需要能將請求和響應與二進制 byte 數(shù)組進行序列化和反序列化即可。細節(jié)內(nèi)容還請自行在本文末尾獲取源代碼進行閱讀
服務端的邏輯相對復雜一些,在服務端的 dotnetCampus.Ipc 層收到客戶端的請求后,服務端將構建一個虛擬的訪問請求,此訪問請求將通過 繼承 IServer 接口的 IpcServer 對象,在 ASP.NET Core 框架內(nèi)發(fā)起請求,通過 MVC 框架層處理之后將響應返回到 IpcServer 對象里交給 dotnetCampus.Ipc 層傳輸給客戶端
在 IpcServer 對象的啟動函數(shù),也就是 StartAsync 函數(shù)里面,將會同步初始化 IpcPipeMvcServerCore 對象。在 IpcPipeMvcServerCore 對象里面將初始化創(chuàng)建 dotnetCampus.Ipc 層的通訊機制。代碼如下
public class IpcServer : IServer { public IpcServer(IServiceProvider services, IFeatureCollection featureCollection, IOptions<IpcServerOptions> optionsAccessor) { // 忽略代碼 var ipcCore = Services.GetRequiredService<IpcPipeMvcServerCore>(); IpcPipeMvcServerCore = ipcCore; } Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) // 忽略代碼 IpcPipeMvcServerCore.Start(); private IpcPipeMvcServerCore IpcPipeMvcServerCore { get; } // 忽略代碼 }
而 IpcPipeMvcServerCore 和 IpcServer 對象都是在調(diào)用 builder.WebHost.UsePipeIpcServer(xxx);
被注入,如以下代碼
public static class WebHostBuilderExtensions { /// <summary> /// Enables the <see cref="IpcServer" /> service. 啟用命名管道IPC服務 /// </summary> /// <param name="builder">The <see cref="IWebHostBuilder"/>.</param> /// <param name="ipcPipeName">設置 Ipc 服務的管道名</param> /// <returns>The <see cref="IWebHostBuilder"/>.</returns> public static IWebHostBuilder UsePipeIpcServer(this IWebHostBuilder builder, string ipcPipeName) { return builder.ConfigureServices(services => { // 忽略代碼 services.AddSingleton<IServer, IpcServer>(); services.AddSingleton<IpcPipeMvcServerCore>(s => new IpcPipeMvcServerCore(s, ipcPipeName)); }); } }
依靠 ASP.NET Core 的機制,將會在主機啟動,調(diào)用 IServer 的 StartAsync 方法。通過 IpcServer 的 StartAsync 方法啟動 IpcPipeMvcServerCore 的邏輯
在 IpcPipeMvcServerCore 里,將初始化 IpcProvider 服務。這里的 IpcProvider 服務是 dotnetCampus.Ipc 提供的服務對外的接口,通過 IpcProvider 可以和 dotnetCampus.Ipc 層的其他 Peer 進行通訊。剛好在客戶端也相同的初始化 IpcProvider 服務,通過 ipcPipeName
管道名可以將客戶端和服務端關聯(lián)
class IpcPipeMvcServerCore { public IpcPipeMvcServerCore(IServiceProvider serviceProvider, string? ipcServerName) { ipcServerName ??= "IpcPipeMvcServer" + Guid.NewGuid().ToString("N"); IpcServer = new IpcProvider(ipcServerName, new IpcConfiguration() { DefaultIpcRequestHandler = new DelegateIpcRequestHandler(async context => { // 核心代碼 }) }); } public void Start() => IpcServer.StartServer(); public IpcProvider IpcServer { set; get; } }
在 dotnetCampus.Ipc 層提供了請求響應框架,可以通過傳入 DefaultIpcRequestHandler 對象用來接收其他端發(fā)送過來的請求,處理完成之后返回給對方。上面代碼的核心就是 DelegateIpcRequestHandler 的處理邏輯,在 context 里讀取客戶端的請求信息,反序列化為 HttpRequestMessage 對象,通過內(nèi)部邏輯進入到 ASP.NET Core 層,再通過 MVC 框架之后拿到請求的返回值,將返回值封裝為 IpcResponseMessageResult 返回給客戶端
IpcServer = new IpcProvider(ipcServerName, new IpcConfiguration() { DefaultIpcRequestHandler = new DelegateIpcRequestHandler(async context => { // 將請求反序列化為 HttpRequestMessage 對象 // 用于傳入到 ASP.NET Core 層 System.Net.Http.HttpRequestMessage? requestMessage = HttpMessageSerializer.DeserializeToRequest(context.IpcBufferMessage.Body); // 創(chuàng)建虛擬的請求,進入到 ASP.NET Core 框架里 var server = (IpcServer) serviceProvider.GetRequiredService<IServer>(); var clientHandler = (ClientHandler) server.CreateHandler(); var response = await clientHandler.SendInnerAsync(requestMessage, CancellationToken.None); // 拿到的返回值序列化為 IpcResponseMessageResult 放入 dotnetCampus.Ipc 層用來返回客戶端 var responseByteList = HttpMessageSerializer.Serialize(response); return new IpcResponseMessageResult(new IpcMessage($"[Response][{requestMessage.Method}] {requestMessage.RequestUri}", responseByteList)); }) });
創(chuàng)建虛擬的請求,進入 ASP.NET Core 框架里的邏輯是服務端最復雜的部分。在 IpcServer 的 CreateHandler 方法里面,將創(chuàng)建 ClientHandler 對象。此 ClientHandler 對象是用來構建虛擬的請求,相當于在當前進程內(nèi)發(fā)起請求而不是通過網(wǎng)絡層發(fā)起請求,代碼如下
public class IpcServer : IServer { /// <summary> /// Creates a custom <see cref="HttpMessageHandler" /> for processing HTTP requests/responses with the test server. /// </summary> public HttpMessageHandler CreateHandler() { // 忽略代碼 return new ClientHandler(BaseAddress, Application) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext }; } }
在也是繼承 HttpMessageHandler 的 ClientHandler 里,也重寫了 SendInnerAsync 方法,此方法將會負責創(chuàng)建 HttpContextBuilder 對象,由 HttpContextBuilder 執(zhí)行具體的調(diào)用 ASP.NET Core 層的邏輯
public async Task<HttpResponseMessage> SendInnerAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 創(chuàng)建 HttpContextBuilder 對象 var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext); var requestContent = request.Content; if (requestContent != null) { // 以下是對 HttpContextBuilder 的初始化邏輯 // Read content from the request HttpContent into a pipe in a background task. This will allow the request // delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios. contextBuilder.SendRequestStream(async writer => { // 忽略初始化邏輯 }); } contextBuilder.Configure((context, reader) => // 忽略初始化邏輯 }); // 忽略其他代碼 // 執(zhí)行實際的調(diào)用 ASP.NET Core 框架邏輯 var httpContext = await contextBuilder.SendAsync(cancellationToken); // 創(chuàng)建 HttpResponseMessage 對象用于返回 var response = new HttpResponseMessage(); // 以下是對 HttpResponseMessage 的初始化邏輯,從 httpContext 里獲取返回值 response.StatusCode = (HttpStatusCode) httpContext.Response.StatusCode; response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>()!.ReasonPhrase; response.RequestMessage = request; response.Version = request.Version; response.Content = new StreamContent(httpContext.Response.Body); return response; }
在 HttpContextBuilder 里,將在 SendAsync 邏輯里調(diào)用 ApplicationWrapper 的 ProcessRequestAsync 方法從而調(diào)入 ASP.NET Core 框架內(nèi)。這里的 ApplicationWrapper 是對 Microsoft.AspNetCore.Hosting.HostingApplication
的封裝,因為此 HostingApplication 類型是不對外公開的。以上這幾個類型的定義邏輯,都是現(xiàn)有的 https://github.com/dotnet/aspnetcore 開源倉庫的代碼
通過當前進程發(fā)起請求而不通過網(wǎng)絡層的邏輯,其實在 ASP.NET Core 開源倉庫里面有默認的一個實現(xiàn)的提供。那就是為了單元測試編寫的 TestHost 機制
在 TestHost 機制里,開發(fā)者可以在單元測試里面開啟 ASP.NET Core 主機,但是不需要監(jiān)聽任何網(wǎng)絡的端口,所有對此主機的測試完全通過 TestHost 機制走進程內(nèi)的模擬請求發(fā)起。對于業(yè)務代碼來說,大多數(shù)時候不需要關注請求的發(fā)起方具體是誰,因此單元測試上可以使用 TestHost 方便進行測試業(yè)務代碼,或者是在集成測試上測試調(diào)用邏輯。使用 TestHost 可以讓單元測試或集成測試不需要關注網(wǎng)絡的監(jiān)聽,防止測試錯服務,方便在 CI 里加入測試邏輯
剛好此機制的代碼也是本庫所需要的,通過拷貝了 https://github.com/dotnet/aspnetcore 開源倉庫的關于 TestHost 的機制代碼,即可用來實現(xiàn) IpcServer 的邏輯
也如放在 IpcServer 的 CreateHandler 函數(shù)上的代碼注釋,這就是原本的 TestHost 里對應函數(shù)的代碼
相當于在 TestHost 機制上再加上一層,這一層就是基于 dotnetCampus.Ipc 層做通訊,通過 TestHost 層創(chuàng)建虛擬的請求,進入 ASP.NET Core 框架
為了方便開發(fā)者接入,也為了防止開發(fā)者接入了 dotnetCampus.Ipc 層的 IpcNamedPipeStreamMvcServer 之后,再接入 TestHost 進行單元測試的沖突,本倉庫更改了所有從 https://github.com/dotnet/aspnetcore 開源倉庫的關于 TestHost 的機制代碼的命名空間,對入口調(diào)用函數(shù)和類型也進行重命名。在每個拷貝的文件上都加上了 // Copy From: https://github.com/dotnet/aspnetcore
的注釋
代碼
本文所有代碼都放在 https://github.com/dotnet-campus/dotnetCampus.Ipc 開源倉庫里,歡迎訪問
參考文檔
HttpRequestMessage C# (CSharp)代碼示例 - HotExamples
c# - How to send a Post body in the HttpClient request in Windows Phone 8? - Stack Overflow
HttpRequestOptions Class (System.Net.Http)
c# - Serialize and deserialize HttpRequestMessage objects - Stack Overflow
Byte Rot: Serialising request and response in ASP.NET Web API
Efficient post calls with HttpClient and JSON.NET
c# - NamedPipe with ASP.Net - Stack Overflow
wcf - Using "named pipes" in ASP.NET HttpModule - Stack Overflow
到此這篇關于dotnet 替換 ASP.NET Core 的底層通訊為命名管道的 IPC 庫的文章就介紹到這了,更多相關ASP.NET Core IPC 庫命名管道內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
asp.net后臺如何動態(tài)添加JS文件和css文件的引用
動態(tài)添加JS文件和css文件的引用在asp.net后臺如何實現(xiàn)呢?首先添加命名空間 using System.Web.UI.HtmlControls,之后按照下面的步驟操作即可2014-09-093分鐘快速學會在ASP.NET Core MVC中如何使用Cookie
這篇文章主要給大家介紹了關于如何通過3分鐘快速學會在ASP.NET Core MVC中使用Cookie的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用ASP.NET具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-12-12Asp.net使用HttpModule壓縮并刪除空白Html請求的實現(xiàn)代碼
當我們壓縮我的Response后再傳到Client端時,可以明顯節(jié)省寬帶. 提升Site的性能. 現(xiàn)在的瀏覽器大部分都支持Gzip,Deflate壓縮2011-11-11asp.net下Response.ContentType類型匯總
asp.net下Response.ContentType類型匯總...2007-04-04.Net Core導入千萬級數(shù)據(jù)至Mysql數(shù)據(jù)庫的實現(xiàn)方法
今天我們談談MySQL怎么高性能插入千萬級的數(shù)據(jù)的,討論這個問題牽扯到一個數(shù)據(jù)遷移功能,高性能的插入數(shù)據(jù),接下來通過本文給大家分享幾種實現(xiàn)方法,感興趣的朋友跟隨小編一起學習下吧2021-05-05asp.net Http異常eurl.axd出錯信息解決方法
在IIS6中同時啟用了ASP.NET 2.0 和 ASP.NET 4.0 后,網(wǎng)站程序可能會出現(xiàn)如下錯誤:“ System.Web.HttpException: Path ‘//eurl.axd/‘ was not found. ”2011-08-08asp.net錯誤處理Application_Error事件示例
Application_Error事件與Page_Error事件相類似,可使用他捕獲發(fā)生在應用程序中的錯誤。由于事件發(fā)生在整個應用程序范圍內(nèi),因此您可記錄應用程序的錯誤信息或處理其他可能發(fā)生的應用程序級別的錯誤2014-01-01