本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第六篇——RPC。

这些教程涵盖了使用RabbitMQ创建消息传递应用程序的基础知识。

你需要安装RabbitMQ服务器才能完成这些教程,请参阅安装指南或使用Docker镜像

这些教程的代码是开源的,官方网站也是如此。

先决条件

本教程假设RabbitMQ已安装并运行在本机上的标准端口(5672)。如果你使用不同的主机、端口或凭据,则需要调整连接设置。

远程过程调用(RPC)

(使用Go RabbitMQ客户端)

在第二个教程中,我们学习了如何使用工作队列在多个worker之间分配耗时的任务。

但是,如果我们需要在远程计算机上运行函数并等待结果怎么办?好吧,那是一个不同的故事。这种模式通常称为远程过程调用RPC

在本教程中,我们将使用RabbitMQ构建一个RPC系统:客户端和可伸缩RPC服务器。由于我们没有值得分配的耗时任务,因此我们将创建一个虚拟RPC服务,该服务返回斐波那契数。

有关RPC的说明

尽管RPC是计算中非常常见的模式,但它经常受到批评。

当程序员不知道函数调用是本地的还是缓慢的RPC时,就会出现问题。这样的混乱会导致系统变幻莫测,并给调试增加了不必要的复杂性。滥用RPC可能会导致无法维护的意大利面条式代码而不是简化软件,

牢记这一点,请考虑以下建议:

  • 确定哪个函数调用是本地的,哪个是远程的。
  • 为你的系统编写文档。明确组件之间的依赖关系。
  • 处理错误情况。 当RPC服务器长时间关闭时,客户端应如何处理?

回调队列

通常,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器发送响应消息。为了接收响应,我们需要发送带有“回调”队列地址的请求。我们可以使用默认队列。让我们尝试一下:

q, err := ch.QueueDeclare(
"", // 不指定队列名,默认使用随机生成的队列名
false, // durable
false, // delete when unused
true, // exclusive
false, // noWait
nil, // arguments
) err = ch.Publish(
"", // exchange
"rpc_queue", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: q.Name, // 在这里指定callback队列名,也是在这个队列等回复
Body: []byte(strconv.Itoa(n)),
})

消息属性

AMQP 0-9-1协议预定义了消息附带的14个属性集。除以下属性外,大多数属性很少使用:

  • persistent:将消息标记为持久性(值为true)或瞬态(false)。你可能还记得第二个教程中的此属性。
  • content_type:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为application/ json是一个好习惯。
  • reply_to:常用于命名回调队列
  • correlation_id:有助于将RPC响应与请求相关联

关联ID(Correlation Id)

在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。这是相当低效的,但是幸运的是,有一种更好的方法——让我们为每个客户端创建一个回调队列。

这就引发了一个新问题,在该队列中收到响应后,尚不清楚响应属于哪个请求。这个时候就该使用correlation_id这个属性了。针对每个请求我们将为其设置一个唯一值。随后,当我们在回调队列中收到消息时,我们将查看该属性,并基于这个属性将响应与请求进行匹配。如果我们看到未知的correlation_id值,则可以放心地丢弃该消息——它不属于我们的请求。

你可能会问,为什么我们应该忽略回调队列中的未知消息,而不是报错而失败?这是由于服务器端可能出现竞争状况。尽管可能性不大,但RPC服务器可能会在向我们发送答案之后但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端上我们必须妥善处理重复的响应,并且理想情况下RPC应该是幂等的。

总结

我们的RPC工作流程如下:

  • 客户端启动时,它将创建一个匿名排他回调队列。
  • 对于RPC请求,客户端发送一条消息,该消息具有两个属性:reply_to(设置为回调队列)和correlation_id(设置为每个请求的唯一值)。
  • 该请求被发送到rpc_queue队列。
  • RPC工作程序(又名:服务器)正在等待该队列上的请求。当出现请求时,它会完成计算工作并把结果作为消息使用replay_to字段中的队列发回给客户端。
  • 客户端等待回调队列上的数据。出现消息时,它将检查correlation_id属性。如果它与请求中的值匹配,则将响应返回给应用程序。

完整示例

斐波那契函数:

func fib(n int) int {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return fib(n-1) + fib(n-2)
}
}

声明我们的斐波那契函数。它仅假设有效的正整数输入。 (不要指望这种方法适用于大量用户,它可能是最慢的递归实现)。

我们的RPC服务器rpc_server.go的代码如下所示:

package main

import (
"log"
"strconv" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
} func fib(n int) int {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return fib(n-1) + fib(n-2)
}
} func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() q, err := ch.QueueDeclare(
"rpc_queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS") msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) go func() {
for d := range msgs {
n, err := strconv.Atoi(string(d.Body))
failOnError(err, "Failed to convert body to integer") log.Printf(" [.] fib(%d)", n)
response := fib(n) err = ch.Publish(
"", // exchange
d.ReplyTo, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: d.CorrelationId,
Body: []byte(strconv.Itoa(response)),
})
failOnError(err, "Failed to publish a message") d.Ack(false)
}
}() log.Printf(" [*] Awaiting RPC requests")
<-forever
}

服务器代码非常简单:

  • 与往常一样,我们首先建立连接,通道并声明队列。
  • 我们可能要运行多个服务器进程。为了将负载平均分配给多个服务器,我们需要在通道上设置prefetch设置。
  • 我们使用Channel.Consume获取去队列,我们从队列中接收消息。然后,我们进入goroutine进行工作,并将响应发送回去。

我们的RPC客户端rpc_client.go的代码:

package main

import (
"log"
"math/rand"
"os"
"strconv"
"strings"
"time" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
} func randomString(l int) string {
bytes := make([]byte, l)
for i := 0; i < l; i++ {
bytes[i] = byte(randInt(65, 90))
}
return string(bytes)
} func randInt(min int, max int) int {
return min + rand.Intn(max-min)
} func fibonacciRPC(n int) (res int, err error) {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() q, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // noWait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") corrId := randomString(32) err = ch.Publish(
"", // exchange
"rpc_queue", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: q.Name,
Body: []byte(strconv.Itoa(n)),
})
failOnError(err, "Failed to publish a message") for d := range msgs {
if corrId == d.CorrelationId {
res, err = strconv.Atoi(string(d.Body))
failOnError(err, "Failed to convert body to integer")
break
}
} return
} func main() {
rand.Seed(time.Now().UTC().UnixNano()) n := bodyFrom(os.Args) log.Printf(" [x] Requesting fib(%d)", n)
res, err := fibonacciRPC(n)
failOnError(err, "Failed to handle RPC request") log.Printf(" [.] Got %d", res)
} func bodyFrom(args []string) int {
var s string
if (len(args) < 2) || os.Args[1] == "" {
s = "30"
} else {
s = strings.Join(args[1:], " ")
}
n, err := strconv.Atoi(s)
failOnError(err, "Failed to convert arg to integer")
return n
}

现在是时候看看rpc_client.gorpc_server.go的完整示例源代码了。

我们的RPC服务现已准备就绪。我们可以启动服务器:

go run rpc_server.go
# => [x] Awaiting RPC requests

要请求斐波那契数,请运行客户端:

go run rpc_client.go 30
# => [x] Requesting fib(30)

这里介绍的设计不是RPC服务的唯一可能的实现,但是它具有一些重要的优点:

  • 如果RPC服务器太慢,则可以通过运行另一台RPC服务器来进行扩展。尝试在新控制台中运行另一个rpc_server.go
  • 在客户端,RPC只需要发送和接收一条消息。结果,RPC客户端只需要一个网络往返就可以处理单个RPC请求。

我们的代码仍然非常简单,并且不会尝试解决更复杂(但很重要)的问题,例如:

  • 如果没有服务器在运行,客户端应如何反应?
  • 客户端是否应该为RPC设置某种超时时间?
  • 如果服务器发生故障并引发异常,是否应该将其转发给客户端?
  • 在处理之前防止无效的传入消息(例如检查边界,类型)。

如果要进行实验,可能会发现管理后台界面对于查看队列很有用。

RabbitMQ Go客户端教程6——RPC的更多相关文章

  1. RabbitMQ Go客户端教程1——HelloWorld

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,共分为六篇,本文是第一篇--HelloWorld. 这些教程涵盖了使用RabbitMQ创建消 ...

  2. RabbitMQ Go客户端教程5——topic

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第五篇--topic. 这些教程涵盖了使用RabbitMQ创建消息传递 ...

  3. RabbitMQ Go客户端教程4——路由

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第四篇--路由. 这些教程涵盖了使用RabbitMQ创建消息传递应用程 ...

  4. RabbitMQ Go客户端教程3——发布/订阅

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第三篇--发布/订阅. 这些教程涵盖了使用RabbitMQ创建消息传递 ...

  5. RabbitMQ Go客户端教程2——任务队列/工作队列

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第二篇--任务队列. 这些教程涵盖了使用RabbitMQ创建消息传递应 ...

  6. RabbitMQ 官方NET教程(六)【RPC】

    在第二个教程中,我们学习了如何使用Work Queues在多个工作者之间分配耗时的任务. 但是如果我们需要在远程计算机上运行功能并等待结果怎么办? 那是一个不同的模式. 此模式通常称为远程过程调用或R ...

  7. Go gRPC教程-客户端流式RPC(四)

    前言 上一篇介绍了服务端流式RPC,客户端发送请求到服务器,拿到一个流去读取返回的消息序列. 客户端读取返回的流的数据.本篇将介绍客户端流式RPC. 客户端流式RPC:与服务端流式RPC相反,客户端不 ...

  8. 【译】RabbitMQ:远程过程调用(RPC)

    在教程二中,我们学习了如何使用工作队列在多个工作线程中分发耗时的任务.但如果我们需要去执行远程机器上的方法并且等待结果会怎么样呢?那又是另外一回事了.这种模式通常被称为远程过程调用(RPC). 本教程 ...

  9. RabbitMq C# .net 教程

    本文转载来自 [http://www.cnblogs.com/yangecnu/p/Introduce-RabbitMQ.html]写的很详细. 文件安装包官方DEMO下载地址是:http://pan ...

随机推荐

  1. 38、python并发编程之IO模型

    目录: 一 IO模型介绍 二 阻塞IO(blocking IO) 三 非阻塞IO(non-blocking IO) 四 多路复用IO(IO multiplexing) 五 异步IO(Asynchron ...

  2. 排查log4j不输出日志到文件的问题

    问题描述 项目使用Spring Boot框架,在pom文件中添加了如下配置: <dependency> <groupId>org.slf4j</groupId> & ...

  3. Git简单介绍以及使用入门

    Git Git:分布式版本控制系统, 此外还有 SVN (集中式版本控制系统) 下载地址(阿里云镜像) :CNPM Binaries Mirror (npmmirror.com) Git Bash : ...

  4. Dubbo扩展点应用之六服务动态降级

    服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别以释放服务器资源保证核心任务的政策运行. 为什么要使用服务降级呢?这是为了防止分布式服务发送雪崩效应,也就是蝴蝶 ...

  5. Azure KeyVault(三)通过 Microsoft.Azure.KeyVault 类库在 .NET Core 上获取 Secrets

    一,引言 上一篇文章,我们介绍了 Azure Key Vault 在实际项目中的用途,Azure Key Vault 作为密钥管理的服务,我们可以很轻松的利用它创建和控制用于加密的密钥,和管理证书和机 ...

  6. OpenLDAP测试搭建

    目录 ldap介绍 测试环境 安装LDAP服务端 设置LDAP的root密码 配置LDAP服务端 创建LDAP证书 设置LDAP数据库 创建LDAP用户 添加防火墙规则 开启LDAP日志 配置LDAP ...

  7. 彻底明白Linux硬链接和软链接

    [硬连接] 在Linux的文件系统中,保存在磁盘分区中的实际文件不管是什么类型系统都给它分配一个编号,称为索引节点号(Inode Index),这个索引节点用来标识这个文件,即这个索引节点就代表了这个 ...

  8. 快速搭建一套k8s集群环境

    参考官网 kubeadm是官方提供的快速搭建k8s集群的开源工具,对于非运维人员学习k8s,kubeadm方式安装相对更简单. kubeadm创建一个集群:https://kubernetes.io/ ...

  9. 企业都适用的自助式BI工具

    ​未来的BI将是自助BI的时代.随着数据爆发式增长,像ERP.OA.CRM等系统在企业运用的越来越多,这些系统的使用必然会产生很多的数据.随着大数据的到来,企业在数据分析展现层面,面临着困境.下面就给 ...

  10. Redis 7.0 新功能新特性总览

    说明:本文根据Redis 7 RC2 的release note 整理并翻译 近日,Redis 开源社区发布了7.0的两个预览版.在这两个预览版中,有很多Redis 7.0中新增加的特性,新增加的命令 ...