您现在的位置是:网站首页> 编程资料编程资料

关于dotnet 替换 ASP.NET Core 的底层通讯为命名管道的 IPC 库的问题_实用技巧_

2023-05-24 343人已围观

简介 关于dotnet 替换 ASP.NET Core 的底层通讯为命名管道的 IPC 库的问题_实用技巧_

这是一个用于本机多进程进行 IPC 通讯的库,此库的顶层 API 是采用 ASP.NET Core 的 MVC 框架,其底层通讯不是传统的走网络的方式,而是通过 dotnetCampus.Ipc 开源项目提供的基于 NamedPipeStream 命名管道的方式进行通讯。相当于替换掉 ASP.NET Core 的底层通讯方式,从走网络换成命名管道的方式。本库的优势是可以使用设计非常好的 ASP.NET Core 的 MVC 框架作为顶层调用 API 层,底层通讯采用可提升传输性能的命名管道,如此可以做到不走网络通讯从而极大减少网络端口占用问题和减少用户端网络环境带来的问题

这是一个用于本机多进程进行 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 万台设备近半年的测试,发现通过此方式的通讯稳定性极高。开源仓库地址:https://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 logger) { Logger = logger; } public ILogger Logger { get; } }

在 FooController 添加 Get 方法,代码如下

[HttpGet] public IActionResult Get() { Logger.LogInformation("FooController_Get"); return Ok(DateTime.Now.ToString()); }

根据 ASP.NET Core 的路由知识,可以在客户端通过 api/Foo 路径访问到以上的 Get 方法。接下来编写客户端的逻辑,先在客户端上的 XAML 界面上添加按钮,代码如下

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 ?? ""}"); return Ok($"PostFooContent Foo1={foo.Foo1};Foo2={foo.Foo2 ?? ""}"); }

以上代码采用 FooContent 作为参数,类型定义如下

public class FooContent { public string? Foo1 { set; get; } public string? Foo2 { set; get; } }

-六神源码网