从零开始搭建游戏服务器 第四节 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的初始化代码. 然后运行.

 @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; 还可以用作为分布式锁来实现一些需要多进程同时处理的业务功能.

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

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

相关文章

Redis部署方式(三)主从模式

在前面单机版的基础上&#xff0c;41为主&#xff0c;30为从。 一、主从搭建 1、主Redis安装 41机器redis主要配置 requirepass redis#!_41 bind 0.0.0.0 port 6379 daemonize yes 2、从redis安装 30机器redis主要配置 requirepass redis#!_30 bind 0.0.0.0 port 6380 da…

【SpringBoot3.x教程04】SpringBoot如何自定义starter

前言&#xff1a;什么是Starter POMs Starter POMs是预配置的依赖集合&#xff0c;旨在提供一种快速的方式来引入和管理Spring及相关技术栈的依赖。每个Starter POM都是针对特定的Spring模块或技术场景设计的。使用Starter POM&#xff0c;开发者只需要添加一个依赖项&#xff…

67、自定义通信帧协议解析

帧格式&#xff1a;方便自定义长度多种帧标识传输 格式规定 帧标识A 类型 备注 A<0x0F 短帧 数据长度1字节 A>0x0F 长帧 数据长度2字节 短帧:帧标识 帧标识取反 帧用户数据字节数 用户数据…用户数据 长帧:帧标识 帧标识取反 帧用户数据字节数(高8位) 帧用户数据字节数…

卸载应用无残留,App Cleaner Uninstaller Pro助你轻松管理Mac

App Cleaner & Uninstaller Pro是一款专为Mac用户设计的强大应用程序清理和卸载工具。这款软件拥有出色的卸载功能&#xff0c;能够彻底删除不再需要的应用程序及其相关文件和数据&#xff0c;确保Mac磁盘空间得到高效释放。同时&#xff0c;其强大的搜索功能可以快速找到与…

机器视觉系统选型-镜头参数

镜头参数&#xff1a; 光圈&#xff1a;光圈是一个用来控制镜头通光量的装置 &#xff0c;表示光圈大小我们是用光圈值&#xff08;F值&#xff09; &#xff0c;如F1.4&#xff0c;F2&#xff0c;F2.8 焦距&#xff08;Focus&#xff09;&#xff1a;透镜中心到其焦点的距离 景…

蓝桥杯刷题(十二)

1.答疑 代码 n int(input()) L [] for i in range(n):a,b,c map(int,input().split())A ab # 进入和答疑时间B abc # 个人总用时L.append([A,B]) L.sort(keylambda x:x[1]) # 个人总用时短的优先 ans tmp 0 # ans为发消息时刻&#xff0c;tmp为前一个人的总用时 for i …

C++ —— 类和对象(终)

目录 1. 日期类的实现 1.1 前置 和 后置 重载 1.2 >> 和 << 的重载 2. const 成员 3. 取地址及const取地址操作符重载 4. 再谈构造函数 4.1 构造函数体赋值 4.2 初始化列表 4.3 隐式类型转换 4.4 explict 关键字 5. static 成员 5.1 概念 5.2 特性 …

最新梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码下载

最新梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码下载 梨花带雨播放器基于thinkphp6开发的XPlayerHTML5网页播放器前台控制面板,支持多音乐平台音乐解析。二开内容:修复播放器接口问题,把接口本地化,但是集成外链播放器接口就不本地化了,我花钱找人写的理解下…

iOS图片占内存大小与什么有关?

1. 问&#xff1a;一张图片所占内存大小跟什么有关&#xff1f; 图片所占内存大小&#xff0c;与图片的宽高有关 我们平时看到的png、jpg、webp这些图片格式&#xff0c;其实都是图片压缩格式。通过对应的算法来优化了大小以节省网络传输与本地保存所需的资源。 但是当我们加…

vant4中如何修改Dialog弹框内容的字体大小

最近在开发一个移动端的需求&#xff0c;用的UI组件库是vant4 简单地总结一下&#xff0c;如何修改Dialog弹框内容的字体大小 我们先看一下Dialog弹框简单的使用 import { showConfirmDialog } from vant;showConfirmDialog({title: 标题,message:如果解决方法是丑陋的&#…

抖音视频无水印下载软件|视频批量提取工具

视频无水印下载软件 随着视频平台上涌现出越来越多精彩的视频内容&#xff0c;很多用户希望能够下载自己喜爱的视频进行保存或分享。为了满足用户需求&#xff0c;我们推出了专业的视频无水印下载软件&#xff0c;让您可以轻松快捷地获取喜欢的视频内容。以下是该软件的安装教…

Sealos 云开发:Laf 出嫁了,与 Sealos 正式结合!

千呼万唤始出来&#xff0c;Laf 云开发最近已正式与 Sealos 融合&#xff0c;入住 Sealos&#xff01;大家可以登录 Sealos 公有云 体验和使用&#xff0c;现在正式介绍一下 Sealos 云开发。 Sealos 云开发是什么&#xff1f; 如图&#xff0c;我们把 Laf 融合到 Sealos 中去了…

MySQL如何用phpMyAdmin创建定时任务事件来执行SQL语句删除_edit_lock和_edit_last?

前面跟大家分享了『WordPress如何批量删除wp_postmeta数据表无用的_edit_lock和_edit_last数据&#xff1f;』和『宝塔面板在计划任务中怎么执行SQL语句删除_edit_lock和_edit_last&#xff1f;』&#xff0c;但是有些站长并不是使用宝塔面板&#xff0c;那么我们如何时间定时删…

AIGC:让生成式AI成为自己的外脑

前言 在数字化浪潮席卷全球的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到了我们生活的方方面面。其中&#xff0c;生成式AI以其独特的魅力&#xff0c;正逐渐改变我们与世界的交互方式。AIGC&#xff08;人工智能生成内容&#xff09;作为生成式AI的重要应用…

灯塔:CSS笔记(4)

伪类选择器&#xff1a; 1.作用与优势&#xff1a; 1.作用&#xff1a;根据元素在HTML中的结构关系查找元素 2.优势&#xff1a;减少对于HTML中类的依赖&#xff0c;有利于保持代码的整洁 3.场景&#xff1a;常用于查找某父级选择器中的子元素 2.选择器 选择器说明E:first-c…

数据库的基本概念和安装MYSQL数据库

目录 一、数据库的发展 1、文件管理系统的缺点 2、数据库管理系统DBMS的优点 3、数据库管理系统&#xff08;DBMS&#xff09; 3.1DBMS的功能 3.2DBMS的工作模式 4、数据库系统的发展 5、数据库管理系统的架构 6、数据库管理系统分类 二、RDBMS关系型数据库的基本介绍…

软考 网工 每日学习打卡 2024/3/19

学习内容 第8章 网络安全 本章主要讲解网络安全方面的基础知识和应用技术。针对考试应该掌握诸如数据加密、报文认 证、数字签名等基本理论&#xff0c;在此基础上深入理解网络安全协议的工作原理&#xff0c;并能够针对具体的 网络系统设计和实现简单的安全解决方案。 本章共有…

Java学习笔记21——使用JDBC访问MySQL数据库

JDBC&#xff08;Java Database Connectivity&#xff0c;Java数据库连接&#xff09;是应用程序编程借口&#xff08;API&#xff09;&#xff0c;描述了一套访问关系数据库的标准Java类库。可以在程序中使用这些API&#xff0c;连接到关系数据库&#xff0c;执行SQL语句&…

由浅到深认识C语言(7):预处理二进制

该文章Github地址&#xff1a;https://github.com/AntonyCheng/c-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.csdn…

Oracle19C静默安装教程

一、安装前的准备 1、安装Linux操作系统 红帽Linux安装教程 2、配置网络源或者本地源 网络源&#xff1a;网络源配置方法 本地源&#xff1a;本地源配置方法 3、hosts文件配置 配置hostname&#xff1a; hostnamectl set-hostname p19c配置hosts文件&#xff1a; cat &…