1. 概述

在我们介绍过 Tell 模式 后,是时候来看看在实际项目中更常见的交互方式:请求-响应模式

这种模式在 Akka 中非常常见,尤其是在需要获取某个操作结果的场景下。本文将介绍几种实现该模式的方式,包括最基础的直接响应、适配响应以及更高级的 Ask 模式。

2. Akka 依赖

要使用 Akka Typed 库,我们需要引入以下依赖:

libraryDependencies += "com.typesafe.akka" % "akka-actor-typed_2.12" % "2.6.8",
libraryDependencies += "com.typesafe.akka" % "akka-actor-testkit-typed_2.12" % "2.6.8" % Test

其中 akka-actor-typed 是核心库,而 akka-actor-testkit-typed 是用于测试的辅助库。

3. 场景设定

为了演示请求-响应模式,我们先定义一个简单的场景:

假设我们需要一个服务,将传入的字符串进行 Base64 编码。客户端发送原始字符串,期望服务返回其 Base64 编码结果。

为此,我们先定义通信协议:

sealed trait Request
final case class ToEncode(payload: String, replyTo: ActorRef[Encoded]) extends Request
sealed trait Response
final case class Encoded(payload: String) extends Response

其中:

  • ToEncode 是请求消息,携带待编码字符串和响应目标 Actor 的引用。
  • Encoded 是编码后的响应消息。

4. 请求-响应:最简单的实现方式

在 Akka 中,要实现请求-响应模式,关键在于使用 ActorRef。**ActorRef[-T] 表示一个可以处理类型为 T 消息的 Actor 引用**。被调用的 Actor 通过这个引用向调用方返回结果。

我们来看 Base64Encoder 的实现:

object Base64Encoder {
  def apply(): Behavior[Request] =
    Behaviors.receiveMessage {
      case ToEncode(payload, replyTo) =>
        val encodedPayload = Base64.getEncoder.encode(payload.getBytes(StandardCharsets.UTF_8))
        replyTo ! Encoded(encodedPayload.toString)
        Behaviors.same
    }
}

在这个例子中,Base64Encoder 接收到 ToEncode 消息后,将 payload 编码并通过 replyTo 发送回响应。

再来看调用方的实现:

object NaiveEncoderClient {
  def apply(encoder: ActorRef[Request]): Behavior[Encoded] =
    Behaviors.setup { context =>
      encoder ! ToEncode("The answer is 42", context.self)
      Behaviors.receiveMessage {
        case Encoded(payload) =>
          context.log.info(s"The encoded payload is $payload")
          Behaviors.empty
      }
    }
}

✅ 优点:实现简单直观
❌ 缺点:调用方必须使用与被调用方一致的消息协议,灵活性差

如果调用方想监听其他类型的消息,就不太适用了。这时,我们需要引入 适配响应模式(Adapted Response)

5. 请求-响应:适配响应模式

适配响应模式适用于调用方需要使用自己的消息协议的场景。

我们定义一个新的协议:

sealed trait Command
final case class KeepASecret(secret: String) extends Command

我们的客户端 EncoderClient 希望既能处理 KeepASecret 消息,也能处理 Encoded 响应。

为此,我们定义一个私有适配消息:

private final case class WrappedEncoderResponse(response: Encoded) extends Command

然后使用 Akka 的 messageAdapter 机制:

val encoderResponseMapper: ActorRef[Encoded] =
  context.messageAdapter(response => WrappedEncoderResponse(response))

最终的完整实现如下:

object EncoderClient {
  def apply(encoder: ActorRef[Request]): Behavior[Command] =
    Behaviors.setup { context =>
      val encoderResponseMapper: ActorRef[Encoded] =
        context.messageAdapter(response => WrappedEncoderResponse(response))

      Behaviors.receiveMessage {
        case KeepASecret(secret) =>
          encoder ! ToEncode(secret, encoderResponseMapper)
          Behaviors.same
        case WrappedEncoderResponse(response) =>
          context.log.info(s"I will keep a secret for you: ${response.payload}")
          Behaviors.same
      }
    }
}

✅ 优点:调用方可以使用自己的协议
⚠️ 注意:每个消息类型只能有一个 adapter,新注册的会覆盖旧的

6. 请求-响应:Ask 模式

Ask 模式用于需要将请求与响应一一对应绑定的场景。它比适配响应模式更加精确,特别适合需要“请求-单响应”语义的交互。

6.1. API 网关场景

我们以一个 API 网关为例。网关接收客户端的编码请求,然后调用编码服务,再将结果返回给客户端。

定义协议:

sealed trait Command
final case class PleaseEncode(payload: String, replyTo: ActorRef[GentlyEncoded]) extends Command
final case class GentlyEncoded(encodedPayload: String)

同时定义适配消息:

private case class AdaptedResponse(payload: String, replyTo: ActorRef[GentlyEncoded]) extends Command
private case class AdaptedErrorResponse(error: String) extends Command

6.2. 如何使用 Ask 模式

使用 context.ask 方法:

implicit val timeout: Timeout = 5.seconds
Behaviors.receiveMessage {
  case PleaseEncode(payload, replyTo) =>
    context.ask(encoder, ref => ToEncode(payload, ref)) {
      case Success(Encoded(encodedPayload)) => AdaptedResponse(encodedPayload, replyTo)
      case Failure(exception) => AdaptedErrorResponse(exception.getMessage)
    }
    Behaviors.same

Ask 模式会返回一个 Future[Encoded],我们可以对这个 Future 进行映射处理。

响应处理逻辑:

Behaviors.receiveMessage {
  case AdaptedResponse(encoded, ref) =>
    ref ! GentlyEncoded(encoded)
    Behaviors.same
  case AdaptedErrorResponse(error) =>
    context.log.error(s"There was an error during encoding: $error")
    Behaviors.same
}

✅ 优点:请求与响应绑定清晰,支持异步处理
⚠️ 注意:需设置超时时间,避免 Future 永远等待

7. 总结

本文介绍了 Akka Typed 中实现请求-响应模式的几种方式:

  • ✅ 简单响应(直接 replyTo)
  • ✅ 适配响应(messageAdapter)
  • ✅ Ask 模式(基于 Future 的绑定)

每种方式都有其适用场景:

模式 适用场景 优点 缺点
简单响应 单一协议 简洁 灵活性差
适配响应 多协议混合 灵活 每类型只能一个 adapter
Ask 模式 异步绑定 精确控制 需处理 Future 和超时

当然,Akka 还提供了更高级的模式,比如:

这些适用于更复杂的交互场景。

📌 本文所有示例代码均可在 GitHub 仓库 中找到。


原始标题:Akka Interaction Patterns: Request-Response