从零开始搭建游戏服务器 第四节 MongoDB引入并实现注册登录

目录

  • 前言
  • 正文
    • 添加依赖
    • 安装MongoDB
    • 添加MongoDB相关配置
    • 创建MongoContext类
    • 尝试初始化DB连接
    • 实现注册功能
    • 测试注册功能
    • 实现登录逻辑
    • 测试登录流程
  • 结语
  • 下节预告

前言

游戏服务器中, 很重要的一点就是如何保存玩家的游戏数据.
当一个服务端架构趋于稳定且功能全面, 开发者会发现服务端的业务开发基本就围绕着CRUD来展开,
即业务数据的创建 \ 查找 \ 更新 \ 删除.
本节内容我们就将MongoDB作为持久化数据库引入项目中.

正文

添加依赖

在build.gradle中添加依赖

implementation 'org.springframework.data:spring-data-mongodb:4.2.4'
implementation 'org.mongodb:mongodb-driver-sync:5.0.0'

安装MongoDB

MongoDB的安装步骤我就不一一解释了,
读者可以选择使用docker快速创建一个MongoDB容器,
也可以使用MongoDB官方提供的免费云数据库进行练习 https://www.mongodb.com/cloud/atlas/register
也可以在本地运行一个MongoDB进程

添加MongoDB相关配置

在common模块下添加common.conf配置文件以及CommonConfig类.

mongodb.host=mongodb://%s:%s@localhost:27017/%s?retryWrites=true&w=majority
mongodb.user=root
mongodb.password=123456
# 登录服数据库名
mongodb.login.db=login
@Getter
@Component
@PropertySource("classpath:common.conf")
public class CommonConfig {@Value("${mongodb.host}")String mongoHost;@Value("${mongodb.user}")String mongoUser;@Value("${mongodb.password}")String mongoPassword;@Value("${mongodb.login.db}")String loginDbName;
}

创建MongoContext类

为了管理MongoDB连接, 创建一个MongoContext类用于初始化mongodb连接与管理.

package org.common.mongo;
import ...
/*** Mongo上下文*/
@Slf4j
@Component
public class MongoContext {private MongoClient mongoClient;private MongoTemplate mongoTemplate;public void initMongoContext(String mongoUrl, String user, String password, String dbName) {String url = String.format(mongoUrl, user, password, dbName);log.info(url);MongoClientSettings.Builder settings = MongoClientSettings.builder();settings.applyConnectionString(new ConnectionString(url));MongoClient mongoClient = MongoClients.create(settings.build());mongoTemplate = new MongoTemplate(mongoClient, dbName);log.info("mongo server ok!");}}

其中MongoClient 是 mongodb-driver-sync库的核心, 用于直接连接和操作 MongoDB 数据库。
而MongoTemplate是spring-data-mongodb库的核心, 它基于MongoClient进行了接口封装, 提供了比 MongoClient 更丰富的功能,包括更简洁的查询构建、更强大的映射支持。

在MongoContext外层封装一层MongoService用来实现Mongo增删改查相关接口。

@Slf4j
@Component
public class MongoService {private MongoContext mongoContext;public void initMongoService(String mongoUrl, String user, String password, String dbName) {MongoContext mongoContext = new MongoContext();mongoContext.initMongoContext(mongoUrl, user, password, dbName);this.mongoContext = mongoContext;}public MongoContext getMongoContext() {return mongoContext;}/*** 插入数据*/public <T extends BaseCollection> boolean insert(T obj) {mongoContext.getMongoTemplate().insert(obj);return true;}/*** 查询数据*/public <T extends BaseCollection> BaseCollection findById(Object id, Class<T> clz) {T object = mongoContext.getMongoTemplate().findById(id, clz);return object;}public <T extends BaseCollection> T findOneByQuery(Criteria criteria, Class<T> clz) {Query query = Query.query(criteria);T object = mongoContext.getMongoTemplate().findOne(query, clz);return object;}//TODO 删//TODO 改

先实现了增查,以便我们后面实现账号注册登录功能来举例。

尝试初始化DB连接

在LoginServer的initServer下面增加MongoContext的初始化代码. 然后运行.

 @Overrideprotected void initServer() {LoginConfig config = SpringUtils.getBean(LoginConfig.class);// actor初始化AkkaContext.initActorSystem();// netty启动NettyServer nettyServer = SpringUtils.getBean(NettyServer.class);nettyServer.start(config.getPort());// mongo服务启动CommonConfig commonConfig = SpringUtils.getBean(CommonConfig.class);MongoService mongoService = SpringUtils.getBean(MongoService.class);mongoService.initMongoService(commonConfig.getMongoHost(), commonConfig.getMongoUser(), commonConfig.getMongoPassword(), commonConfig.getLoginDbName());log.info("LoginServer start!");}

启动LoginServer得到结果:
mongo连接成功

实现注册功能

上一节我们使用Protobuf创建了注册协议, 从客户端发送到了登录服进行解析.
接下来我们将注册的账号密码进行入库以便后续取出使用.

我们先构思一下一个账号应该有的数据, 创建一个AccountCollection类, 用于映射Mongo数据库中的AccountCollection表.

@Document
public class AccountCollection extends BaseCollection {@Idprivate long accountId;private String accountName;private String password;// getter & setter
}

很好理解, @Document注解表示该类是一个mongo的文档映射类, @Id表示这个字段作为该文档的主键.
BaseCollection目前就是一个空的Abstract类, 实现了Serializable接口.

public abstract class BaseCollection implements Serializable {
}

接下来修改ConnectActor中的onClientUpMsg方法, 该方法负责接收客户端上行协议并进行解包.

    private Behavior<BaseMsg> onClientUpMsg(ClientUpMsg msg) throws InvalidProtocolBufferException {Pack decode = PackCodec.decode(msg.getData());log.info("receive client up msg. cmdId = {}", decode.getCmdId());byte[] data = decode.getData();if (decode.getCmdId() == ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE) {// 注册协议LoginProtoHandler.onPlayerRegisterMsg(this, PlayerMsg.C2SPlayerRegister.parseFrom(data));}return this;}

我增加了一个LoginProtoHandler类用于处理登录相关的业务逻辑.
若是将所有的代码都写在ConnectActor, 将来这里的代码会越来越长最终变得不可控.
使用单一职责的思想, 使ConnectActor只负责进行协议的解包, 具体业务逻辑由各个功能模块自己实现, 将来游戏服我们也会这么处理, 这里只简单提一嘴.

@Slf4j
public class LoginProtoHandler {public static void onPlayerRegisterMsg(ConnectActor actor, PlayerMsg.C2SPlayerRegister up) {log.info("player register, accountName = {}, password = {}", up.getAccountName(), up.getPassword());long accountId = 1L;String accountName = up.getAccountName();String password = up.getPassword();AccountCollection accountCollection = new AccountCollection();accountCollection.setAccountId(accountId);accountCollection.setAccountName(accountName);accountCollection.setPassword(password);MongoService mongoService = SpringUtils.getBean(MongoService.class);boolean res = mongoService.insert(accountCollection);log.info("create account collection. accountId = {}, accountName = {}, res = {}", accountId, accountName, res);// 回包PlayerMsg.S2CPlayerRegister.Builder builder = PlayerMsg.S2CPlayerRegister.newBuilder();builder.setSuccess(res);byte[] down = PackCodec.encode(new Pack(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE, builder.build().toByteArray()));actor.getCtx().writeAndFlush(down);}}

LoginProtoHandler负责处理客户端上行的关于账号登录相关的协议.
onPlayerRegisterMsg负责账号注册相关逻辑.
由于账号Id的生成规则我还没想好, 先用一个1L来进行测试, 然后我们调用MongoService的insert方法, 将accountCollection写入mongo. 并进行回包.

修改ClientMain, 使我们在输入"register"时, 发送注册协议进行账号的注册.

	@Overrideprotected void handleBackGroundCmd(String cmd) {if (cmd.equals("test")) {channel.writeAndFlush("test".getBytes());} else if (cmd.equals("register")) {PlayerMsg.C2SPlayerRegister.Builder builder = PlayerMsg.C2SPlayerRegister.newBuilder();builder.setAccountName("clintAccount");builder.setPassword("123456");Pack pack = new Pack(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE, builder.build().toByteArray());byte[] data = PackCodec.encode(pack);channel.writeAndFlush(data);} else if (cmd.equals("login")) {PlayerMsg.C2SPlayerLogin.Builder builder = PlayerMsg.C2SPlayerLogin.newBuilder();builder.setAccountName("clintAccount");builder.setPassword("123456");Pack pack = new Pack(ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE, builder.build().toByteArray());byte[] data = PackCodec.encode(pack);channel.writeAndFlush(data);}}

这里顺便把登录的协议一并附上, 没啥技术含量就不多讲.

测试注册功能

启动登录服, 启动客户端, 客户端控制台输入register.
看看登录服的打印:
注册成功
再看看mongo中数据是否写入成功:
db数据
可以看到确实写入了一条AccountCollection文档, 字段_id为每个collection的主键, 他的数值也就是我们使用@Id注解标识的字段, 另外_class是spring-data-mongo库为我们添加的类名, 用于读取数据时反序列化用. 需要注意的是如果你的AccountCollection类修改了包名或类名, 这里反序列化就会失败, 需要额外添加处理.

实现登录逻辑

登录与注册相差不大, 只是把添加数据修改为查找数据.
修改LoginProtoHandler, 添加onPlayerLoginMsg

    public static void onPlayerLoginMsg(ConnectActor actor, PlayerMsg.C2SPlayerLogin up) {String accountName = up.getAccountName();String password = up.getPassword();MongoService mongoService = SpringUtils.getBean(MongoService.class);Criteria criteria = Criteria.where("accountName").is(accountName);AccountCollection accountCollection = mongoService.findOneByQuery(criteria, AccountCollection.class);PlayerMsg.S2CPlayerLogin.Builder builder = PlayerMsg.S2CPlayerLogin.newBuilder();if (accountCollection == null) {log.warn("login without account. accountName = {}", accountName);builder.setSuccess(false);} else if( !accountCollection.getPassword().equals(password) ) {log.warn("login password error. accountName = {}", accountName);builder.setSuccess(false);} else {log.info("login success. accountName = {}, accountId = {}", accountName, accountCollection.getAccountId());builder.setSuccess(true);builder.setAccountId(accountCollection.getAccountId());}byte[] down = PackCodec.encode(new Pack(ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE, builder.build().toByteArray()));actor.getCtx().writeAndFlush(down);}

这里我们使用Criteria创建一个条件, 根据accountName来查找一条mongo中的文档, 然后对比密码是否一致, 来实现登录流程.
修改ConnectActor使其对Login协议进行解包并分发到LoginProtoHandler中.

    private Behavior<BaseMsg> onClientUpMsg(ClientUpMsg msg) throws InvalidProtocolBufferException {Pack decode = PackCodec.decode(msg.getData());log.info("receive client up msg. cmdId = {}", decode.getCmdId());byte[] data = decode.getData();if (decode.getCmdId() == ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE) {// 注册协议LoginProtoHandler.onPlayerRegisterMsg(this, PlayerMsg.C2SPlayerRegister.parseFrom(data));} else if (decode.getCmdId() == ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE) {// 登录协议LoginProtoHandler.onPlayerLoginMsg(this, PlayerMsg.C2SPlayerLogin.parseFrom(data));}return this;}

测试登录流程

启动登录服, 启动客户端, 客户端控制台输入login.
看看登录服的打印:
登录成功

结语

本节我们将MongoDB引入到项目中作为我们的持久化数据库来使用, 并通过注册登录的两个小例子, 来展示spring-data-mongo这个库的用法.
我们只需要定好映射类, 便算是搭建好了一张表结构, 使用起来还是很简单的, 当然我们后面还会继续对其封装, 减少业务开发人员对MongoService的直接调用.
另外, 我们实现的注册登录例子十分粗糙, 其实只是做了一次mongo的读写, 对于游戏服务器来说, 注册登录功能是重中之重, 它维护着玩家的账号安全, 同时也是我们整个游戏的入口.
对于账号注册, 我们还需要做 账号id生成, 账号名重复性检测, accountId与accountName的缓存映射, 后期还有sdk接入等工作.
对于账号登录, 我们还需要做 登录状态修改, 多点登录顶号, 分配游戏服 等工作.
这些我们后面会继续优化.

下节预告

下一节笔者将会引入redis作为游戏的缓存数据库. 当游戏玩家变多, 使用缓存数据库可以大幅减小数据库读写压力. 同时redis的特性可以做很多事情, 比如我们可以用redis的incrby来做账号的递增而不怕多进程中为玩家分配到同一个id; 还可以用作为分布式锁来实现一些需要多进程同时处理的业务功能.

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【黄金手指】windows操作系统环境下使用jar命令行解压和打包Springboot项目jar包

一、背景 项目中利用maven将Springboot项目打包成生产环境jar包。名为 prod_2024_1.jar。 需求是 修改配置文件中的某些参数值&#xff0c;并重新发布。 二、解压 jar -xvf .\prod_2024_1.jar释义&#xff1a; 这段命令是用于解压缩名为"prod_2024_1.jar"的Java归…

Spring MVC入门(4)

请求 获取Cookie/Session 获取Cookie 传统方式: RequestMapping("/m11")public String method11(HttpServletRequest request, HttpServletResponse response) {//获取所有Cookie信息Cookie[] cookies request.getCookies();//打印Cookie信息StringBuilder build…

数据格式化方法

首先你需要一个可以展示代码的组件&#xff1b; 我使用的是tech-ui(内部组件库)&#xff1b; 你如果没有类似的组件&#xff0c;可以参考以下链接替代&#xff1a; react-monaco-editor -- 代码编辑器(适用Umi)_umi monaco editor-CSDN博客 Codemirror -- 代码编辑器(react…

鸿蒙Harmony应用开发—ArkTS-像素单位

ArkUI为开发者提供4种像素单位&#xff0c;框架采用vp为基准数据单位。 说明&#xff1a; 本模块首批接口从API version 7开始支持&#xff0c;后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 名称描述px屏幕物理像素单位。vp屏幕密度相关像素&#xff0c;…

【机器学习入门 】支持向量机

系列文章目录 第1章 专家系统 第2章 决策树 第3章 神经元和感知机 识别手写数字——感知机 第4章 线性回归 第5章 逻辑斯蒂回归和分类 前言 支持向量机(Support Vector Machine) 于1995年发表&#xff0c;由于其优越的性能和广泛的适用性&#xff0c;成为机器学习的主流技术&…

GJB 5000变更控制规程

1 目的及范围 本规程规定了配置项更改的相关管理规定&#xff0c;适用于整个项目生存周期中的配置项变更&#xff0c;包括项目计划、需求等技术文档变更。主要对受控后的配置项进行管理&#xff0c;处于开发库的配置项不受变更控制规程约束。 2 引用文件 a&#xff09; Q/A…

电脑显示丢失mfc140u.dll怎么办?五种可解决方法分享

DLL&#xff08;动态链接库&#xff09;是一种常见的文件类型&#xff0c;它包含了可以被多个程序共享的代码和数据。MFC140u.dll就是这样一种DLL文件&#xff0c;它的全称是Microsoft Foundation Class Library 14.0 Unicode Version。那么&#xff0c;mfc140u.dll是什么&…

升级 HarmonyOS 4 版本,腕上智慧更进一步

HUAWEI WATCH GT 3 系列升级 HarmonyOS 4 新版本后&#xff0c;手表体验更进一步&#xff0c;快来看看有哪些变化吧~

概念解析 | 现象揭秘:经验模态分解的奥秘

注1:本文系"概念解析"系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:经验模态分解(Empirical Mode Decomposition, EMD) 概念解析 | 现象揭秘:经验模态分解的奥秘 Decomposing Signal Using Empirical Mode Decomposition — Algorith…

【RabbitMQ | 第一篇】消息队列基础知识

文章目录 1.消息队列基础知识1.1什么是消息队列&#xff1f;1.2消息队列有什么用&#xff1f;&#xff08;结合项目说&#xff09;1.2.1异步处理1.2.2削峰/限流1.2.3降低系统耦合性1.2.4实现分布式事务 1.3消息队列的缺点1.4JMS和AMQP1.4.1 JMS的两种消息模型&#xff08;1&…

HTML5+CSS3小实例:具有悬停效果的3D闪耀动画

实例:具有悬停效果的3D闪耀动画 技术栈:HTML+CSS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, init…

思科无线控制器配置学习

文章目录 简单拓扑WLC配置 简单拓扑 WLC配置 WLC#show running-config Building configuration...Current configuration : 11943 bytes ! ! Last configuration change at 16:22:44 UTC Thu Mar 14 2024 by admin ! version 17.9 service timestamps debug datetime msec se…

设计模式 --4:工厂方法模式

总结 &#xff1a; 个人理解&#xff1a; 工厂方法模式就是在简单工程模式的基础下将工厂类抽象出来。如果不抽象工厂类 &#xff0c;每一次创建一个新的算法&#xff0c;都要修改原来的工厂类&#xff0c;这不符合 开放–封闭原则 将工厂类给抽象出来&#xff0c;让具体的算法…

LeetCode 面试经典150题 380.O(1)时间插入、删除和获取随机元素

题目&#xff1a; 实现RandomizedSet 类&#xff1a; RandomizedSet() 初始化 RandomizedSet 对象bool insert(int val) 当元素 val 不存在时&#xff0c;向集合中插入该项&#xff0c;并返回 true &#xff1b;否则&#xff0c;返回 false 。bool remove(int val) 当元素 va…

Chain of Note-CoN增强检索增强型语言模型的鲁棒性

Enhancing Robustness in Retrieval-Augmented Language Models 检索增强型语言模型&#xff08;RALMs&#xff09;在大型语言模型的能力上取得了重大进步&#xff0c;特别是在利用外部知识源减少事实性幻觉方面。然而&#xff0c;检索到的信息的可靠性并不总是有保证的。检索…

【算法刷题】Day34

文章目录 1. 最长递增子序列的个数题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 代码&#xff1a; 2. 最长数对链题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表…

001、Dynamo Python获取链接文件Document

前一次&#xff0c;我分享了一些关于 Parameter的探究&#xff0c;有读者留言&#xff0c;希望讲一些关于Dynamo中Python Script的教程&#xff0c;其实这部分&#xff0c;我也是新手&#xff0c;我也是不会了就百度&#xff0c;代码不在多&#xff0c;有用就行。 所以呢&…

【XR806开发板试用】使用PWM模块模拟手机呼吸灯提示功能

一般情况下&#xff0c;我们的手机在息屏状态&#xff0c;当收到消息处于未读状态时&#xff0c;会有呼吸灯提醒&#xff0c;这次有幸抽中XR806开发板的试用&#xff0c;经过九牛二虎之力终于将环境搞好了&#xff0c;中间遇到各种问题&#xff0c;在我的另一篇文章中已详细描述…

网络原理(4)——TCP协议的特性

目录 一、滑动窗口 1、ack丢了 2、数据丢了 二、流量控制&#xff08;流控&#xff09; 三、拥塞控制 拥塞窗口动态变化的规则 四、延时应答 五、捎带应答 六、面向字节流 七、异常情况 &#xff08;1&#xff09;进程崩溃了 &#xff08;2&#xff09;其中一方关机…

贪吃蛇(C语言超详细版)

目录 前言&#xff1a; 总览&#xff1a; API&#xff1a; 控制台程序&#xff08;Console&#xff09;&#xff1a; 设置坐标&#xff1a; COORD&#xff1a; GetStdHandle&#xff1a; STD_OUTPUT_HANDLE参数&#xff1a; SetConsoleCursorPosition&#xff1a; …