从零开始搭建游戏服务器 第二节 Actor模型与应用

目录

  • 复习
  • 本节内容
  • 正文
    • 什么是Actor模型
    • 如何应用
    • 创建Actor基类
    • 创建RootActor
    • 创建AkkaContext
    • 创建ConnectActorManager和ConnectActor
    • 生成actor并发送消息给它
  • 课后作业
  • 结尾

复习

上一节我们使用gradle构建了一个多模块系统。
并且在登录服启动了Netty服务,监听config文件中配置的端口。
在client模块中使用Netty创建一个客户端连接到登录服,并且发送并接收协议。

本节内容

本节我们学习一下Actor模型。并将其应用在我们的登录服中。
我们将会为每一个连接上来的客户端生成一个actor专门用于处理该连接发送上来的请求数据。

正文

什么是Actor模型

Actor模型是一种强大的并发计算模型。
在Actor模型中,一个Actor是一个最基本的计算单元,它可以看作是一个个独立的实体,它们之间毫无关联,但是可以通过消息来通信。
一个Actor收到其他Actor的信息后,可以根据需要作出各种响应。每个Actor的数据相互隔离,使用消息传递的方式来进行并发操作,避免了多线程编程中常见的同步和共享内存问题,从而提高了程序的可靠性和可伸缩性。
使用Actor模型最好的一点在于,为Actor逻辑进行编码时,无需过多考虑多线程并发问题,因为它是通过消息驱动的且其内部数据只能由其本身进行修改,而每个Actor的消息处理是串行的,所以每个Actor内部的数据不会被并发修改。
出bug的情况较少,对新人友好,意味着可以多招新人程序员来开发以节约项目成本。

如何应用

要在java中使用Actor模型,可以使用akka的actor库进行开发。
Kilim是一个比akka更轻量化的actor库,但是社区规模相对较小。
本项目选择使用akka的actor库。

        implementation group: 'com.typesafe.akka', name: 'akka-actor-typed_3', version: '2.8.5'

创建Actor基类

因为我们项目中将会有多种不同功能的actor出现,我们先定一个actor基类用于规范所有actor。
创建BaseMsg和BaseActor两个类

/**
* 消息基类 所有Actor消息的基类
*/
public class BaseMsg implements Serializable {
}

/**
* Actor基类
*/
@Slf4j
public abstract class BaseActor extends AbstractBehavior<BaseMsg> {
    public BaseActor(ActorContext<BaseMsg> context) {
        super(context);
    }

    @Override
    public Receive<BaseMsg> createReceive() {
        ReceiveBuilder<BaseMsg> builder = newReceiveBuilder();
        builder.onMessage(BaseMsg.class, this::onBaseMsg);
        return builder.build();
    }

    private Behavior<BaseMsg> onBaseMsg(BaseMsg msg) {
        log.info("receive base msg. {}", msg.toString());
        return this;
    }
}

BaseActor继承与AbstractBehavior,它接收所有BaseMsg类型的消息,当收到BaseMsg消息时打印receive base msg.

创建RootActor

RootActor是一个用于初始化ActorSystem和创建其他Actor的守护Actor

public class RootActor extends BaseActor{
    public RootActor(ActorContext<BaseMsg> context) {
        super(context);
    }
    public static Behavior<BaseMsg> create() {
        return Behaviors.setup(RootActor::new);
    }
}

创建AkkaContext

AkkaContext用于储存ActorSystem上下文,并提供创建Actor的方法。

final public class AkkaContext {

    private static ActorSystem<BaseMsg> ACTOR_SYSTEM;

    public static void initActorSystem() {
        ACTOR_SYSTEM = ActorSystem.create(RootActor.create(), "ActorSystem");
    }

    public static ActorRef<BaseMsg> createActor(Behavior<BaseMsg> behavior, String name) {
        return ACTOR_SYSTEM.systemActorOf(behavior, name, Props.empty());
    }
}

服务器启动时调用initActorSystem初始化akka上下文。需要生成Actor则调用createActor。

创建ConnectActorManager和ConnectActor

在登录服中,我们会为每一个连接分配一个Actor,称之为ConnectActor,用于接收各个连接发送过来的消息,并串行地处理消息逻辑。
为了管理ConnectActor,我们需要一个ConnectActorManager用于存储每一个连接与connectActor的映射关系。

首先我们需要为每一个连接分配一个唯一id,称之为connectId。
修改LoginNettyHandler中负责处理建立连接逻辑的接口channelActive

	// 登录服ctx自定义属性
    private static final AttributeKey<HashMap<String, Object>> loginContextAttr = AttributeKey.valueOf("login");
	/**
     * 建立连接
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
        String ip = address.getAddress().getHostAddress();

        HashMap<String, Object> context = this.getContextAttrMap(ctx);
        if (context == null) {
            context = new HashMap<>();
            ctx.channel().attr(loginContextAttr).set(context);
        }
        // 先只用uuid的64位来做connectId,只要不超过64位理论上不会重复
        long connectId = UUID.randomUUID().getLeastSignificantBits();
        context.put("connectId", connectId);
        context.put("ip", ip);

        if (ctx.channel().isActive()) {
            log.info("创建连接—成功:ip = {},connectId = {}", ip, connectId);
        }
    }
    /**
     * 从ctx中获取自定义属性参数
     */
    private HashMap<String, Object> getContextAttrMap(ChannelHandlerContext ctx) {
        return ctx.channel().attr(loginContextAttr).get();
    }

我们为每一个连接设置了一个存放自定义属性的HashMap,然后使用UUID生成了一个connectId存入其中。

然后我们创建一个ConnectActor类,和RootActor类类似,继承BaseActor类,并实现create方法。

public class ConnectActor extends BaseActor {

    private long connectId;

    private ChannelHandlerContext ctx;

    public ConnectActor(ActorContext<BaseMsg> context, long connectId, ChannelHandlerContext ctx) {
        super(context);
        this.connectId = connectId;
        this.ctx = ctx;
    }

    /**
     * 生成一个ConnectActor的行为
     */
    public static Behavior<BaseMsg> create(long connectId, ChannelHandlerContext ctx) {
        return Behaviors.setup(context -> new ConnectActor(context, connectId, ctx));
    }
}

创建ConnectActorManager类,用于管理所有的ConnectActor。

@Component
public class ConnectActorManager {

    private final Map<Long, ActorRef<BaseMsg>> connectActorMap = new ConcurrentHashMap<>();

    public ActorRef<BaseMsg> getConnectActor(long connectId) {
        return connectActorMap.get(connectId);
    }

    public static ConnectActorManager getInstance() {
        return SpringUtils.getBean(ConnectActorManager.class);
    }
    /**
     * 创建一个ConnectActor
     */
    public ActorRef<BaseMsg> createConnectActor(long connectId, ChannelHandlerContext ctx) {
        ActorRef<BaseMsg> actor = AkkaContext.createActor(ConnectActor.create(connectId, ctx), String.valueOf(connectId));
        connectActorMap.put(connectId, actor);
        return actor;
    }
}

在ConnectActorManager类中,我们定义了一个ConcurrentHashMap,用于存放所有的ConnectActor,因为Netty接收数据是多线程的,所以可能有多个线程同时对其进行读写操作,所以使用ConcurrentHashMap保证线程安全。

生成actor并发送消息给它

我们修改LoginNettyHandler类,处理读取数据的接口channelRead0.

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {
        HashMap<String, Object> context = this.getContextAttrMap(ctx);
        long connectId = (long)context.get("connectId");
        ConnectActorManager actorManager = SpringUtils.getBean(ConnectActorManager.class);
        ActorRef<BaseMsg> connectActor = actorManager.getConnectActor(connectId);
        if (connectActor == null) {
            connectActor = actorManager.createConnectActor(connectId, ctx);
        }
        log.info(new String(msg));
        BaseMsg baseMsg = new BaseMsg();
        connectActor.tell(baseMsg);
        ctx.channel().writeAndFlush(msg);
    }

当接收到数据时,我们从ctx中获得建立连接时生成的connectId,再用其去ConnectActorManager中获取对应的connectActor,没有则创建一个新的。
然后我们创建一个消息BaseMsg,将其发送给connectActor进行处理。
最后我们将数据原封不动地返回回去。

运行一下试试。
运行结果
可以看到我们的服务器收到了test消息,并且给ConnectActor发送了一个BaseMsg消息。

课后作业

现在我们可以给ConnectActor发送消息了,是不是可以将BaseMsg细分成更多不一样的消息,用于处理ctx的不同类型的数据。
我预计为BaseMsg创建不同的子类,用来通知ConnectActor来做出不同的行为。
ClientUpMsg:客户端上行数据消息;
ConnectClosedMsg:客户端断开消息;

但是我不会在这里将代码展示出来,我希望读者们可以自己去实现这两个消息,只需要打印出不同的日志就行。

结尾

本节我们讲了Actor模型并且在实战中使用了它。
Actor模型是一个非常好用的并发计算模型,有了它可以使开发者不用过多关心并发问题和数据共享问题,只需要专注于业务逻辑的开发即可。
希望本节的内容能对你有所帮助,有任何问题可以评论或私信。
感兴趣的同学可以关注该专栏,我会持续更新更多关于游戏服务器开发的内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/462121.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

spacy进行简单的自然语言处理的学习

自然语言处理基本概念 概念&#xff1a;自然语言处理&#xff0c;是让机器理解人的语言的过程。 作用&#xff1a;通过使用自然语言处理&#xff0c;机器可以理解人的语言&#xff0c;从而进行语义分析&#xff0c;例如&#xff1a;从一句话中判断喜怒哀乐&#xff1b;从一段文…

lua脚本的基础内容

官方地址&#xff1a;http://luajit.org/ 官方wiki地址&#xff1a;http://wiki.luajit.org/Home 推荐书籍&#xff1a; OpenResty 最佳实践&#xff1a;https://moonbingbing.gitbooks.io/openresty-best-practices/content/ lua基础文档&#xff1a;https://www.runoob.com/l…

数据库-mysql安装

我们使用两种方式安装配置mysql数据库 一种采用无安装绿色版 一种采用官方提供的msi&#xff0c;windows安装版 亲测两种都可运行&#xff0c;有的电脑可能其中一种不能运行那可以尝试另外一种&#xff0c;有条件的同学可以试试docker版。 mysql安装 初次安装mysql之前建议大家…

消息队列思想学习(以及池化思想延展)

目录 消息队列的功能 消息中间件必备 池化思想以及弹性线程池的设计 弹性连接池 [核心参数&#xff1a;初始连接数&#xff0c;最大连接数&#xff0c;最大空闲时间] 弹性线程池 [核心参数&#xff1a;coreThreadCount, maxThreadCount] 引言&#xff1a;为啥要把消息队列…

JUC之Java对象内存布局

Java对象 对象在堆中的存储布局 它保存了什么 对象指向它的类元数据的指针&#xff0c;虚拟机通过这个指针来确定这个对象是哪个类的实例 对象头有多大&#xff1f;在64位系统中&#xff0c;Mark Word占了8个字节&#xff0c;类型指针占了8个字节&#xff0c;一共是16个字…

uniapp——第2篇:编写vue语法

前提&#xff0c;建议先学会前端几大基础&#xff1a;HTML、CSS、JS、Ajax&#xff0c;还有一定要会Vue!&#xff08;Vue2\Vue3&#xff09;都要会&#xff01;&#xff01;&#xff01;不然不好懂 一、去哪写&#xff1f; 就在【pages】的你的人一个页面文件夹里的【.vue】文…

中间件 | RPC - [Dubbo]

INDEX 1 Dubbo 与 web 容器的关系2 注册发现流程3 服务配置3.1 注册方式 & 订阅方式3.2 服务导出3.3 配置参数 4 底层技术4.1 Dubbo 的 spi 机制4.2 Dubbo 的线程池4.3 Dubbo 的负载均衡策略4.3 Dubbo 的协议 1 Dubbo 与 web 容器的关系 dubbo 本质上是一个 RPC 框架&…

计算机二级Python题目13

目录 1. 基本题 1.1 基本题1 1.2 基本题2 1.3 基本题3 2. turtle画图 3. 大题 3.1 大题1 3.2 大题2 1. 基本题 1.1 基本题1 lseval(input()) s"" for item in ls:if type(item)type("香山"):s item print(s) 1.2 基本题2 import random random.se…

使用tui-image-editor 图片编辑 标注图片

需求背景&#xff1a; 鼠标悬浮在图片上 出现编辑按钮 点击编辑 对该图片进行编辑&#xff08;输入文案、涂鸦、标记、裁剪等&#xff09; 可以体验一下它线上编辑器 Image-editor | TOAST UI :: Make Your Web Delicious! 使用 首先在你的前端项目中安装&#xff1a; np…

python-在图片上标实心圆点

代码&#xff1a; from PIL import Image, ImageDraw# 打开图像 image_path path_to_your_image.jpg image Image.open(image_path)# 创建一个可以在上面绘图的对象 draw ImageDraw.Draw(image)# 设置圆点的坐标和颜色 x 100 # 圆点的x坐标 y 100 # 圆点的y坐标 color …

【JVM】GCRoot

GC root原理 通过对枚举GCroot对象做引用可达性分析&#xff0c;即从GC root对象开始&#xff0c;向下搜索&#xff0c;形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用&#xff0c;没有形成引用链&#xff0c;那么该对象等待GC回收。 可以作为GC Roots的对…

Vue命令式组件的编写与应用

目录 1.引言 2.传统的组件 3.命令式组件 4.命令式组件的应用场景 1.引言 大家好&#xff01;今天我们来聊聊Vue.js中的一个有趣话题——命令式组件。你有没有觉得&#xff0c;有时候我们在Vue模板里写组件&#xff0c;就像是在玩搭积木&#xff0c;每个积木都有固定的形状…

第二百零六回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"给geolocator插件提交问题的结果"相关的内容&#xff0c;本章回中将介绍自定义标题栏.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

set与zset数据类型

set类型基础 redis集合(set)类型和list列表类型类似&#xff0c;都可以用来存储多个字符串元素的 集合。但是和list不同的是set集合当中不允许重复的元素。而且set集合当中元素是没有顺序的&#xff0c;不存在元素下标。 redis的set类型是使用哈希表构造的&#xff0c;因此复…

Java面向对象案例之描述专业和学生(4)

类的方法图 学生类&#xff1a; 属性&#xff1a;学号&#xff0c;姓名&#xff0c;年龄&#xff0c;所学习的专业方法&#xff1a;学习的方法&#xff0c;描述学习状态。描述内容包括姓名、学号、年龄、所学习的专业信息 专业类&#xff1a; 属性&#xff1a;专业编号&#xf…

阅读 - 二维码扫码登录原理

在日常生活中&#xff0c;二维码出现在很多场景&#xff0c;比如超市支付、系统登录、应用下载等等。了解二维码的原理&#xff0c;可以为技术人员在技术选型时提供新的思路。对于非技术人员呢&#xff0c;除了解惑&#xff0c;还可以引导他更好地辨别生活中遇到的各种二维码&a…

线性回归 quickstart

构建一元一次方程 100个&#xff08;X, y &#xff09;&#xff0c;大概是’y3x4’ import numpy as npnp.random.seed(42) # to make this code example reproducible m 100 # number of instances X 2 * np.random.rand(m, 1) # column vector y 4 3 * X np.random…

深度揭秘HW中的灰色技术(盗号,个人实时定位,远程监听)

xss漏洞&#xff1a;是数量最多的漏洞 cross-site scripting 跨站脚本攻击 1.反射型xss&#xff1a;代码存在于网址中&#xff0c;将存在恶意代码的链接发给用户&#xff0c;点击后才能攻击成功实施。 2.存储型xss&#xff1a;指Web应用程序会将用户输入的数据信息保持在服务…

2024批量下载微博内容导出excel,数据包含微博链接,内容,点赞数,转发数,评论数,话题等

以歌手李健这个号为例&#xff0c;共抓取727条微博&#xff0c;导出的excel微博数据包含微博链接,微博正文,原始图片链接,被转发微博原始图片链接,是否为原创微博,微博视频链接,发布位置,发布时间,发布工具,点赞数,转发数,评论数,话题等。 第一条微博发布于2010年5月31 。 再根…

电机参数辨识算法(2)——基于高频注入的磁链辨识策略

电机参数辨识算法&#xff08;1&#xff09;——基于高频注入的电感辨识策略-CSDN博客https://blog.csdn.net/m0_46903653/article/details/136722750?spm1001.2014.3001.5501上一期已经讲过了电感辨识方法。 今天这是参数辨识的第二期&#xff0c;今天来简单看看磁链的辨识。…