开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
今天接着来学习下使用RabbitMQ调用RPC模式,需要在远程计算机上运行函数并返回结果,这就叫做远程调用或者RPC
RPC概念
RPC(Remote Procedure Call,远程过程调用)是一种通过网络请求从远程服务器调用服务,不需要了解网络底层通信协议,RPC 协议基于传输层的 TCP 或 UDP 协议,或者是应用层的 HTTP 协议构建,允许开发者直接调用另一台计算机上的程序,从而使得开发网络分布式应用程序更加容易,例如微服务就是用了RPC调用
下面将用RabbitMQ构建一个RPC系统:客户端和RPC服务端,我们创建一个返回斐波那契数的服务
创建一个RPC需要注意以下三点:
- 关注哪个函数是本地调用,哪个函数是远程调用的
- 写好系统文档,标明每个组价之间的依赖关系
- 可以及时处理延迟情况,RPC服务器长时间未反应,客服端如何处理。
回调队列
客户端发送请求消息,服务端发送响应消息,为了接收响应消息,需要发送带有回调队列地址的请求,可以使用默认队列
q, err := ch.QueueDeclare(
"", // 使用空串,默认使用随机生成的队列名 例如gen- xxx
false, // durable
false, // delete when unused
true, // exclusive
false, // noWait
nil, // arguments
)
err = ch.Publish(
"", // exchange
"rpc_queue_YYQQ", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: q.Name, // 指定回调队列名称,在这个队列等待回复
Body: []byte(strconv.Itoa(n)),
})
AMQP定义了消息附带的14个属性集合,以下列出的属性是常用的:
persistent:将消息标记为持久性(值为true)或瞬态(false),前面的文章有介绍到content_type:用于描述编码的MIME类型,例如,使用JSON编码格式,将属性设置为application/ jsonreply_to:指定回调队列的名称correlation_id:用于将RPC响应与请求相关联
关联ID--Correlation Id
上面的方法中,对每个RPC请求都创建一个回调队列太耗时了,其实可以对每一个客户端创建一个回调队列,如何处理该队列接收到响应之后,是来自哪个请求,这时候就需要correlation_id这个属性,针对每个请求设置一个唯一值,回调队列中就可以查看该属性,并根据这个属性将响应与请求进行匹配,如果接收到未知的correlation_id值,就可以放心地丢弃该消息,这不属于我们任何一个请求。
RPC相关流程
- 客户端启动时,将创建一个匿名的回调队列
- 对于RPC请求,客户端发送一条消息,该消息具有两个属性:
reply_to(设置为回调队列名称)和correlation_id(设置为每个请求的唯一值) - RPC请求被发送到
rpc_queue队列 - 服务端Server正在等待该队列上的请求,当出现请求时,它会完成计算工作并把结果作为消息,通过
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服务端 server.go 代码如下
func main() {
conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
DoError(err, "Failed connect to RabbitMQ!")
defer conn.Close()
ch, err := conn.Channel()
DoError(err, "Failed to open a channel")
defer ch.Close()
queue, err := ch.QueueDeclare(
"rpc_queue",
false,
false,
false,
false,
nil)
DoError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
DoError(err, "Failed to set QoS")
content, err := ch.Consume(
queue.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
DoError(err, "Failed to register a consumer")
var wait chan struct{}
go func() {
for msg := range content {
log.Printf("Received a message %s", msg.Body)
n, err := strconv.Atoi(string(msg.Body))
DoError(err, "Failed to convert body to integer")
log.Printf("调用函数 fib(%d) ", n)
respone := fib(n)
err = ch.Publish(
"",
msg.ReplyTo,
false,
false,
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: msg.CorrelationId,
Body: []byte(strconv.Itoa(respone)),
})
DoError(err, "Failed to publish a message")
msg.Ack(false) //手动确认消息
}
}()
log.Printf(" -- Awaiting RPC requests")
<-wait
}
上述代码解释:
- 首先建立RabbitMQ连接,新建通道并声明队列
- 如果要运行多个服务器进程,为了将负载平均分配给多个服务器,需要在通道上打开
prefetch设置。 - 我们使用
Channel.Consume获取去队列,我们从队列中接收消息。然后,打开goroutine进行工作,并将响应发送回去。
执行服务端程序
go run server.go
RPC客户端 client.go 代码如下:
// 处理错误信息
func DoError(err error, msg string) {
if err != nil {
log.Fatalf("%s:%s", msg, err)
}
}
func fibonacciRPC(n int) (res int, err error) {
conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
DoError(err, "Failed connect to RabbitMQ!")
defer conn.Close()
ch, err := conn.Channel()
DoError(err, "Failed to open a channel")
defer ch.Close()
queue, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // noWait
nil, // arguments
)
DoError(err, "Failed to declare a queue")
msgs, err := ch.Consume(
queue.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
DoError(err, "Failed to register a consumer")
corrId := "YYQQ" //唯一配对码
err = ch.Publish(
"", // exchange
"rpc_queue", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: queue.Name,
Body: []byte(strconv.Itoa(n)),
})
DoError(err, "Failed to publish a message")
for d := range msgs {
if corrId == d.CorrelationId {
res, err = strconv.Atoi(string(d.Body))
DoError(err, "Failed to convert body to integer")
break
}
}
return
}
func getFrom(args []string) int {
var res string
if (len(args) < 2) || os.Args[1] == "" {
res = "20"
} else {
res = strings.Join(args[1:], " ")
}
n, err := strconv.Atoi(res)
DoError(err, "Failed to convert to integer")
return n
}
func main() {
n := getFrom(os.Args)
log.Printf(" Requesting fib(%d)", n)
res, err := fibonacciRPC(n)
DoError(err, "Failed to handle RPC request")
log.Printf(" Requesting fib(%d) = %d", n, res)
}
执行客户端程序
go run rpc_client.go 10
上面例子非常简单,不能解决更复杂的问题,例如:
- 如果没有服务器在运行,客户端应如何反应
- 客户端是否应该为RPC设置某种超时时间
- 如果服务器发生故障并引发异常,是否应该将其转发给客户端
- 传入参数需要进行边界检查、类型检查
总结
今天浅谈了Go与RabbitMQ(六),还有很多相关的知识后面会继续深入,对于刚入门go语言的我来说,还有许多地方需要学习,有错误的地方欢迎大家指出,共同进步!!