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

news2024/11/29 2:40:38

这里写目录标题

  • 前言
  • 正文
    • 添加依赖
    • 安装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的初始化代码. 然后运行.

 @Override
    protected 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 {
    @Id
    private 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"时, 发送注册协议进行账号的注册.

	@Override
    protected 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.coloradmin.cn/o/1529990.html

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

相关文章

Spring单元测试+Mockito

一&#xff0c;背景 单元测试基本上是开发逃不过的一个工作内容&#xff0c;虽然往往因为过于无聊&#xff0c;或者过于麻烦&#xff0c;而停止于项目的迭代之中&#xff0c;不了了之了。其实不是开发们懒&#xff0c;而是上头要求的测试覆盖率高&#xff0c;但是又没有好用的…

zookeeper快速入门(合集)

zookeeper作为一个分布式协调框架&#xff0c;它的创建就是为了方便或者简化分布式应用的开发。除了服务注册与发现之外&#xff0c;它还能够提供更多的功能&#xff0c;但是对于入门来说&#xff0c;看这一篇就够了。后续会讲zookeeper的架构设计与原理&#xff0c;比如zookee…

MySQL 数据库设计范式

第一范式&#xff08;1NF&#xff09; 每一列都是不可分割的原子数据项第二范式&#xff08;2NF&#xff09; 在1NF的基础上&#xff0c;非码属性必须完全依赖于候选码(在1NF基础上消除非主属性对主码的部分函数依赖) 1.函数依赖A->B&#xff0c;如果通过A属性(属性组)的值…

[LeetBook]【学习日记】排序算法——归并排序

主要思想 归并排序是一种分治算法&#xff0c;其排序过程包括分和治分是指将要排序的序列一分为二、二分为四&#xff0c;直到单个序列中只有一个数治是指在分完后&#xff0c;将每两个元素重新组合&#xff0c;四合为二、二合为一&#xff0c;最终完成排序 图片作者&#xf…

python 实现把内层文件夹的文件,复制/剪切到外层文件夹

文章目录 如下图所示&#xff0c;收集了很多省市的文件&#xff0c;结果发现市一级的文件与区县一级的文件混在一起了。 接下来使用代码实现&#xff1a; 根据关键词识别出 市一级的文件&#xff1b;把市一级的文件&#xff0c;移动或者复制到省文件夹下&#xff1b;给出了py…

FPGA高端项目:FPGA基于GS2971+GS2972架构的SDI视频收发+GTX 8b/10b编解码SFP光口传输,提供2套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐本博主所有FPGA工程项目-->汇总目录本博已有的 SDI 编解码方案本方案的SDI接收发送本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收HLS图像缩放HLS多路视频拼接应用本方案的SDI…

前端静态开发案例-基于H5C3开发的仿照视频网站的前端静态页面-2 样式表部分和效果展示

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载 charset "utf-8"; /* 程序员田宝宝原创版权所有&#xff0c;仿冒必究&#xff0c;该界面是仿照某视频网站官网开发的静态页面 */ …

【Thread 线程】线程的方法与状态

SueWakeup 个人中心&#xff1a;SueWakeup 系列专栏&#xff1a;学习Java 个性签名&#xff1a;保留赤子之心也许是种幸运吧 本文封面由 凯楠&#x1f4f7; 友情赞助播出&#xff01; 目录 一个线程的生命周期 线程终止的原因 线程的方法 Thread 类的静态方法 1. 设置线程…

linux单机部署hadoop

1.下载安装包 https://archive.apache.org/dist/hadoop/common/ 2.上传压缩 3.修改配置文件 1)设置JDK的路径 cd /usr/local/software/hadoop-3.1.3/etc/hadoop vi hadoop-env.sh export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.402.b06-1.el7_9.x86_64/ 查看…

【数据结构和算法初阶(C语言)】二叉树的顺序结构--堆的实现/堆排序/topk问题详解---二叉树学习日记②1

目录 ​编辑 1.二叉树的顺序结构及实现 1.1 二叉树的顺序结构 2 堆的概念及结构 3 堆的实现 3.1堆的代码定义 3.2堆插入数据 3.3打印堆数据 3.4堆的数据的删除 3.5获取根部数据 3.6判断堆是否为空 3.7 堆的销毁 4.建堆以及堆排序 4.1堆排序---是一种选择排序 4.2升序建大堆&a…

微信小程序 canvas层级过高覆盖原生组件

一、背景 微信小程序中使用signature第三方插件完成签名效果&#xff0c;但真机调试时发现canvas层级过高遮挡了按钮 二、具体问题 问题原因&#xff1a;签名后点击按钮无法生效 问题代码&#xff1a; <template><view class"sign_page" v-cloak>&l…

nodejs基于vue超市信息管理系统flask-django-php

互联网的快速发展&#xff0c;使世界各地的各种组织的管理方式发生了根本性的变化&#xff0c;我国政府、企业等组织在上个世纪90年代就已开始考虑使用互联网来管理信息。由于以前的种种因素&#xff0c;比如网络的普及率不高&#xff0c;用户对它的认知度不够&#xff0c;以及…

计算机网络:数据交换方式

计算机网络&#xff1a;数据交换方式 电路交换分组交换报文交换传输对比 本博客介绍计算机之间数据交换的三种方式&#xff0c;分别是电路交换、分组交换以及报文交换。 电路交换 我们首先来看电路交换&#xff0c;在电话问世后不久&#xff0c;人们就发现要让所有的电话机都…

基于Spring Boot网络相册设计与实现

摘 要 网络相册设计与实现的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&am…

【OCR】OCR开源文字识别工具

在日常的工作中&#xff0c;例如自动化测试开展时&#xff0c;经常涉及到一些验证码识别、文本识别、图像识别的场景&#xff0c;市面上虽也有很多识别工具&#xff0c;但质量、准确性参差不齐。 今天给大家推荐一个开源OCR项目:Umi-OCR&#xff0c;功能很强大&#xff0c;而且…

jquery 列表框可以手动修改(调用接口修改)

类似于这种 直接上代码 列表框 <td>//目的主要是获取属性名的(要更改的属性名) 在下面juqery的这一行(var field $(thisobj).prev(input).attr(name);)有体现<input type"hidden" name"voyage" value"${M_PSI_PERIOD_INFO.port}">…

Vulnhub - Hacker_Kid

希望和各位大佬一起学习&#xff0c;如果文章内容有错请多多指正&#xff0c;谢谢&#xff01; 个人博客链接&#xff1a;CH4SER的个人BLOG – Welcome To Ch4sers Blog Hacker_Kid 靶机下载地址&#xff1a;https://www.vulnhub.com/entry/hacker-kid-101,719/ WP 参考&…

路由器里如何设置端口映射?

在互联网时代&#xff0c;我们经常需要将内部网络的服务暴露到公网以便其他人访问。直接将内部网络暴露在公网上存在一定的安全风险。为了解决这个问题&#xff0c;我们可以利用路由器里设置端口映射来实现将特定端口的访问请求转发到内部网络的特定设备上。 端口映射的原理 端…

机器人离散化阻抗控制

机器人离散化阻抗控制是一种控制策略&#xff0c;它结合了阻抗控制的思想与离散化方法&#xff0c;以实现对机器人运动与外力之间动态关系的精细调节。这种控制方法旨在使机器人在与环境交互时能够表现出期望的阻抗特性&#xff0c;从而实现对接触力和位置的精确控制。 在离散…

AnyGo for Mac最新激活版:位置模拟软件打破地域限制

AnyGo for Mac&#xff0c;一款专为Mac用户打造的位置模拟软件&#xff0c;让您能够轻松打破地域限制&#xff0c;畅享无限可能。 软件下载&#xff1a;AnyGo for Mac v7.0.0最新激活版 通过AnyGo&#xff0c;您可以随时随地模拟出任何地理位置&#xff0c;无论是国内热门景点还…