浅谈Go与RabbitMQ(六)

122 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

今天接着来学习下使用RabbitMQ调用RPC模式,需要在远程计算机上运行函数并返回结果,这就叫做远程调用或者RPC

RPC概念

RPC(Remote Procedure Call,远程过程调用)是一种通过网络请求从远程服务器调用服务,不需要了解网络底层通信协议,RPC 协议基于传输层的 TCP 或 UDP 协议,或者是应用层的 HTTP 协议构建,允许开发者直接调用另一台计算机上的程序,从而使得开发网络分布式应用程序更加容易,例如微服务就是用了RPC调用

下面将用RabbitMQ构建一个RPC系统:客户端和RPC服务端,我们创建一个返回斐波那契数的服务

创建一个RPC需要注意以下三点:

  1. 关注哪个函数是本地调用,哪个函数是远程调用的
  2. 写好系统文档,标明每个组价之间的依赖关系
  3. 可以及时处理延迟情况,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/ json
  • reply_to:指定回调队列的名称
  • correlation_id:用于将RPC响应与请求相关联

关联ID--Correlation Id

上面的方法中,对每个RPC请求都创建一个回调队列太耗时了,其实可以对每一个客户端创建一个回调队列,如何处理该队列接收到响应之后,是来自哪个请求,这时候就需要correlation_id这个属性,针对每个请求设置一个唯一值,回调队列中就可以查看该属性,并根据这个属性将响应与请求进行匹配,如果接收到未知的correlation_id值,就可以放心地丢弃该消息,这不属于我们任何一个请求。

RPC相关流程

image.png

  • 客户端启动时,将创建一个匿名的回调队列
  • 对于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

image.png

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

image.png

上面例子非常简单,不能解决更复杂的问题,例如:

  • 如果没有服务器在运行,客户端应如何反应
  • 客户端是否应该为RPC设置某种超时时间
  • 如果服务器发生故障并引发异常,是否应该将其转发给客户端
  • 传入参数需要进行边界检查、类型检查

总结

今天浅谈了Go与RabbitMQ(六),还有很多相关的知识后面会继续深入,对于刚入门go语言的我来说,还有许多地方需要学习,有错误的地方欢迎大家指出,共同进步!!