Actors介绍-聊天室实现

akka版本2.6.9
版权声明:本文为博主原创文章,未经博主允许不得转载。

 在使用Akka Actors时需要导入相关依赖(默认介绍maven的,如果您是使用sbt或gradle自行官网查看)

<properties>
  <akka.version>2.6.10</akka.version>
  <scala.binary.version>2.13</scala.binary.version>
</properties>
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-actor-typed_${scala.binary.version}</artifactId>
  <version>${akka.version}</version>
</dependency>

 Akka模块的Java和Scala dsl都捆绑在同一个JAR中。为了获得平稳的开发体验,在使用Eclipse或IntelliJ这样的IDE时,您可以禁用自动导入器,使其在使用Scala时不建议javadsl导入,或者相反。

一个更复杂的例子
 下一个例子更现实,演示了一些重要的模式:

  • 使用接口和实现该接口的类来表示参与者可以接收的多个消息
  • 通过使用子角色处理会话
  • 通过改变行为来处理状态
  • 使用多个参与者以类型安全的方式表示协议的不同部分
    在这里插入图片描述
    功能性风格
     首先,我们将以函数样式显示此示例,然后以面向对象样式显示相同的示例。选择使用哪种风格是一个品味问题,两种风格都可以混合使用,这取决于哪种风格对特定的参与者来说是最好的。在样式指南中提供了选择的注意事项。
     考虑一个运行聊天室的actor:客户端actor可以通过发送一条包含他们的屏幕名的消息来连接,然后他们可以发布消息。聊天室actor将把所有发布的消息发送给所有当前连接的客户端actor。协议定义可以如下所示:
static interface RoomCommand {}

public static final class GetSession implements RoomCommand {
  public final String screenName;
  public final ActorRef<SessionEvent> replyTo;

  public GetSession(String screenName, ActorRef<SessionEvent> replyTo) {
    this.screenName = screenName;
    this.replyTo = replyTo;
  }
}

interface SessionEvent {}

public static final class SessionGranted implements SessionEvent {
  public final ActorRef<PostMessage> handle;

  public SessionGranted(ActorRef<PostMessage> handle) {
    this.handle = handle;
  }
}

public static final class SessionDenied implements SessionEvent {
  public final String reason;

  public SessionDenied(String reason) {
    this.reason = reason;
  }
}

public static final class MessagePosted implements SessionEvent {
  public final String screenName;
  public final String message;

  public MessagePosted(String screenName, String message) {
    this.screenName = screenName;
    this.message = message;
  }
}

interface SessionCommand {}

public static final class PostMessage implements SessionCommand {
  public final String message;

  public PostMessage(String message) {
    this.message = message;
  }
}
private static final class NotifyClient implements SessionCommand {
  final MessagePosted message;

  NotifyClient(MessagePosted message) {
    this.message = message;
  }
}

 最初,客户端参与者只能访问一个ActorRef<GetSession>这让他们迈出了第一步。
 一旦建立了客户端的会话,它就会得到一个包含句柄的SessionGranted消息,该句柄用于解锁下一个协议步骤,即发布消息。需要将PostMessage命令发送到这个表示已添加到聊天室的会话的特定地址。会话的另一个方面是客户端通过replyTo参数显示了它自己的地址,以便后续的messagepost事件可以发送给它。
 这说明了actor如何表达Java对象上的方法调用以外的内容。声明的消息类型及其内容描述了一个完整的协议,该协议可以涉及多个Actor,并且可以通过多个步骤进行演化。下面是聊天室协议的实现:

public class ChatRoom {
  private static final class PublishSessionMessage implements RoomCommand {
    public final String screenName;
    public final String message;

    public PublishSessionMessage(String screenName, String message) {
      this.screenName = screenName;
      this.message = message;
    }
  }

  public static Behavior<RoomCommand> create() {
    return Behaviors.setup(
        ctx -> new ChatRoom(ctx).chatRoom(new ArrayList<ActorRef<SessionCommand>>()));
  }

  private final ActorContext<RoomCommand> context;

  private ChatRoom(ActorContext<RoomCommand> context) {
    this.context = context;
  }

  private Behavior<RoomCommand> chatRoom(List<ActorRef<SessionCommand>> sessions) {
    return Behaviors.receive(RoomCommand.class)
        .onMessage(GetSession.class, getSession -> onGetSession(sessions, getSession))
        .onMessage(PublishSessionMessage.class, pub -> onPublishSessionMessage(sessions, pub))
        .build();
  }

  private Behavior<RoomCommand> onGetSession(
      List<ActorRef<SessionCommand>> sessions, GetSession getSession)
      throws UnsupportedEncodingException {
    ActorRef<SessionEvent> client = getSession.replyTo;
    ActorRef<SessionCommand> ses =
        context.spawn(
            Session.create(context.getSelf(), getSession.screenName, client),
            URLEncoder.encode(getSession.screenName, StandardCharsets.UTF_8.name()));
    // narrow to only expose PostMessage
    client.tell(new SessionGranted(ses.narrow()));
    List<ActorRef<SessionCommand>> newSessions = new ArrayList<>(sessions);
    newSessions.add(ses);
    return chatRoom(newSessions);
  }

  private Behavior<RoomCommand> onPublishSessionMessage(
      List<ActorRef<SessionCommand>> sessions, PublishSessionMessage pub) {
    NotifyClient notification =
        new NotifyClient((new MessagePosted(pub.screenName, pub.message)));
    sessions.forEach(s -> s.tell(notification));
    return Behaviors.same();
  }

  static class Session {
    static Behavior<ChatRoom.SessionCommand> create(
        ActorRef<RoomCommand> room, String screenName, ActorRef<SessionEvent> client) {
      return Behaviors.receive(ChatRoom.SessionCommand.class)
          .onMessage(PostMessage.class, post -> onPostMessage(room, screenName, post))
          .onMessage(NotifyClient.class, notification -> onNotifyClient(client, notification))
          .build();
    }

    private static Behavior<SessionCommand> onPostMessage(
        ActorRef<RoomCommand> room, String screenName, PostMessage post) {
      // from client, publish to others via the room
      room.tell(new PublishSessionMessage(screenName, post.message));
      return Behaviors.same();
    }

    private static Behavior<SessionCommand> onNotifyClient(
        ActorRef<SessionEvent> client, NotifyClient notification) {
      // published from the room
      client.tell(notification.message);
      return Behaviors.same();
    }
  }
}

 通过改变行为而不是使用任何变量来管理状态。
 当一个新的GetSession命令进来时,我们将该客户端添加到返回行为中的列表中。然后,我们还需要创建会话的ActorRef,它将用于发布消息。在本例中,我们希望创建一个非常简单的Actor,它将PostMessage命令重新打包到PublishSessionMessage命令中,该命令还包含屏幕名。
我们在这里声明的行为可以处理RoomCommand的两种子类型。GetSession已经解释过了,来自会话Actor的PublishSessionMessage命令将触发包含的聊天室消息向所有连接的客户端传播。但是我们不想给PublishSessionMessage命令发送到任意客户的能力,我们保留权利内部会话actor我们create-otherwise客户可能会带来完全不同的屏幕名称(想象一下GetSession协议进一步包括身份验证信息安全)。因此,PublishSessionMessage具有私有可视性,不能在聊天室之外创建。
如果我们不关心保护会话和屏幕名之间的通信,那么我们可以改变协议,如PostMessage被删除,所有客户端只获得一个ActorRef<PublishSessionMessage>;发送。在这种情况下,不需要会话actor,我们可以使用context.getSelf()。在这种情况下,类型检查是有效的,因为ActorRef<RoomCommand>;类型参数是相反的,这意味着我们可以使用一个ActorRef<RoomCommand>;无论一个ActorRef< PublishSessionMessage>;这是有道理的,因为前者比后者会说更多的语言。相反就会有问题,所以传递一个ActorRef<PublishSessionMessage>;在ActorRef< RoomCommand>会导致类型错误。
尝试
 为了看到这个聊天室在行动,我们需要写一个客户端Actors,可以使用它:

public class Gabbler {
  public static Behavior<ChatRoom.SessionEvent> create() {
    return Behaviors.setup(ctx -> new Gabbler(ctx).behavior());
  }

  private final ActorContext<ChatRoom.SessionEvent> context;

  private Gabbler(ActorContext<ChatRoom.SessionEvent> context) {
    this.context = context;
  }

  private Behavior<ChatRoom.SessionEvent> behavior() {
    return Behaviors.receive(ChatRoom.SessionEvent.class)
        .onMessage(ChatRoom.SessionDenied.class, this::onSessionDenied)
        .onMessage(ChatRoom.SessionGranted.class, this::onSessionGranted)
        .onMessage(ChatRoom.MessagePosted.class, this::onMessagePosted)
        .build();
  }

  private Behavior<ChatRoom.SessionEvent> onSessionDenied(ChatRoom.SessionDenied message) {
    context.getLog().info("cannot start chat room session: {}", message.reason);
    return Behaviors.stopped();
  }

  private Behavior<ChatRoom.SessionEvent> onSessionGranted(ChatRoom.SessionGranted message) {
    message.handle.tell(new ChatRoom.PostMessage("Hello World!"));
    return Behaviors.same();
  }

  private Behavior<ChatRoom.SessionEvent> onMessagePosted(ChatRoom.MessagePosted message) {
    context
        .getLog()
        .info("message has been posted by '{}': {}", message.screenName, message.message);
    return Behaviors.stopped();
  }
}

 通过这种行为,我们可以创建一个actor,它将接受聊天室会话、发布消息、等待它的发布,然后终止。最后一步需要改变行为的能力,我们需要从正常的运行行为转换到终止状态。这就是为什么在这里我们不返回相同的值,而返回另一个特殊的停止值。
 现在要尝试一下,我们必须同时启动聊天室和gabbler当然我们是在Actor系统中进行的。既然只能有一个用户监护人,我们可以从聊天室的gabbler(这不是我们想要的——它使逻辑复杂化了)或者从聊天室的gabbler(这是荒谬的)或者我们从第三个角色开始他们两个——我们唯一明智的选择:

public class Main {
  public static Behavior<Void> create() {
    return Behaviors.setup(
        context -> {
          ActorRef<ChatRoom.RoomCommand> chatRoom = context.spawn(ChatRoom.create(), "chatRoom");
          ActorRef<ChatRoom.SessionEvent> gabbler = context.spawn(Gabbler.create(), "gabbler");
          context.watch(gabbler);
          chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler));

          return Behaviors.receive(Void.class)
              .onSignal(Terminated.class, sig -> Behaviors.stopped())
              .build();
        });
  }

  public static void main(String[] args) {
    ActorSystem.create(Main.create(), "ChatRoomDemo");
  }
}

 在良好的传统中,我们称主actor为它是什么,它直接对应于传统Java应用程序中的主方法。这个Actor将自动执行它的工作,我们不需要从外部发送消息,所以我们将它声明为Void类型。actor不仅接收外部消息,还会收到某些系统事件的通知,即所谓的信号。为了访问它们,我们选择使用receive behavior decorator来实现这个特定的操作。对于信号(Signal的子类)将调用提供的onSignal函数,对于用户消息将调用onMessage函数。
 这个特殊的主要Actor是使用behavior创建的。设置,它就像行为的工厂。与行为相反,行为实例的创建被推迟到actor启动时。在actor运行之前立即创建行为实例的接收。设置中的工厂函数作为参数传递给ActorContext,例如,它可以用于生成子actor。这个主actor创建聊天室和gabbler,并启动它们之间的会话,当gabbler结束时,我们将收到由于调用了context而终止的事件。观看。这允许我们关闭Actor系统:当主Actor终止时,就不再需要做什么了。
 因此,在创建具有主actor行为的actor系统之后,我们可以让主方法返回,ActorSystem将继续运行,JVM将保持活动状态,直到根actor停止。
面向对象的风格
 上面的示例使用了函数式编程风格,在这种风格中,您将一个函数传递给工厂,然后由工厂构造一个行为,对于有状态的actor,这意味着将不可变的状态作为参数传递,并在需要对更改的状态进行操作时切换到新的行为。另一种表达方式是更面向对象的风格,其中定义了actor行为的具体类,可变状态作为字段保存在其中。
 选择使用哪种风格是一个品味问题,两种风格都可以混合使用,这取决于哪种风格对特定的actor来说是最好的。在样式指南中提供了选择的注意事项。
AbstractBehavior API
 定义一个基于actor行为的类首先要扩展akka.actor.type .javads . abstractbehavioro其中T是行为将接受的消息类型。
 让我们重复上面使用AbstractBehavior实现的更复杂示例中的聊天室示例。与actor交互的协议看起来是一样的:

static interface RoomCommand {}

public static final class GetSession implements RoomCommand {
  public final String screenName;
  public final ActorRef<SessionEvent> replyTo;

  public GetSession(String screenName, ActorRef<SessionEvent> replyTo) {
    this.screenName = screenName;
    this.replyTo = replyTo;
  }
}

static interface SessionEvent {}

public static final class SessionGranted implements SessionEvent {
  public final ActorRef<PostMessage> handle;

  public SessionGranted(ActorRef<PostMessage> handle) {
    this.handle = handle;
  }
}

public static final class SessionDenied implements SessionEvent {
  public final String reason;

  public SessionDenied(String reason) {
    this.reason = reason;
  }
}

public static final class MessagePosted implements SessionEvent {
  public final String screenName;
  public final String message;

  public MessagePosted(String screenName, String message) {
    this.screenName = screenName;
    this.message = message;
  }
}

static interface SessionCommand {}

public static final class PostMessage implements SessionCommand {
  public final String message;

  public PostMessage(String message) {
    this.message = message;
  }
}

private static final class NotifyClient implements SessionCommand {
  final MessagePosted message;

  NotifyClient(MessagePosted message) {
    this.message = message;
  }
}

 最初,客户端参与者只能访问一个actorref&gt这让他们迈出了第一步。一旦建立了客户端的会话,它就会得到一个包含句柄的SessionGranted消息,该句柄用于解锁下一个协议步骤,即发布消息。需要将PostMessage命令发送到这个表示已添加到聊天室的会话的特定地址。会话的另一个方面是客户端通过replyTo参数显示了它自己的地址,以便后续的messagepost事件可以发送给它。
 这说明了actor如何表达Java对象上的方法调用以外的内容。声明的消息类型及其内容描述了一个完整的协议,该协议可以涉及多个参与者,并且可以通过多个步骤进行演化。下面是聊天室协议的AbstractBehavior实现:

public class ChatRoom {
  private static final class PublishSessionMessage implements RoomCommand {
    public final String screenName;
    public final String message;

    public PublishSessionMessage(String screenName, String message) {
      this.screenName = screenName;
      this.message = message;
    }
  }

  public static Behavior<RoomCommand> create() {
    return Behaviors.setup(ChatRoomBehavior::new);
  }

  public static class ChatRoomBehavior extends AbstractBehavior<RoomCommand> {
    final List<ActorRef<SessionCommand>> sessions = new ArrayList<>();

    private ChatRoomBehavior(ActorContext<RoomCommand> context) {
      super(context);
    }

    @Override
    public Receive<RoomCommand> createReceive() {
      ReceiveBuilder<RoomCommand> builder = newReceiveBuilder();

      builder.onMessage(GetSession.class, this::onGetSession);
      builder.onMessage(PublishSessionMessage.class, this::onPublishSessionMessage);

      return builder.build();
    }

    private Behavior<RoomCommand> onGetSession(GetSession getSession)
        throws UnsupportedEncodingException {
      ActorRef<SessionEvent> client = getSession.replyTo;
      ActorRef<SessionCommand> ses =
          getContext()
              .spawn(
                  SessionBehavior.create(getContext().getSelf(), getSession.screenName, client),
                  URLEncoder.encode(getSession.screenName, StandardCharsets.UTF_8.name()));
      // narrow to only expose PostMessage
      client.tell(new SessionGranted(ses.narrow()));
      sessions.add(ses);
      return this;
    }

    private Behavior<RoomCommand> onPublishSessionMessage(PublishSessionMessage pub) {
      NotifyClient notification =
          new NotifyClient((new MessagePosted(pub.screenName, pub.message)));
      sessions.forEach(s -> s.tell(notification));
      return this;
    }
  }

  static class SessionBehavior extends AbstractBehavior<ChatRoom.SessionCommand> {
    private final ActorRef<RoomCommand> room;
    private final String screenName;
    private final ActorRef<SessionEvent> client;

    public static Behavior<ChatRoom.SessionCommand> create(
        ActorRef<RoomCommand> room, String screenName, ActorRef<SessionEvent> client) {
      return Behaviors.setup(context -> new SessionBehavior(context, room, screenName, client));
    }

    private SessionBehavior(
        ActorContext<ChatRoom.SessionCommand> context,
        ActorRef<RoomCommand> room,
        String screenName,
        ActorRef<SessionEvent> client) {
      super(context);
      this.room = room;
      this.screenName = screenName;
      this.client = client;
    }

    @Override
    public Receive<SessionCommand> createReceive() {
      return newReceiveBuilder()
          .onMessage(PostMessage.class, this::onPostMessage)
          .onMessage(NotifyClient.class, this::onNotifyClient)
          .build();
    }

    private Behavior<SessionCommand> onPostMessage(PostMessage post) {
      // from client, publish to others via the room
      room.tell(new PublishSessionMessage(screenName, post.message));
      return Behaviors.same();
    }

    private Behavior<SessionCommand> onNotifyClient(NotifyClient notification) {
      // published from the room
      client.tell(notification.message);
      return Behaviors.same();
    }
  }
}

 状态是通过类中的字段管理的,就像常规的面向对象类一样。由于状态是可变的,所以我们不会从消息逻辑返回不同的行为,但是可以返回AbstractBehavior实例本身(this)作为处理传入的下一个消息的行为。我们还可以返回Behavior。以同样的方式达到同样的目的。
 在这个示例中,我们创建了单独的语句来创建行为构建器,但是它也会从每一步返回构建器本身,因此也可以使用更流畅的行为定义样式。您应该选择的内容取决于参与者接受的消息集的大小。
 也可以返回一个新的不同的AbstractBehavior,例如代表一个不同的国家在一个有限状态机(FSM),或使用一个功能行为的工厂与功能结合面向对象风格的不同部分的生命周期相同的演员的行为。
 当新的GetSession命令传入时,我们将该客户端添加到当前会话列表中。然后,我们还需要创建会话的ActorRef,它将用于发布消息。在本例中,我们希望创建一个非常简单的Actor,它将PostMessage命令重新打包到PublishSessionMessage命令中,该命令还包含屏幕名。
 为了实现为会话生成子元素的逻辑,我们需要访问ActorContext。这是在创建行为时作为构造函数参数注入的,请注意我们是如何将AbstractBehavior和behavior结合在一起的。在创建工厂方法中执行此操作。
 我们在这里声明的行为可以处理RoomCommand的两种子类型。GetSession已经解释过了,来自会话参与者的PublishSessionMessage命令将触发包含的聊天室消息向所有连接的客户端传播。但是我们不想给PublishSessionMessage命令发送到任意客户的能力,我们保留权利内部会话参与者我们create-otherwise客户可能会带来完全不同的屏幕名称(想象一下GetSession协议进一步包括身份验证信息安全)。因此,PublishSessionMessage具有私有可视性,不能在聊天室之外创建。
如果我们不关心保护会话和屏幕名之间的通信,那么我们可以改变协议,如PostMessage被删除,所有客户端只获得一个actorref&publishsessionmessage>发送。在这种情况下,不需要会话actor,我们可以使用context.getSelf()。在这种情况下,类型检查是有效的,因为actorref>类型参数是相反的,这意味着我们可以使用一个actorref & RoomCommand>无论一个ActorRef< PublishSessionMessage>这是有道理的,因为前者比后者会说更多的语言。相反就会有问题,所以传递一个actorref&publishsessionmessage>在ActorRef< RoomCommand>会导致类型错误。
试试
 为了看到这个聊天室在行动,我们需要写一个客户端Actor,可以使用它:

public class Gabbler extends AbstractBehavior<ChatRoom.SessionEvent> {
  public static Behavior<ChatRoom.SessionEvent> create() {
    return Behaviors.setup(Gabbler::new);
  }

  private Gabbler(ActorContext<ChatRoom.SessionEvent> context) {
    super(context);
  }

  @Override
  public Receive<ChatRoom.SessionEvent> createReceive() {
    ReceiveBuilder<ChatRoom.SessionEvent> builder = newReceiveBuilder();
    return builder
        .onMessage(ChatRoom.SessionDenied.class, this::onSessionDenied)
        .onMessage(ChatRoom.SessionGranted.class, this::onSessionGranted)
        .onMessage(ChatRoom.MessagePosted.class, this::onMessagePosted)
        .build();
  }

  private Behavior<ChatRoom.SessionEvent> onSessionDenied(ChatRoom.SessionDenied message) {
    getContext().getLog().info("cannot start chat room session: {}", message.reason);
    return Behaviors.stopped();
  }

  private Behavior<ChatRoom.SessionEvent> onSessionGranted(ChatRoom.SessionGranted message) {
    message.handle.tell(new ChatRoom.PostMessage("Hello World!"));
    return Behaviors.same();
  }

  private Behavior<ChatRoom.SessionEvent> onMessagePosted(ChatRoom.MessagePosted message) {
    getContext()
        .getLog()
        .info("message has been posted by '{}': {}", message.screenName, message.message);
    return Behaviors.stopped();
  }
}

 现在要尝试一下,我们必须同时启动聊天室和gabbler当然我们是在Actor系统中进行的。既然只能有一个用户监护人,我们可以从聊天室的gabbler(这不是我们想要的——它使逻辑复杂化了)或者从聊天室的gabbler(这是荒谬的)或者我们从第三个角色开始他们两个——我们唯一明智的选择:

public class Main {
  public static Behavior<Void> create() {
    return Behaviors.setup(
        context -> {
          ActorRef<ChatRoom.RoomCommand> chatRoom = context.spawn(ChatRoom.create(), "chatRoom");
          ActorRef<ChatRoom.SessionEvent> gabbler = context.spawn(Gabbler.create(), "gabbler");
          context.watch(gabbler);
          chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler));

          return Behaviors.receive(Void.class)
              .onSignal(Terminated.class, sig -> Behaviors.stopped())
              .build();
        });
  }

  public static void main(String[] args) {
    ActorSystem.create(Main.create(), "ChatRoomDemo");
  }
}

 在良好的传统中,我们称主参与者为它是什么,它直接对应于传统Java应用程序中的主方法。这个Actor将自动执行它的工作,我们不需要从外部发送消息,所以我们将它声明为Void类型。参与者不仅接收外部消息,还会收到某些系统事件的通知,即所谓的信号。为了访问它们,我们选择使用receive behavior decorator来实现这个特定的操作。对于信号(Signal的子类)将调用提供的onSignal函数,对于用户消息将调用onMessage函数。
 这个特殊的主要参与者是使用behavior创建的。设置,它就像行为的工厂。与行为相反,行为实例的创建被推迟到参与者启动时。在actor运行之前立即创建行为实例的接收。设置中的工厂函数作为参数传递给ActorContext,例如,它可以用于生成子actor。这个主参与者创建聊天室和gabbler,并启动它们之间的会话,当gabbler结束时,我们将收到由于调用了context而终止的事件。观看。这允许我们关闭Actor系统:当主Actor终止时,就不再需要做什么了。
 因此,在创建具有主参与者行为的参与者系统之后,我们可以让主方法返回,ActorSystem将继续运行,JVM将保持活动状态,直到根参与者停止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值