NServiceBus入门:多个endpoint(Introduction to NServiceBus: Multiple endpoints)
原文地址:https://docs.particular.net/tutorials/intro-to-nservicebus/3-multiple-endpoints/
侵删。
目前为止,我们只是在一个endpoint中进行操作,但是真实的系统不会是这样的。消息通信系统的强大之处在于它可以在多进程多个服务器中执行代码,他们依赖交换message来协作。
在今天这节课中,我们将会把message handler转移到另外一个endpoint之中,然后我们会讨论如何在多个endpoint的系统中运行。
在接下来的15-20分钟里面,你会学习如何在多个endpoint之间发送信息和如何控制endpoint之间的message逻辑路由。
发送message
我们已经展示了单独一个endpoint如何使用SendLocal() 方法来“发送message给它自己”。我们使用这个endpoint来构建一个UI的时候用了IEndpointInstance 接口,而我们在处理message的时候使用了IMessageHandlerContext 接口,SendLocal() 在这两个接口中都有定义。
// From endpoint startup code await endpointInstance.SendLocal(command) .ConfigureAwait(false); // From a message handler await context.SendLocal(command) .ConfigureAwait(false);
将message发送到其他的endpoint也是完全一样的,我们只要把Local这个单词从方法名称中去掉就可以了。
// From endpoint startup code await endpointInstance.Send(command) .ConfigureAwait(false); // From a message handler await context.Send(command) .ConfigureAwait(false);
二者主要的区别是使用SendLocal()的时候, 终点(本地)已经知道的了。那么当我们调用Send()方法的时候,NServiceBus 是如何知道要将message发送到哪里呢?
逻辑路由
我们可以在代码中指定message要发往的地方,Send()
的一个重载来让我们完成这个工作:
// Not recommended, most of the time await endpointInstance.Send("Destination", command) .ConfigureAwait(false); // On the IMessageHandlerContext too, but still not recommended await context.Send("Destination", command) .ConfigureAwait(false);
然而,在大多数的情况下这个不是一个很好的做法。因为这要求每个开发者都要记住每一个message要发往哪里,以及每次发送的message的类型。
NServiceBus 应当设计成能具备感知配置的功能,因此无论何时,每当message发送出去的时候,框架就已经知道它应该被发送到何方了。
逻辑路由(logical routing)就是将特定的message类型映射到能够处理这个message的逻辑endpoint上去的一种映射方式。每一个command类型的message都应该有一个逻辑endpoint来接收它并且处理它。
我们之所以称它是逻辑路由,是因为这只是一个逻辑层而已,而不是和物理路由(physical routing)一样必不可少。在一个逻辑endpoint中,可能有包含多个部署在各个服务器中的物理endpoint实例。
endpoint是一个逻辑概念,通过一个endpoint名称和相关的代码来定义,它定义了一个message的接受者和处理者。
endpoint 实例是一个部署在服务器上的endpoint物理实例。多个endpoint实例可能会被部署在多个服务器上面来将大量信息分流到各个服务器上面。
现在我们将仅仅关注逻辑路由,把剩下的(物理路由,分流等等)放在下次讲。
由于逻辑路由并不关注物理上的东西,它仅仅定义了逻辑上面的接收从属关系,因此这应该是开发者应该控制的而不是操作者关心的。操作者可以仅仅使用配置文件将endpoint转移到其他的服务器上面,而改变message逻辑上的接收者就需要代码上面的修改和对代码的重新编译/部署。
因此,在代码里面去定义逻辑路由就说得过去了。
定义逻辑路由
message路由是message transport的功能,所以所有的路由功能都由transport对象提供。当我们定义一个message transport的时候,就会返回一个transport对象。就像下面使用MSMQ transport的例子这样:
var transport = endpointConfiguration.UseTransport<MsmqTransport>(); // Returns a RoutingSettings<MsmqTransport> var routing = transport.Routing();
RoutingSettings<T> 面向transport对象,路由选项在这个类里面以扩展方法的方式暴露。因此,仅仅对应这个transport的有效路由选项才会暴露。路由配置仅仅适用于Microsoft Azure中,例如,它不会再使用MSMQ transport的时候分发出API。(原文:RoutingSettings<T> is scoped to the actual transport being used, and routing options are exposed as extension methods on this class. Therefore, only routing options that are viable for the transport in use will appear. Routing configurations only applicable to Microsoft Azure, for example, won't clutter up the API when using the MSMQ transport.)
如果你在这节课使用SQL Server transport ,路由选项和MSMQ是一样的。
为了定义路由,可以从routing变量开始配置,然后调用RouteToEndpoint 方法,有三个重载方法:
// Specify the routing for a specific type routing.RouteToEndpoint(typeof(DoSomething), "SomeEndpoint"); // Specify the routing for all messages in an assembly routing.RouteToEndpoint(typeof(DoSomething).Assembly, "SomeEndpoint"); // Specify the routing for all messages in a given assembly and namespace routing.RouteToEndpoint(typeof(DoSomething).Assembly, "Specific.Namespace", "SomeEndpoint");
现在我们使用第一个重载方法,指定一个message类型。
练习
现在让我们将我们上节课创建的endpoint分开来。我们会重新配置我们的解决方案,因此ClientUI endpoint会发送 PlaceOrder command给一个新的endpoint,我们叫它Sales。Sales会变成真正的PlaceOrder command的逻辑接收者,然后我们会看到NServiceBus 从一个endpoint发送一个信息给另一个endpoint。
创建一个新的endpoint
首先,让我们为新的endpoint创建一个项目。
1.创建一个新的控制台应用程序,取名为Sales。
2.在Sales项目中,添加NServiceBus NuGet包。
3.在Sales项目中添加对Messages项目的引用,现在我们可以获取PlaceOrder message了。
既然我们已经有了一个Sales endpoint的项目了,我们需要添加一些类似的代码来配置和启动一个NServiceBus endpoint:
class Program { static void Main() { AsyncMain().GetAwaiter().GetResult(); } static async Task AsyncMain() { Console.Title = "Sales"; var endpointConfiguration = new EndpointConfiguration("Sales"); var transport = endpointConfiguration.UseTransport<MsmqTransport>(); endpointConfiguration.UseSerialization<JsonSerializer>(); endpointConfiguration.UsePersistence<InMemoryPersistence>(); endpointConfiguration.SendFailedMessagesTo("error"); endpointConfiguration.EnableInstallers(); var endpointInstance = await Endpoint.Start(endpointConfiguration) .ConfigureAwait(false); Console.WriteLine("Press Enter to exit."); Console.ReadLine(); await endpointInstance.Stop() .ConfigureAwait(false); } }
大多数的配置看起来和在ClientUI endpoint中一样。在两个endpoint钟配置能够匹配是很重要的(特别是message transport和序列化器)不然的话endpoint就很难解析对方。
例如,如果ClientUI endpoint使用.UseSerialization<XmlSerializer>()而Sales endpoint使用.UseSerialization<JsonSerializer>(), Sales endpoint 就无法解析ClientUI发送的序列化成XML格式的message,因为它需要的是JSON格式。
虽然大多配置都是一样的,我还是把不同之处展示一下:
Console.Title = "Sales"; var endpointConfiguration = new EndpointConfiguration("Sales");
显而易见,它们不同之处是控制台的标题“Sales”和EndpointConfiguration 的构造函数。这个构造函数定义这个Sales endpoint的名字,并且给它一个标识符。
这个就意味着Sales endpoint 将会创建它自己的用于监听message的队列,队列叫做Sales。我们现在已经有两个进程,并且各自都有自己的队列,因此我们可以在他们之间发送message。
虽然有很多重复的工作,但是要记住这个只是一个入门的练习。有很多各种各样的方法,例如使用INeedInitialization interface接口,可以将这些重复的配置代码集中起来。
调试这些项目
现在,我们可以运行Sales endpoint,尽管我们不希望Sales做除了启动,创建它的队列,然后等待永远不会到来的message之外的任何工作。如果你喜欢,这小节是一个很好的练习,如果你很忙的话也可以略过。
你会发现,在NServiceBus 解决方案中经常需要一次性运行多个项目(endpoint)。如果你想要更轻松一些,可以通过使用vs的多项目启动特性来让两个endpoint(ClientUI 和 Sales)在运行的时候同时启动。
如果你现在运行了这个项目,ClientUI 会做和之前一样的工作,而Sales 会启动然后等待永远不会到达的message。
转移handler
现在让我们将handler从ClientUI 转到Sales 里面去。
1.在解决方案资源管理器中,在ClientUI 项目里面找到PlaceOrderHandler.cs文件,然后拖到Sales 项目中去。
2.打开Sales项目中的PlaceOrderHandler.cs 文件,改变名称空间为Sales 。
3.Visual Studio 在你拖动文件到另一个项目中去的默认操作是拷贝它,所以你必须要在ClientUI 中删掉原来的PlaceOrderHandler.cs 文件。
如何你尝试着在ClientUI中产生一个订单,项目会报错,因为ClientUI没有handler来处理它了:
System.InvalidOperationException: No handlers could be found for message type: Messages.Commands.PlaceOrder
事实上,你可能会看到满屏的错误信息,因为这个message会不停的尝试,然后会中断一段时间之后再尝试,直到经过一段时间之后才会停止。我们会在第五节课中解决这个问题。
这样做的意义在于,如果一个message不小心发送到一个错误的endpoint里面去,它不会悄无声息地失败,并且这些错误信息不会被抛弃。
发送到另一个endpoint
现在我们要在ClientUI 中做一些改变,这样它就会发送PlaceOrder
command到Sales endpoint去了。
1.在ClientUI endpoint中,修改Program.cs文件,把endpointInstance.SendLocal(command) 替换成endpointInstance.Send(command)。
2.在AsyncMain 中,使用transport 变量来获得路由配置信息,然后通过添加下面的配置MSMQ transport的代码来指定PlaceOrder 的逻辑路由:
var routing = transport.Routing(); routing.RouteToEndpoint(typeof(PlaceOrder), "Sales");
这能够将PlaceOrder command发送到Sales endpoint去。
运行解决方案
现在当我们运行解决方案的时候,我们会获得两个控制台窗口,一个是ClientUI ,另一个是Sales。在折腾了那么久之后我们终于可以看到它们两了,我们可以尝试着在ClientUI 窗口中按P来发送一个订单。
你也可以右键点击窗口的标题栏,勾掉Let system position window选项,防止窗口每次都出现在不同的屏幕位置上。
在ClientUI 窗口里面,我们会看到这些输出:
INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit. p INFO ClientUI.Program Sending PlaceOrder command, OrderId = af0d1aa7-1611-4aa0-b83d-05e2d931d532 INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit. p INFO ClientUI.Program Sending PlaceOrder command, OrderId = e19d6160-595a-4c30-98b5-ea07bc44a6f8 INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit.
所有东西都一样,除了command不在这里处理了。
在Sales 窗口中,我们会看到这些输出:
Press Enter to exit. INFO Sales.PlaceOrderHandler Received PlaceOrder, OrderId = af0d1aa7-1611-4aa0-b83d-05e2d931d532 INFO Sales.PlaceOrderHandler Received PlaceOrder, OrderId = e19d6160-595a-4c30-98b5-ea07bc44a6f8
现在,我们已经成功地创建了两个进程,然后获取了进程间的通讯信息。现在让我们做一些不同的。关闭Sales endpoint窗口,让ClientUI 单独运行,然后按P来发送一些message到Sales endpoint中去。这么做是可以的,message会被发送,不会有任何错误,因为Sales endpoint已经离线。
重新启动Sales endpoint。在它启动之后,它会接收并且处理所有在队列中等待处理的message。
这个方式的价值在于它能够扮演你离线的系统,然后让所有工作照常进行下去不会出错。在离线的系统上线之后,一切工作都能够回归正轨。
总结
在这节课中,我们学习了如何在两个endpoint之间发送信息。这节课之前我们现在已经知道发送和处理message的基础知识。我们学习了如何控制逻辑路由,系统就会知道这些message应该发送到哪里。
在下一节课中,我们会学习关于事件,一个不同的message类型,然后使用发布者订阅者模式,将事件发布给多个订阅者。我们也会学习如何用这种模式来将我们的分布式系统以一种更加逻辑清晰和更易于维护的方式解耦。