NATS: 请求-响应消息
请求-回复消息
https://docs.nats.io/nats-concepts/core-nats/reqreply
请求-回复
在分布式系统中,请求-回复是一种常见的模式。发送请求之后,应用程序或者基于特定的超时等待回复,或者 同步
收到响应内容
现代系统不断增长的复杂性需要诸如 位置透明性
的特性,扩展与收缩,可发现性等等。为了支持该特性,多种其他的技术需要与其他组件协作,sidecar 和代理。NATS 从使用另外一种途径,实现的请求-回复模式更加简单。
NATS 使得请求-回复更为简单和强大
- NATS 使用核心通讯机制来支持请求-回复模式 - 发布和订阅。请求使用一个回复主题发布到特定的主题上,响应者监听在该主题上,然后将回复发送回回复主题。回复主题被称为
Inbox
收件箱。这些唯一的主题被动态直接返回给请求者,而不受彼此位置的影响。 - 多个NATS 响应者可以构成动态的 queue 组。进而,不需要手动将订阅者添加进入或者从组中删除来启动或者停止分布式消息。这是自动完成的,该特性支持响应者根据需要扩展或者收缩。
- NATS 应用程序
退出前抽干
(在关闭连接之前处理缓冲的消息))。该特性支持应用程序可以收缩而不会丢失请求消息。 - 因为 NATS 基于发布订阅,可观察性如同执行其他应用一样简单,可以观察请求和响应来测量延迟,注意异常情况、直接可扩展性等。
- NATS 的强大甚至可以支持多响应,使用首个响应并高效丢弃其他的消息。该特性优美支持多个响应者,减少响应的延迟和抖动。
模式
使用 请求-回复 演练 来实验该功能
没有响应者
当请求发送到没有订阅者的主题上时,可以方便地知道这种问题。
对于该场景,NATS 客户端可以 可选没有响应者消息 。这要求服务器和客户端支持 Headers。在启用之后,发送到没有订阅者的主题后,将立即收到一个 503 的状态,没有主体的回复响应。
多数的客户端对于此种场景将抛出或者返回一个错误,例如
m, err := nc.Request("foo", nil, time.Second);
# err == nats.ErrNoResponders
https://natsbyexample.com/examples/messaging/request-reply/dotnet2
请求-响应
模式支持客户端在发送一个消息之后,期待得到某种响应返回。在实践中,请求消息或者是一个命令,期望请求的服务处理某种工作并返回状态的变化,或者是一个查询,用来请求信息。
与诸如 HTTP 的请求响应约束不同,NATS 并不严格限制在客户端和服务端的点对点的约束。请求-响应模式是构建在核心的发布订阅模型之上的。
在默认情况下,这意味着任何对请求消息的订阅者都可以作为响应者并回复客户端。这是因为 NATS 并不限制点对点的交互,客户端可以向 NATS 指示应允许多个回复。
下面的示例展示了基本的请求-响应模式,包括标准的在没有订阅者存在的情况下的 没有响应者
错误处理,以及响应请求的消息。
代码说明
首先需要安装 NATS.Net NuGet 包。
创建 NATS 连接
代码中使用常用的命名空间和创建 NATS 链接
连接到 NATS 服务器,因为连接是 Disposable 的,我们应该 flush 使用的缓冲区并关闭连接。
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using NATS.Client.Core;
var stopwatch = Stopwatch.StartNew();
var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222";
Log($"[CON] Connecting to {url}...");
var opts = NatsOpts.Default with { Url = url };
await using var nats = new NatsConnection(opts);
创建消息处理器,订阅到目标主题上
创建消息的事件处理器,然后借助于通配符 greet.*
订阅到目标主题上。
当客户端准备请求的时候,客户端填充 reply-to
字段,然后开始将它作为主题监听 ()订阅) 它,对于响应者来说,只是简单的发布消息到 reply-to 上。
await using var sub = await nats.SubscribeCoreAsync<int>("greet.*");
var reader = sub.Msgs;
var responder = Task.Run(async () =>
{
await foreach (var msg in reader.ReadAllAsync())
{
var name = msg.Subject.Split('.')[1];
Log($"[REP] Received {msg.Subject}");
await Task.Delay(500);
await msg.ReplyAsync($"Hello {name}!");
}
});
客户端发送请求,等待响应最多 1s
var replyOpts = new NatsSubOpts { Timeout = TimeSpan.FromSeconds(2) };
Log("[REQ] From joe");
var reply = await nats.RequestAsync<int, string>("greet.joe", 0, replyOpts: replyOpts);
Log($"[REQ] {reply.Data}");
Log("[REQ] From sue");
reply = await nats.RequestAsync<int, string>("greet.sue", 0, replyOpts: replyOpts);
Log($"[REQ] {reply.Data}");
Log("[REQ] From bob");
reply = await nats.RequestAsync<int, string>("greet.bob", 0, replyOpts: replyOpts);
Log($"[REQ] {reply.Data}");
服务端取消订阅
取消订阅。
等待原有的处理队列完成
await sub.UnsubscribeAsync();
await responder;
服务端取消订阅之后,后继请求将会超时
try
{
reply = await nats.RequestAsync<int, string>("greet.joe", 0, replyOpts: replyOpts);
Log($"[REQ] {reply.Data} - This will timeout. We should not see this message.");
}
catch (NatsNoReplyException)
{
Log("[REQ] timed out!");
}
完成
Log("Bye!");
return;
void Log(string log) => Console.WriteLine($"{stopwatch.Elapsed} {log}");