dotnet 替換 ASP.NET Core 的底層通訊為命名管道的 IPC 庫
- 2022 年 2 月 9 日
- 筆記
- dotnet core, WPF
這是一個用於本機多進程進行 IPC 通訊的庫,此庫的頂層 API 是採用 ASP.NET Core 的 MVC 框架,其底層通訊不是傳統的走網路的方式,而是通過 dotnetCampus.Ipc 開源項目提供的基於 NamedPipeStream 命名管道的方式進行通訊。相當於替換掉 ASP.NET Core 的底層通訊方式,從走網路換成命名管道的方式。本庫的優勢是可以使用設計非常好的 ASP.NET Core 的 MVC 框架作為頂層調用 API 層,底層通訊採用可提升傳輸性能的命名管道,如此可以做到不走網路通訊從而極大減少網路埠佔用問題和減少用戶端網路環境帶來的問題
背景
本機內多進程通訊 IPC 不同於跨設備系統的 RPC 通訊方式,大多數的 IPC 通訊都需要處理複雜的用戶端環境問題。對於 RPC 通訊來說,大部分時候,服務端都在開發者完全管控的環境下運行。但 IPC 通訊則無論是服務端還是客戶端都可能是在用戶端運行的。然而用戶端上,無論是系統還是其他環境都是十分複雜的,特別是在中國的,魔改的系統,兇狠的殺毒軟體,這些都會讓 IPC 通訊受到非預期的打斷
傳統的 dotnet 系的 IPC 手段有很多個,提供給開發使用的頂層框架也有很多,如 .NET Remoting 和 WCF 等。但是在遷移到 dotnet core 時,由於底層運行時機制的變更,如透明代理不再支援類對象只能支援介面的行為變更,就讓 .NET Remoting 從機制性不受支援。為了方便將應用遷移到 dotnet core 框架上,可採用 dotnet campus 組織基於最友好的 MIT 協議開源的 dotnetCampus.Ipc 開源庫進行本機內多進程通訊
此 dotnetCampus.Ipc 開源庫底層可基於命名管道進行通訊,經過了約 600 萬台設備近半年的測試,發現通過此方式的通訊穩定性極高。開源倉庫地址://github.com/dotnet-campus/dotnetCampus.Ipc
無論是 RPC 還是 IPC 通訊,其頂層提供給開發者使用的 API 層,主流上有兩個設計陣營。一個是如 .NET Remoting 一樣的傳輸類對象的方式,此方法可以極大隱藏 RPC 或 IPC 的細節,調用遠程進程的對象就和調用本機進程一樣。另一個陣營是本文的主角,如 ASP.NET Core 的 MVC 模式,通過路由配合參數傳遞,進行控制器處理的模式,此方式的優良設計已被 ASP.NET Core 所證明,本文也就不多說了
默認下,如此妙的 ASP.NET Core 的 MVC 層框架是僅提供網路傳輸的方式。然而在詭異的用戶端環境下,將有層出不窮的網路通訊問題,如埠被佔用,特殊的軟體阻止上網等等。讓 ASP.NET Core 從走網路的方式,替換為走命名管道的方式,可以極大提升在用戶端的穩定性
再次表揚 ASP.NET Core 的優秀設計,在 ASP.NET Core 里,各個模組分層明確,這也就讓更換 ASP.NET Core 里的「通訊傳輸」(其實本意是 IServer 層)這個工作十分簡單
在採用 ASP.NET Core 作為 IPC 的頂層調用時,那此時的通訊方式一定就是 服務端-客戶端 的形式。服務端可以採用替換 ASP.NET Core 的「通訊傳輸」為 dotnetCampus.Ipc 的基於命名管道的傳輸方式。客戶端呢?對 ASP.NET Core 來說,最期望客戶端的行為是通過 HttpClient 來進行發起調用。剛好 dotnet 下默認的 HttpClient 是支援注入具體的消息傳輸實現,通過將 dotnetCampus.Ipc 封裝為 HttpClient 的消息傳輸 HttpMessageHandler 就可以讓客戶端也走 dotnetCampus.Ipc 的傳輸。如此封裝,相當於在 服務端和客戶端 的底層傳輸,全部都在 dotnetCampus.Ipc 層內,分層圖如下,通過 dotnetCampus.Ipc 維持穩定的傳輸從而隱藏具體的 IPC 細節,業務端可以完全復用原有的知識,無須引入額外的 IPC 知識
充當 IPC 里的服務端和客戶端的業務程式碼將分別與 ASP.NET Core 和 HttpClient 對接。而 ASP.NET Core 和 HttpClient 又與 dotnetCampus.Ipc 層對接,一切的跨進程通訊邏輯都在 dotnetCampus.Ipc 這一層內完成,由 dotnetCampus.Ipc 層維持穩定的 IPC 傳輸。下面來看看如何使用此方式開發應用
使用方法
接下來將使用 PipeMvcServerDemo 和 PipeMvcClientDemo 這兩個例子項目來演示如何使用 ASP.NET Core 的 MVC 層框架加命名管道 NamedPipeStream 做通訊傳輸的本機內多進程的跨進程通訊 IPC 方式
按照慣例,在 dotnet 系的應用上使用庫之前,先通過 NuGet 進行安裝。從業務上人為分為服務端和業務端的兩個項目,分別安裝給服務端用的 dotnetCampus.Ipc.PipeMvcServer 庫,和給客戶端用的 dotnetCampus.Ipc.PipeMvcClient 庫
新建的 PipeMvcServerDemo 和 PipeMvcClientDemo 這兩個基於 .NET 6 的例子項目都是先基於 WPF 的項目模板創建,從業務上人為分為服務端和業務端的兩個項目其實都是運行在相同的一個電腦內,只是為了方便敘述,強行將 PipeMvcServerDemo 稱為服務端項目,將 PipeMvcClientDemo 稱為客戶端項目
服務端
先從 PipeMvcServerDemo 服務端項目開始寫起,在安裝完成 dotnetCampus.Ipc.PipeMvcServer 庫之後,為了使用上 ASP.NET Core 的 MVC 框架,需要在此 WPF 應用裡面初始化 ASP.NET Core 框架
初始化的邏輯,和純放在伺服器上的 ASP.NET Core 服務應用只有一點點的差別,那就是在初始化時,需要調用 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();
}
調用 UsePipeIpcServer 擴展方法,需要額外加上 using dotnetCampus.Ipc.PipeMvcServer;
命名空間。在 UsePipeIpcServer 方法裡面需要傳入一個參數,此參數用於開啟的 IPC 服務所使用的服務名,也就是作為命名管道的管道名。服務名的字元串要求是在當前機器上唯一不重複,推薦採用屬性的命名法對其命名傳入。此後,客戶端的程式碼即可採用此服務名連接上服務端
也僅僅只需加上 UsePipeIpcServer 擴展方法即可完成對服務端的 IPC 的所有配置
客戶端
完成服務端的配置之後,可以開始對客戶端的配置邏輯,客戶端只需要知道服務端的服務名,即如上例子的 "PipeMvcServerDemo"
字元串,即可建立和服務端的通訊。在此庫的設計上,可以認為服務端的服務名和傳統的 C/S 端應用的服務端地址是等同的,至少需要知道服務端的地址才能連接上
在客戶端的任意程式碼里,可採用 IpcPipeMvcClientProvider 提供的 CreateIpcMvcClientAsync 靜態方法傳入服務名,拿到可以和服務端通訊的 HttpClient 對象,如以下程式碼
using dotnetCampus.Ipc.PipeMvcClient;
HttpClient ipcPipeMvcClient = await IpcPipeMvcClientProvider.CreateIpcMvcClientAsync("PipeMvcServerDemo");
以上程式碼拿到的 ipcPipeMvcClient
對象即可和傳統的邏輯一樣,進行服務端的請求邏輯,如下文所演示的例子。可以看到客戶端的配置邏輯,也只有在初始化時,獲取 HttpClient 的邏輯不同
如上面演示的程式碼,可以看到,無論是客戶端還是服務端,初始化的程式碼都是一句話,沒有很多的細節邏輯,方便入手
調用
下面開始演示服務端和客戶端調用的例子。為了讓客戶端能調用到客戶端對應的服務內容,需要先在服務端創建對應的服務邏輯。以下將演示 GET 和 POST 方法和對應的路由和參數調用方法
在服務端 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());
}
根據 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 方法,運行效果如下
如上圖可以看到,客戶端成功調用了服務端,從服務端拿到了返回值
接下來的例子是在 GET 請求帶上參數,如實現遠程調用計算服務功能,在客戶端發送兩個 int 數給服務端進行計算相加的值。服務端的程式碼如下
public class FooController : ControllerBase
{
[HttpGet("Add")]
public IActionResult Add(int a, int b)
{
Logger.LogInformation($"FooController_Add a={a};b={b}");
return Ok(a + b);
}
}
客戶端在 XAML 介面添加對應按鈕的程式碼省略,按鈕的事件里調用方法程式碼如下
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}");
}
運行效果如下
可以看到客戶端成功調用了服務端執行了計算,拿到了返回值
通過以上的例子可以看到,即使底層更換為 IPC 通訊,對於上層業務程式碼,調用服務端的邏輯,依然沒有引入任何新的 IPC 知識,都是對 HttpClient 的調用
接下來是 POST 調用的程式碼,服務端在 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 方法帶參數方式請求服務端,服務端處理客戶端請求過來的參數執行實際的業務邏輯,服務端的程式碼依然放在 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 作為參數,類型定義如下
public class FooContent
{
public string? Foo1 { set; get; }
public string? Foo2 { set; get; }
}
客戶端程式碼如下,為了給出更多細節,我將不使用 PostAsJsonAsync 方法,而是先創建 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 參數傳給服務端
以上就是 GET 和 POST 的例子,幾乎看不出來加上 IPC 前後對 ASP.NET Core 應用調用的差別,除了要求需要使用特定的 HttpClient 對象之外,其他的邏輯都相同。以上的例子項目,可以從本文末尾獲取
如關注此庫的實現原理,請繼續閱讀下文
原理
先從客戶端方向開始,在客戶端里使用的 HttpClient 是被注入了使用 IPC 底層框架通訊的 IpcNamedPipeClientHandler 對象,此 IpcNamedPipeClientHandler 對象是一個繼承 HttpMessageHandler 類型的對象
在 IpcNamedPipeClientHandler 重寫了 HttpMessageHandler 類型的 SendAsync 方法,可以讓所有使用 HttpClient 發送的請求,進入 IpcNamedPipeClientHandler 的邏輯。在此方法裡面,將序列化請求,將請求通過 dotnetCampus.Ipc 發送到服務端,再通過 dotnetCampus.Ipc 提供的消息請求機制,等待收到服務端對此請求的返回值。等收到服務端的返回值之後,封裝成為 HttpResponseMessage 返回值,讓此返回值接入到 HttpClient 的機制框架,從而實現調用 HttpClient 發送的請求是通過 dotnetCampus.Ipc 層傳輸而不是走網路。進入 dotnetCampus.Ipc 層是被設計為對等層,對客戶端來說,進入 dotnetCampus.Ipc 層具體是走到 ASP.NET Core 的 MVC 或者是其他框架都是不需要關注的。對客戶端來說,只需要知道進入 dotnetCampus.Ipc 層的請求,可以進行非同步等待請求,細節邏輯不需要關注
以下是 IpcNamedPipeClientHandler 的實現程式碼
class IpcNamedPipeClientHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 序列化請求消息,準備通過 IPC 層發送
var message = HttpMessageSerializer.Serialize(request);
// 創建 IPC 消息的 Tag 內容,此 Tag 內容僅用來調試和記錄日誌
var ipcMessageTag = request.RequestUri?.ToString() ?? request.Method.ToString();
// 在 dotnetCampus.Ipc 層,採用 P2P 模型,沒有具體的服務端和客戶端
// 但是 P2P 模型是可以模擬 C/S 模型的,只需要讓某個端(Peer)充當服務端,另外的端充當客戶端即可
// 在 dotnetCampus.Ipc 庫里,採用 PeerProxy 表示某個端
// 這裡的端表示的是 IPC 的某個端,大部分時候可以認為是一個進程
// 以下的 ServerProxy 就是充當服務端的一個端,將在此框架內被初始化創建
// 通過 PeerProxy 發送 IPC 請求,此時的 IPC 請求將會被 PipeMvcServer 處理
// 在 PipeMvcServer 裡面,將通過 ASP.NET Core MVC 框架層進行調度,分發到對應的控制器處理
// 控制器處理完成之後,將由 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 方法,不僅需要創建 HttpClient 對象,還需要先嘗試連接服務端。儘管從 HttpClient 的設計上,應該是發起請求時才去連接服務端,但因為這是 IPC 通訊,且為了解決 IPC 初始化邏輯的多進程資源競爭,當前版本採用在獲取 HttpClient 也就是發起具體請求之前,連接服務端
/// <summary>
/// 提供給客戶端調用 MVC 的 Ipc 服務的功能
/// </summary>
public static class IpcPipeMvcClientProvider
{
/// <summary>
/// 獲取訪問 Mvc 的 Ipc 服務的對象
/// </summary>
/// <param name="ipcPipeMvcServerName">對方 Ipc 服務名</param>
/// <param name="clientIpcProvider">可選,用來進行 Ipc 連接的本地服務。如不傳或是空,將創建新的 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 方式設計的,因此客戶端也需要創建自己的 IpcProvider 對象。客戶端可選傳入已有的 IpcProvider 對象進行復用,就如 HttpClient 復用邏輯一樣。但創建 IpcProvider 對象是很便宜的,不會佔用多少資源,是否復用在性能上沒有多少影響。但是支援傳入 IpcProvider 更多是可以方便開發者對 IpcProvider 進行的訂製邏輯,例如注入自己的數組池和日誌等
以上就是客戶端的邏輯。關於如何序列化請求消息等,這些就屬於細節了,無論採用什麼方法,只需要能將請求和響應與二進位 byte 數組進行序列化和反序列化即可。細節內容還請自行在本文末尾獲取源程式碼進行閱讀
服務端的邏輯相對複雜一些,在服務端的 dotnetCampus.Ipc 層收到客戶端的請求後,服務端將構建一個虛擬的訪問請求,此訪問請求將通過 繼承 IServer 介面的 IpcServer 對象,在 ASP.NET Core 框架內發起請求,通過 MVC 框架層處理之後將響應返回到 IpcServer 對象里交給 dotnetCampus.Ipc 層傳輸給客戶端
在 IpcServer 對象的啟動函數,也就是 StartAsync 函數裡面,將會同步初始化 IpcPipeMvcServerCore 對象。在 IpcPipeMvcServerCore 對象裡面將初始化創建 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 對象都是在調用 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 的機制,將會在主機啟動,調用 IServer 的 StartAsync 方法。通過 IpcServer 的 StartAsync 方法啟動 IpcPipeMvcServerCore 的邏輯
在 IpcPipeMvcServerCore 里,將初始化 IpcProvider 服務。這裡的 IpcProvider 服務是 dotnetCampus.Ipc 提供的服務對外的介面,通過 IpcProvider 可以和 dotnetCampus.Ipc 層的其他 Peer 進行通訊。剛好在客戶端也相同的初始化 IpcProvider 服務,通過 ipcPipeName
管道名可以將客戶端和服務端關聯
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 對象用來接收其他端發送過來的請求,處理完成之後返回給對方。上面程式碼的核心就是 DelegateIpcRequestHandler 的處理邏輯,在 context 里讀取客戶端的請求資訊,反序列化為 HttpRequestMessage 對象,通過內部邏輯進入到 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);
// 創建虛擬的請求,進入到 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));
})
});
創建虛擬的請求,進入 ASP.NET Core 框架里的邏輯是服務端最複雜的部分。在 IpcServer 的 CreateHandler 方法裡面,將創建 ClientHandler 對象。此 ClientHandler 對象是用來構建虛擬的請求,相當於在當前進程內發起請求而不是通過網路層發起請求,程式碼如下
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 方法,此方法將會負責創建 HttpContextBuilder 對象,由 HttpContextBuilder 執行具體的調用 ASP.NET Core 層的邏輯
public async Task<HttpResponseMessage> SendInnerAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 創建 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) =>
{
// 忽略初始化邏輯
});
// 忽略其他程式碼
// 執行實際的調用 ASP.NET Core 框架邏輯
var httpContext = await contextBuilder.SendAsync(cancellationToken);
// 創建 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 邏輯里調用 ApplicationWrapper 的 ProcessRequestAsync 方法從而調入 ASP.NET Core 框架內。這裡的 ApplicationWrapper 是對 Microsoft.AspNetCore.Hosting.HostingApplication
的封裝,因為此 HostingApplication 類型是不對外公開的。以上這幾個類型的定義邏輯,都是現有的 //github.com/dotnet/aspnetcore 開源倉庫的程式碼
通過當前進程發起請求而不通過網路層的邏輯,其實在 ASP.NET Core 開源倉庫裡面有默認的一個實現的提供。那就是為了單元測試編寫的 TestHost 機制
在 TestHost 機制里,開發者可以在單元測試裡面開啟 ASP.NET Core 主機,但是不需要監聽任何網路的埠,所有對此主機的測試完全通過 TestHost 機制走進程內的模擬請求發起。對於業務程式碼來說,大多數時候不需要關注請求的發起方具體是誰,因此單元測試上可以使用 TestHost 方便進行測試業務程式碼,或者是在集成測試上測試調用邏輯。使用 TestHost 可以讓單元測試或集成測試不需要關注網路的監聽,防止測試錯服務,方便在 CI 里加入測試邏輯
剛好此機制的程式碼也是本庫所需要的,通過拷貝了 //github.com/dotnet/aspnetcore 開源倉庫的關於 TestHost 的機製程式碼,即可用來實現 IpcServer 的邏輯
也如放在 IpcServer 的 CreateHandler 函數上的程式碼注釋,這就是原本的 TestHost 里對應函數的程式碼
相當於在 TestHost 機制上再加上一層,這一層就是基於 dotnetCampus.Ipc 層做通訊,通過 TestHost 層創建虛擬的請求,進入 ASP.NET Core 框架
為了方便開發者接入,也為了防止開發者接入了 dotnetCampus.Ipc 層的 IpcNamedPipeStreamMvcServer 之後,再接入 TestHost 進行單元測試的衝突,本倉庫更改了所有從 //github.com/dotnet/aspnetcore 開源倉庫的關於 TestHost 的機製程式碼的命名空間,對入口調用函數和類型也進行重命名。在每個拷貝的文件上都加上了 // Copy From: //github.com/dotnet/aspnetcore
的注釋
程式碼
本文所有程式碼都放在 //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