介绍
中继服务器
中继服务器在发布客户端和订阅客户端之间传递消息。服务器不会更改消息本身——仅从发布客户端传递到一组订阅客户端。
简单中继gRPC示例中介绍了简单中继服务器(和客户端)。这篇文章是gRPC复习,只有学习价值,所以,那篇文章中描述的服务器太简单了,无法在实际应用中使用——所有已发布的消息都发送到每个订阅的客户端——没有分离,例如,按主题。
此处描述的中继服务器允许按主题分隔消息——只有订阅主题的客户端才会获得发布到该主题的消息。此服务器可用于相同或不同计算机上不同进程之间的通信。
关注点分离
分离关注点意味着将您的产品/项目拆分为一组几乎独立的部分,每个部分都可以几乎独立于其余部分进行开发、调试、维护和扩展,因此修复或扩展一个部分不会导致产品其余部分出现问题或更改。
项目架构师最重要的任务之一是找出实现最佳关注点分离的方法,以便各个开发人员不会经常互相踩到脚趾。
我经常写关于使用IoC和插件架构来分离关注点的文章。插件架构背后的主要思想是各个插件彼此独立,尽管它们可能依赖于包含共享接口和功能的常见项目。
本文继续讨论关注点分离,这次应用于gRPC中继服务器和客户端。
我们将展示一个gRPC中继服务器,该服务器可以轻松扩展以传递新的消息类型(独立于其他消息类型),而无需对服务器或现有消息进行任何修改。
代码位置
示例代码位于NP.Samples存储库的GrpcRelayServer文件夹下。解决方案NP.Grpc.RelayServerRunner.sln包含中继客户端和服务器,可以在GrpcRelayServer/NP下找到。Grpc.RelayServerRunner 文件夹位于同一存储库中。
代码语言
此时,服务器和客户端都是用C#编写的。我计划稍后添加Python和JavaScript客户端的示例。
中继客户端/服务器代码示例
代码概述
打开解决方案并查看解决方案资源管理器:
解决方案中有5个控制台项目——一个用于运行中继服务器,4个用于运行客户端——2个发布客户端和2个订阅客户端。
运行服务器的项目是NP.Grpc.RelayServerRunner。它仅依赖于解决方案中的一个库项目——NP.Grpc.RelayServerConfig,该库项目仅包含一个GrpcServerConfig类,其目的是在客户端和服务器之间共享服务器端口和主机:
public class GrpcServerConfig : IGrpcConfig
{
public string ServerName => "localhost";
public int Port => 5555;
}
如上所述,RelayServer允许添加各种主题,以便客户端可以发布和订阅它们。
我们的示例有两个主题——Person和Org(组织)。
Person相关项目位于“Topics/Person”解决方案文件夹下,而Org相关项目位于“Topics/Organization”下。
Person相关项目的安排和依赖关系与组织相关项目的安排和依赖关系完全相同,因此,我们只讨论Person相关项目,但请记住,组织主题的所有内容也相同。
Person有两个控制台项目:
SubscribePersonClient——从Topic.PersonTopic主题枚举值接收Person类型的所有已发布对象。
- PublishPersonClient——将Person类型的对象发布到Topic.PersonTopic主题枚举值。
这两个Person客户端项目都依赖于:
- 在 Person.proto protobuf 文件中定义两者PersonType和Topic枚举的PersonData项目。请注意,Person.proto 作为链接存在于PersonData项目中。文件本身定义为PersonProtos项目中的内容文件。这样做是为了我们可以创建不同语言(JavaScript或Python)的客户端项目,这些项目将从PersonProtos项目中读取 Person.proto 文件并构建自己的存根,这与PersonData项目在C#中的方式不同。
- NP.Grpc.ClientBuilder包含有关连接到服务器的信息的项目。
- NP.Grpc.ClientBuilder项目,它吸收了一些用于生成客户端的功能,这些功能可以在解决方案中的每个客户端中重复使用。
下面是项目依赖关系图:
请注意,为了清楚起见,我们跳过了Org相关项目,但它们与Person相关项目具有完全相同的依赖项。
关于关注点的分离,Org和Person客户端项目完全不相互依赖,在添加另一个Topic及其项目时不需要修改服务器。在讨论代码时,我们将更多地讨论它。
运行服务器和客户端
解决方案中有五个控制台项目——一个服务器项目、两个订阅客户端(每个主题一个)和两个发布客户端(每个主题一个):
若要运行项目,只需在“解决方案资源管理器”中右键单击该项目,然后选择“调试->启动(不调试)”菜单选项。
首先启动服务器项目——NP.Grpc.RelayServerRunner。
然后启动两个订阅客户端项目:SubscribePersonClient和SubscribeOrgClient。将启动的控制台窗口拉到屏幕的不同角落,以便您可以轻松查看哪个是哪个。
现在,您可以按您选择的任何顺序重复启动和重新启动两个发布项目PublishPersonClient和PublishOrgClient。Published人员信息(string“Joe Doe”)将在SubscribePersonClient控制台上逐行打印。已发布的Org信息(string “Google, Inc”)将在SubscribeOrgClient控制台上逐行打印。
现在开始代码!
RelayServerRunner代码
服务器和客户端代码非常简单——大部分复杂性被引用的项目吸收(一些最重要的项目仅作为插件引用,如下所述,如创建和安装插件作为Nuget包中所述)。
以下是启动服务器的代码:
using NP.Grpc.CommonRelayInterfaces;
using NP.Grpc.RelayServerConfig;
using NP.IoCy;
// create container builder with Enum keys
var containerBuilder = new ContainerBuilder<Enum>();
// Register IGrpcConfig type to be resolved to GrpcServerConfig objectp
containerBuilder.RegisterSingletonType<IGrpcConfig, GrpcServerConfig>();
// Dynamically load and inject all the plugins from the subfolders of
// Plugins/Services folder under TargetFolder of the project
// TargetFolder is where the executable of the project is located
// e.g. folder bin/Debug/net6.0 under the projects directory.
containerBuilder.RegisterPluginsFromSubFolders("Plugins/Services");
// build the IoC container from container builder
var container = containerBuilder.Build();
// get the reference to the relay server from the plugin
// The server will start running the moment it is created.
IRelayServer relayServer = container.Resolve<IRelayServer>();
// prevent the program from exiting
Console.ReadLine();
IRelayServer类型在服务器和客户端项目引用的NP.Grpc.CommonRelayInterfaces NuGet包中定义。
请注意,主插件来自NP.Grpc.RelayServer nuget包,并被NP.Grpc.RelayServerRunner.csproj 代码复制到Plugin/Services目录,如创建和安装插件作为Nuget包一文中所述。
这是NP.Grpc.RelayServerRunner.csproj代码可以做到这一点:
<ItemGroup>
...
<PackageReference Include="NP.Grpc.RelayServer" Version="1.0.7"
GeneratePathProperty="true">
<!-- Do not reference the assets of the NP.Grpc.RelayServer package
(since we are using it as a plugin instead -->
<ExcludeAssets>All</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<RelayServerFiles Include="$(PkgNP_Grpc_RelayServer)\lib\net6.0\**\*.*" />
</ItemGroup>
<Target Name="CopyServerPluginFromNugetPackage" AfterTargets="Build">
<PropertyGroup>
<!-- Set the output folder for the relay server plugin -->
<ServerPluginFolder>$(TargetDir)\Plugins\Services\NP.Grpc.RelayServer
</ServerPluginFolder>
</PropertyGroup>
<!-- remove the old plugin directory -->
<RemoveDir Directories="$(ServerPluginFolder)" />
<!-- Copy the contents of NP.Grpc.RelayServer.nupkg to the
$(TargetDir)\Plugins\Services\NP.Grpc.RelayServer plugin folder -->
<Copy SourceFiles="@(RelayServerFiles)"
DestinationFolder="$(ServerPluginFolder)\%(RecursiveDir)" />
</Target>
Plugins/Services目录下还有两个插件文件夹——OrgData 和 PersonData——它们由相同命名项目的后期生成事件复制:
这两个插件,OrgData和PersonData需要在那里,以便容器可以将数组或允许的主题注入服务器,该服务器(在我们的示例中)由来自两个不同枚举的两个主题组成:{NP.OrgClient.Topic.OrgTopic, NP.PersonClient.Topic.PersonTopic}。请注意,其中一个主题来自NP.OrgClient.Topic枚举,另一个主题来自NP.PersonClient.Topic枚举,这两个枚举在两个不同的项目中相应地定义——一个在OrgData上,另一个在 PersonData中定义。
将它们组合成单个可注入的Enum值集合是通过所描述的NP.IoCy框架的多单元格功能实现的,例如在具有多单元格的多个插件中。
当我们谈论客户端项目的代码时,我们将更多地讨论将两个值(每个插件一个)绑定到一个多单元格中。
生成的集合指定哪些主题可以发送到服务器。尝试发送另一个主题,不在允许的主题集合中将导致错误。
插件根据其IoC属性注入到NP.Grpc.RelayServer对象中。
还有一个对象需要注入到服务器中—— 一个IGrpcConfig的实现,在我们的例子中,它来自依赖NP.GrpcRelayServerConfig项目中定义的GrpcServerConfig类型。
下面是此类型向IoC容器注册的方式:
// Register IGrpcConfig type to be resolved to GrpcServerConfig objectp
containerBuilder.RegisterSingletonType<IGrpcConfig, GrpcServerConfig>();
这是GrpcServerConfig类的非常简单的实现:
public class GrpcServerConfig : IGrpcConfig
{
public string ServerName => "localhost";
public int Port => 5555;
}
我们只需将服务器名称分配给"localhost"和端口5555。
请注意,插件架构很容易让我们将简单的IGrpcConfig实现换成更复杂的实现,例如,根据配置文件值分配服务器和端口名称的实现。
PersonData和PersonProto项目
如上所述,PersonProtos项目将Person.proto文件定义为内容文件,而PersonData项目创建包含指向被视为protobuf文件的 Person.proto 文件的链接:
<ItemGroup>
<Protobuf Include="..\PersonProtos\Person.proto"
Link="Person.proto" GrpcServices="Client" ProtoRoot=".."/>
</ItemGroup>
上面是 PersonData.csproj 项目文件中强制自动生成C#客户端存根的行。
定义 Person.proto 文件的PersonProto项目和链接到该文件的PersonData项目之间的这种划分是必要的,以防我们想使用C#以外的其他语言。稍后(在以后的文章中),将展示如何创建Python和JavaScript客户端——因为不同的项目也引用 Person.proto 作为链接。
以下是Person.proto文件的内容:
syntax = "proto3";
package NP.PersonClient;
enum Topic
{
None = 0;
PersonTopic = 10;
}
message Person
{
string Name = 1;
int32 Age = 2;
}
它将Person类型定义为具有两个属性——string Name和Int32 Age。它还使用一个非平凡值PersonTopic = 10;定义主题枚举。请注意,相应PersonTopic C# enum的整数值将10为。
另请注意,由于我们要区分Org和Person主题,因此OrgTopic的整数值20与 OrgProtos/Org.proto 文件中一样:
enum Topic
{
None = 0;
OrgTopic = 20;
}
PersonData项目中的另一个重要文件是 TopicsGetter.cs。它定义了一个返回PersonTopic enum值作为MultiCell Topics集合一部分的方法:
[HasRegisterMethods]
public static class TopicsGetter
{
/// Returns the PersonTopic value as part of the MultiCell Topics collection
[RegisterMultiCellMethod(cellType: typeof(Enum), resolutionKey: IoCKeys.Topics)]
public static Enum GetTopics() { return NP.PersonClient.Topic.PersonTopic; }
}
NP.IoCy容器,基于该RegisterMultiCellMethod属性创建一个集合,其中包含来自不同主题中的枚举的Enum值,以便服务器将拥有所有允许的主题的列表。
PersonData项目有一个后期构建事件,该事件将其编译的内容复制到RelayServer的Plugins/Services/PersonData文件夹下,以便由服务器的IoC容器创建和填充Topics集合:
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<!-- copy to the server to register the topic -->
<Exec Command="xcopy "$(OutDir)"
"$(SolutionDir)\bin\$(Configuration)\net6.0\Plugins\Services\$(ProjectName)\"
/S /R /Y /I" />
</Target>
SubcribePersonClient项目
SubcribePersonClient创建一个订阅客户端,侦听Topic.PersonTopic等待Person类型的对象到达。每次到达后,它都会将Person.Name属性值打印到控制台。
// create relay client
IRelayClient relayClient = ClientBuilder.GetClient();
// observe Topic PersonTopic and define the action on arrived Person object
// by calling subscribe
IDisposable disposable =
relayClient
.ObserveTopicStream<Person>(Topic.PersonTopic)
.Subscribe(OnPersonDataArrived);
void OnPersonDataArrived(Person person)
{
// print Person.Name for every new person
// coming from the server
Console.WriteLine(person.Name);
}
// prevent from exiting
Console.ReadLine();
请注意,用于创建四个客户端中的任何一个的通用代码位于共享NP.Grpc.ClientBuilder项目的ClientBuilder类中:
public static class ClientBuilder
{
private static IRelayClient? _relayClient;
public static IRelayClient GetClient()
{
if (_relayClient == null)
{
// create container builder with keys limited to Enum (enumeration values)
var containerBuilder = new ContainerBuilder<System.Enum>();
// Register GrpcServerConfig containing server Name as "localhost"
// and server port - 5555 to be retuned by the container
// for the IGrpcConfig type.
containerBuilder.RegisterType<IGrpcConfig, GrpcServerConfig>();
// register multicell of cell type Enum and resolution key IoCKeys.Topics
containerBuilder.RegisterMultiCell(typeof(System.Enum), IoCKeys.Topics);
// get the plugins from Plugins/Services folder under
// the folder containing client executable
containerBuilder.RegisterPluginsFromSubFolders("Plugins/Services");
var container = containerBuilder.Build();
// create the relay client
_relayClient = container.Resolve<IRelayClient>();
}
// return relay client
return _relayClient;
}
}
SubscribePersonClient(与解决方案中的所有其他客户端一样)使用NP.Grpc.RelayClient NuGet包作为插件(如创建和安装插件作为Nuget包中所述)。
下面是NP.Grpc.SubscribePersonClient.csproj代码,它将NP.Grpc.RelayClient nuget包的内容复制到NP.Grpc.SubscribePersonClient可执行目录下的Plugin/Services文件夹中:
<ItemGroup>
...
<!-- GeneratePathProperty set to true,
generates PkgNP_Grpc_RelayClient as the root folder
for the package contents -->
<PackageReference Include="NP.Grpc.RelayClient"
Version="1.0.6" GeneratePathProperty="true">
<ExcludeAssets>All</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<RelayClientFiles Include="$(PkgNP_Grpc_RelayClient)\lib\net6.0\**\*.*" />
</ItemGroup>
<Target Name="CopyClientPluginFromNugetPackage" AfterTargets="Build">
<PropertyGroup>
<!-- path for client plugin folder -->
<ClientPluginFolder>$(TargetDir)\Plugins\Services\NP.Grpc.RelayClient
</ClientPluginFolder>
</PropertyGroup>
<!-- remove the old folder with plugin folder (if exists) -->
<RemoveDir Directories="$(ClientPluginFolder)" />
<!-- copy the the contents of the nuget package into the client plugin folder -->
<Copy SourceFiles="@(RelayClientFiles)"
DestinationFolder="$(ClientPluginFolder)\%(RecursiveDir)" />
</Target>
PublishPersonClient项目
PublishPersonClient项目创建一个名为“Joe Doe”的Person对象并将其发布到主题PersonTopic中。这是其program.cs文件的非常简单的内容:
/ get the client from ClientBuilder
IRelayClient relayClient = ClientBuilder.GetClient();
// create person 30 years old, named Joe Doe
Person person = new Person { Age = 30, Name = "Joe Doe"};
// publish the person to Topic.PersonTopic
await relayClient.Publish(Topic.PersonTopic, person);
与SubscribePersonClient项目相同,它依赖于NP.Grpc.ClientBuilder并使用NP.Grpc.RelayClient nuget包作为插件。
组织主题项目
“组织”文件夹下的项目与“人员”文件夹下的项目几乎相同,只是它们处理Org对象:
message Org
{
string Name = 1;
int32 NumberPeople = 2;
}
同样如上所述,Topic.OrgTopic enum值具有不同的值20,而不是Topic.PersonTopic的10:
enum Topic
{
None = 0;
OrgTopic = 20;
}
请注意,Org和Person项目不相互依赖,服务器也不依赖它们。因此,只要我们确保主题枚举具有不同的整数值和名称,我们就可以根据需要制作任意数量的独立客户端项目,而无需对其他客户端项目或服务器进行任何修改。
结论
本文给出了中继服务器的用法示例,该服务器允许将方法发布到不同的主题并订阅它们。中继服务器不会将通道上的消息更改为相应的主题。
本文还演示了如何扩展主题和消息,而它们之间没有任何依赖关系,并且不同主题之间的关注点完全分离。
https://www.codeproject.com/Articles/5353258/Publish-Subscribe-gRPC-Relay-Server-with-Separatio