做色情網(wǎng)站需要種子搜索神器網(wǎng)頁版
目錄
- 前言
- 正文
- 添加依賴
- 安裝MongoDB
- 添加MongoDB相關(guān)配置
- 創(chuàng)建MongoContext類
- 嘗試初始化DB連接
- 實現(xiàn)注冊功能
- 測試注冊功能
- 實現(xiàn)登錄邏輯
- 測試登錄流程
- 結(jié)語
- 下節(jié)預(yù)告
前言
游戲服務(wù)器中, 很重要的一點就是如何保存玩家的游戲數(shù)據(jù).
當(dāng)一個服務(wù)端架構(gòu)趨于穩(wěn)定且功能全面, 開發(fā)者會發(fā)現(xiàn)服務(wù)端的業(yè)務(wù)開發(fā)基本就圍繞著CRUD來展開,
即業(yè)務(wù)數(shù)據(jù)的創(chuàng)建 \ 查找 \ 更新 \ 刪除.
本節(jié)內(nèi)容我們就將MongoDB作為持久化數(shù)據(jù)庫引入項目中.
正文
添加依賴
在build.gradle中添加依賴
implementation 'org.springframework.data:spring-data-mongodb:4.2.4'
implementation 'org.mongodb:mongodb-driver-sync:5.0.0'
安裝MongoDB
MongoDB的安裝步驟我就不一一解釋了,
讀者可以選擇使用docker快速創(chuàng)建一個MongoDB容器,
也可以使用MongoDB官方提供的免費(fèi)云數(shù)據(jù)庫進(jìn)行練習(xí) https://www.mongodb.com/cloud/atlas/register
也可以在本地運(yùn)行一個MongoDB進(jìn)程
添加MongoDB相關(guān)配置
在common模塊下添加common.conf配置文件以及CommonConfig類.
mongodb.host=mongodb://%s:%s@localhost:27017/%s?retryWrites=true&w=majority
mongodb.user=root
mongodb.password=123456
# 登錄服數(shù)據(jù)庫名
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;
}
創(chuàng)建MongoContext類
為了管理MongoDB連接, 創(chuàng)建一個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 數(shù)據(jù)庫。
而MongoTemplate是spring-data-mongodb庫的核心, 它基于MongoClient進(jìn)行了接口封裝, 提供了比 MongoClient 更豐富的功能,包括更簡潔的查詢構(gòu)建、更強(qiáng)大的映射支持。
在MongoContext外層封裝一層MongoService用來實現(xiàn)Mongo增刪改查相關(guān)接口。
@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;}/*** 插入數(shù)據(jù)*/public <T extends BaseCollection> boolean insert(T obj) {mongoContext.getMongoTemplate().insert(obj);return true;}/*** 查詢數(shù)據(jù)*/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 改
先實現(xiàn)了增查,以便我們后面實現(xiàn)賬號注冊登錄功能來舉例。
嘗試初始化DB連接
在LoginServer的initServer下面增加MongoContext的初始化代碼. 然后運(yùn)行.
@Overrideprotected void initServer() {LoginConfig config = SpringUtils.getBean(LoginConfig.class);// actor初始化AkkaContext.initActorSystem();// netty啟動NettyServer nettyServer = SpringUtils.getBean(NettyServer.class);nettyServer.start(config.getPort());// mongo服務(wù)啟動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得到結(jié)果:
實現(xiàn)注冊功能
上一節(jié)我們使用Protobuf創(chuàng)建了注冊協(xié)議, 從客戶端發(fā)送到了登錄服進(jìn)行解析.
接下來我們將注冊的賬號密碼進(jìn)行入庫以便后續(xù)取出使用.
我們先構(gòu)思一下一個賬號應(yīng)該有的數(shù)據(jù), 創(chuàng)建一個AccountCollection類, 用于映射Mongo數(shù)據(jù)庫中的AccountCollection表.
@Document
public class AccountCollection extends BaseCollection {@Idprivate long accountId;private String accountName;private String password;// getter & setter
}
很好理解, @Document注解表示該類是一個mongo的文檔映射類, @Id表示這個字段作為該文檔的主鍵.
BaseCollection目前就是一個空的Abstract類, 實現(xiàn)了Serializable接口.
public abstract class BaseCollection implements Serializable {
}
接下來修改ConnectActor中的onClientUpMsg方法, 該方法負(fù)責(zé)接收客戶端上行協(xié)議并進(jìn)行解包.
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) {// 注冊協(xié)議LoginProtoHandler.onPlayerRegisterMsg(this, PlayerMsg.C2SPlayerRegister.parseFrom(data));}return this;}
我增加了一個LoginProtoHandler類用于處理登錄相關(guān)的業(yè)務(wù)邏輯.
若是將所有的代碼都寫在ConnectActor, 將來這里的代碼會越來越長最終變得不可控.
使用單一職責(zé)的思想, 使ConnectActor只負(fù)責(zé)進(jìn)行協(xié)議的解包, 具體業(yè)務(wù)邏輯由各個功能模塊自己實現(xiàn), 將來游戲服我們也會這么處理, 這里只簡單提一嘴.
@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負(fù)責(zé)處理客戶端上行的關(guān)于賬號登錄相關(guān)的協(xié)議.
onPlayerRegisterMsg負(fù)責(zé)賬號注冊相關(guān)邏輯.
由于賬號Id的生成規(guī)則我還沒想好, 先用一個1L來進(jìn)行測試, 然后我們調(diào)用MongoService的insert方法, 將accountCollection寫入mongo. 并進(jìn)行回包.
修改ClientMain, 使我們在輸入"register"時, 發(fā)送注冊協(xié)議進(jìn)行賬號的注冊.
@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);}}
這里順便把登錄的協(xié)議一并附上, 沒啥技術(shù)含量就不多講.
測試注冊功能
啟動登錄服, 啟動客戶端, 客戶端控制臺輸入register.
看看登錄服的打印:
再看看mongo中數(shù)據(jù)是否寫入成功:
可以看到確實寫入了一條AccountCollection文檔, 字段_id為每個collection的主鍵, 他的數(shù)值也就是我們使用@Id注解標(biāo)識的字段, 另外_class是spring-data-mongo庫為我們添加的類名, 用于讀取數(shù)據(jù)時反序列化用. 需要注意的是如果你的AccountCollection類修改了包名或類名, 這里反序列化就會失敗, 需要額外添加處理.
實現(xiàn)登錄邏輯
登錄與注冊相差不大, 只是把添加數(shù)據(jù)修改為查找數(shù)據(jù).
修改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創(chuàng)建一個條件, 根據(jù)accountName來查找一條mongo中的文檔, 然后對比密碼是否一致, 來實現(xiàn)登錄流程.
修改ConnectActor使其對Login協(xié)議進(jìn)行解包并分發(fā)到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) {// 注冊協(xié)議LoginProtoHandler.onPlayerRegisterMsg(this, PlayerMsg.C2SPlayerRegister.parseFrom(data));} else if (decode.getCmdId() == ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE) {// 登錄協(xié)議LoginProtoHandler.onPlayerLoginMsg(this, PlayerMsg.C2SPlayerLogin.parseFrom(data));}return this;}
測試登錄流程
啟動登錄服, 啟動客戶端, 客戶端控制臺輸入login.
看看登錄服的打印:
結(jié)語
本節(jié)我們將MongoDB引入到項目中作為我們的持久化數(shù)據(jù)庫來使用, 并通過注冊登錄的兩個小例子, 來展示spring-data-mongo這個庫的用法.
我們只需要定好映射類, 便算是搭建好了一張表結(jié)構(gòu), 使用起來還是很簡單的, 當(dāng)然我們后面還會繼續(xù)對其封裝, 減少業(yè)務(wù)開發(fā)人員對MongoService的直接調(diào)用.
另外, 我們實現(xiàn)的注冊登錄例子十分粗糙, 其實只是做了一次mongo的讀寫, 對于游戲服務(wù)器來說, 注冊登錄功能是重中之重, 它維護(hù)著玩家的賬號安全, 同時也是我們整個游戲的入口.
對于賬號注冊, 我們還需要做 賬號id生成, 賬號名重復(fù)性檢測, accountId與accountName的緩存映射, 后期還有sdk接入等工作.
對于賬號登錄, 我們還需要做 登錄狀態(tài)修改, 多點登錄頂號, 分配游戲服 等工作.
這些我們后面會繼續(xù)優(yōu)化.
下節(jié)預(yù)告
下一節(jié)筆者將會引入redis作為游戲的緩存數(shù)據(jù)庫. 當(dāng)游戲玩家變多, 使用緩存數(shù)據(jù)庫可以大幅減小數(shù)據(jù)庫讀寫壓力. 同時redis的特性可以做很多事情, 比如我們可以用redis的incrby來做賬號的遞增而不怕多進(jìn)程中為玩家分配到同一個id; 還可以用作為分布式鎖來實現(xiàn)一些需要多進(jìn)程同時處理的業(yè)務(wù)功能.