从零开始搭建游戏服务器 第二节 登录注册功能
目录
- 上节代码优化
- 接入mongo
- 导入依赖包
- 安装mongoDB
- 添加mongo相关配置
- 登录注册功能
- 思考
- 优化
- protobuf引入
- 协议分发注解
- 总结
上节代码优化
在上一节代码中,还有几个点可以进行优化。
-
ip地址和端口硬编码
我们把ip地址和端口从代码中剥离出来,单独记录在配置文件中,方便进行管理和修改。
在resources目录下新建一个game.conf文件
ip = "127.0.0.1"
port = 2333
在build.gradle中添加依赖
implementation group: 'com.typesafe', name: 'config', version: '1.4.2'
在GameBeanConfiguration类添加代码
@Bean(name = "gameConfig")Config getGameConfig() {Config config = ConfigFactory.parseFile(new File("src/main/resources/game.conf"));return config;}
修改GameMain类,修改startNetty(),传入参数int port, 获取gameConfig后拿到端口,传入startNetty方法中。
Config gameConfig = (Config) springContext.getBean("gameConfig");// 启动Netty服务器try {startNetty(gameConfig.getInt("port"));log.info("Netty server start!");} catch (InterruptedException e) {e.printStackTrace();}
运行一下,启动成功,优化完成。
接入mongo
导入依赖包
在build.gradle中添加代码,导入mongodb相关依赖包
implementation group: 'org.springframework.data', name: 'spring-data-mongodb', version: '2.2.12.RELEASE'implementation group: 'org.mongodb', name: 'mongo-java-driver', version: '3.12.2'
安装mongoDB
我这里有一台linux服务器,采用docker安装mongoDB。
docker pull mongo
docker run -itd --name mongo -p 27017:27017 mongo --auth
// 进入mongo修改用户名和密码
docker exec -it mongo mongosh admin
db.createUser({user:'admin',pwd:'admin',roles:[{role:'userAdminAnyDatabase',db:'admin'},"readWriteAnyDatabase"]});
// 测试用户添加成功
db.auth('admin', 'admin')
// 创建数据库game
use game
添加mongo相关配置
往game.conf中增加mongo相关配置
mongo.ip = "127.0.0.1"
mongo.port = 27017
mongo.user = "admin"
mongo.psw = "admin"
mongo.database = "game"
增加MongoDbContext类,以后的Mongo相关操作都由他来完成
@Slf4j
public class MongoDbContext {// mongo连接客户端private MongoClient mongoClient;// templateprivate MongoTemplate template;public MongoDbContext(MongoClient mongoClient, String databaseGame) {this.mongoClient = mongoClient;this.template = new MongoTemplate(mongoClient, databaseGame);log.info("MongoDb finish start!");}public MongoClient getMongoClient() {return mongoClient;}public MongoTemplate getTemplate() {return template;}
}
在GameBeanConfiguration中注册MongoDbContext
@Bean(name = "mongoDbContext")MongoDbContext getMongoDbContext() {Config gameConfig = getGameConfig();String mongoDbIp = gameConfig.getString("mongo.ip");int mongoDbPort = gameConfig.getInt("mongo.port");String mongoDbUser = gameConfig.getString("mongo.user");String mongoDbPsw = gameConfig.getString("mongo.psw");String mongoDbAdmin = gameConfig.getString("mongo.database.admin");String mongoDbGame = gameConfig.getString("mongo.database.game");String url = "mongodb://" + mongoDbUser + ":" + mongoDbPsw + "@" + mongoDbIp + ":" + mongoDbPort + "/" + mongoDbAdmin;MongoClientSettings.Builder mongoDbSettings = MongoClientSettings.builder();mongoDbSettings.writeConcern(WriteConcern.MAJORITY);mongoDbSettings.applyConnectionString(new ConnectionString(url));MongoClient mongoClient = MongoClients.create(mongoDbSettings.build());return new MongoDbContext(mongoClient, mongoDbGame);}
启动服务器,会发现控制台不停打印Mongo连接检测的信息,对我们进行debug查看控制台信息很不友好,因此我们再resource目录下添加logback.xml对日志打印逻辑进行修改。
${CONSOLE_STR} utf8
MongoDb配置完成,接下来我们开发登录注册功能。
为了方便管理,我们修改一下项目结构,如下:

项目基本框架类放入frame包中,再新建func用于存放游戏功能逻辑
登录注册功能
在func下添加新包,player用于管理玩家登录注册相关类,新建PlayerEntity作为存放玩家数据的Bean
public class PlayerEntity {// 用户名private String userName;// 密码private String password;public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}
新建PlayerProtoHandler用于处理客户端发上来的数据。我们先简单处理,暂时不用客户端上行用户名密码之类的数据。
@Slf4j
@Component
public class PlayerProtoHandler {@Autowiredprivate MongoDbContext mongoDbContext;public String login() {List all = mongoDbContext.getTemplate().findAll(PlayerEntity.class);log.info(String.valueOf(all));log.info("player login.");return "*success*";}public String register() {PlayerEntity entity = new PlayerEntity();entity.setUserName("111111");entity.setPassword("111111");mongoDbContext.getTemplate().insert(entity);log.info("player register finish.");return "success";}
}
在修改NettyMessageHandler,对客户端上行的数据进行分发
@Sharable
@Component
@Slf4j
public class NettyMessageHandler extends SimpleChannelInboundHandler
启动我们的客户端,分别输入register和login,可以在服务端控制台上得到结果,成功注册了名为111111的玩家。

思考
上面的开发过程其实很难受,因为我们双端都使用了String类型的交互协议,不仅要考虑双端如何约定我们的消息结构,还要对每一个消息进行手动添加逻辑分发。这大大增加了我们的代码开发量,使我们无法专注在具体功能逻辑的开发。
因此我们将会进行下面几个优化方案:
- 修改交互协议,由String修改为protobuf。
- 通过Spring注解,开发协议派发机制。
优化
protobuf引入
protobuf是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化和反序列化。
对我们来说,引入protobuf不仅规范了双端交互协议的制定,也减轻了我们编写协议序列化反序列化的代码量。
我们先下载protobuf编译器,我这里下载了22.1版本
https://github.com/protocolbuffers/protobuf/releases
我们在resources下添加目录proto用于存放我们的协议文件,将刚下载的protoc.exe移入其中,并创建一个player.proto协议文件
syntax = "proto3";
// 生成包名
option java_package = "com.wfgame.protobuf";
// 生成类型
option java_outer_classname = "PlayerProto";message Player {string playerName = 1;string password = 2;
}// 玩家登录
message C2SPlayerLogin {Player player = 1; // 登录玩家信息
}
message S2CPlayerLogin {bool success = 1; // 是否登录成功
}// 玩家注册
message C2SPlayerRegister {Player player = 1; // 登录玩家信息
}
message S2CPlayerRegister {bool success = 1; // 是否注册成功
}
创建一个协议号相关的proto,起名为ProtoEnum.proto
syntax = "proto3";option java_package = "com.wfgame.protobuf";
option java_outer_classname = "ProtoEnum";enum CmdIdEnum {NONE = 0;// 玩家相关协议 前缀 110PLAYER_LOGIN = 11001; // 玩家登录PLAYER_REGISTER = 11002; // 玩家注册
}
再创建一个protoc.bat批处理指令
@echo off
for %%f in (*.proto) do (protoc --proto_path=. --java_out="../../java" %%~nf%%~xfecho %%~nf%%~xf
)
pause
运行批处理命令,会发现在对应的包下生成了Player协议类

build.gradle引入依赖
implementation 'com.google.protobuf:protobuf-java:3.22.2'implementation 'com.google.protobuf:protobuf-java-util:3.22.2'
为了方便进行协议消息的分发处理,我们需要给每一条协议增加一条协议码,简称cmdId,为此我们不能直接使用Netty的protobuf编解码器,我需要要在每一条proto上再包装一层。
在frame包下创建ProtoPack类
public class ProtoPack {/*** 头长度 cmdId 4位*/private static final int LEN = 4;/*** 把byte[]转化成Pack*/public static Pack decode(byte[] data) {int it = 0;int cmd = readInt(data, 0);byte[] newData = Arrays.copyOfRange(data, it + LEN, data.length);return new Pack(cmd, newData);}/*** 把Pack转化成byte[]*/public static byte[] encode(Pack pack) {return encode(pack.getCmdId(), pack.getData());}public static byte[] encode(int cmdId, byte[] data) {byte[] res = new byte[data.length + LEN];int it = 0;writeInt(res, 0, cmdId);it += LEN;for (int i = 0; i < data.length; ++i) {res[it + i] = data[i];}return res;}/*** 从byte[]中获取cmdId*/public static int getCmdId(byte[] data) {return readInt(data, 0);}/*** byte[]从offset位置开始往后读一个int*/private static int readInt(byte[] bytes, int offset){int it = offset;return (bytes[it++] & 0xFF)+ ((bytes[it++] & 0xFF) << 8)+ ((bytes[it++] & 0xFF) << 16)+ ((bytes[it++] & 0xFF) << 24);}/*** byte[]从offset位置开始往后写一个int value*/private static void writeInt(byte[] bytes, int offset, int value) {int it = offset;bytes[it++] = (byte) (value & 0xFF);bytes[it++] = (byte) ((value >>> 8) & 0xFF);bytes[it++] = (byte) ((value >>> 16) & 0xFF);bytes[it++] = (byte) ((value >>> 24) & 0xFF);}public static final class Pack {// 协议号private int cmdId;// 协议内容private byte[] data;public Pack() {}public Pack(int cmdId, byte[] data) {this.cmdId = cmdId;this.data = data;}public int getCmdId() {return cmdId;}public void setCmdId(int cmdId) {this.cmdId = cmdId;}public byte[] getData() {return data;}public void setData(byte[] data) {this.data = data;}}}
由于我们不会永远是java → java的信息交互,我们应该能够支持与其他语言的客户端进行交互,因此我们将会使用byte[]进行数据发送与接受,并且在消息的开头加上我们消息的长度,方便进行编解码。
我们先修改客户端的代码
public static void login() throws IOException {PlayerProto.C2SPlayerLogin.Builder builder = PlayerProto.C2SPlayerLogin.newBuilder();PlayerProto.Player player = PlayerProto.Player.newBuilder().setPlayerName("111111").setPassword("111111").build();builder.setPlayer(player);ProtoPack.Pack pack = new ProtoPack.Pack(ProtoEnum.CmdIdEnum.PLAYER_LOGIN_VALUE, builder.build().toByteArray());byte[] encode = ProtoPack.encode(pack);byte[] bytes = addLength(encode);System.out.println(Arrays.toString(bytes));writer.write(bytes);writer.flush();}public static void register() throws IOException {PlayerProto.C2SPlayerRegister.Builder builder = PlayerProto.C2SPlayerRegister.newBuilder();PlayerProto.Player player = PlayerProto.Player.newBuilder().setPlayerName("111111").setPassword("111111").build();builder.setPlayer(player);ProtoPack.Pack pack = new ProtoPack.Pack(ProtoEnum.CmdIdEnum.PLAYER_REGISTER_VALUE, builder.build().toByteArray());byte[] encode = ProtoPack.encode(pack);byte[] bytes = addLength(encode);ProtoPack.writeInt(bytes, 0, bytes.length);writer.write(bytes);writer.flush();}/*** 添加长度到消息头*/public static byte[] addLength(byte[] bytes) {byte[] newBytes = new byte[bytes.length + 4];ProtoPack.writeInt(newBytes, 0, bytes.length);byte[] lengthBytes = toUnsignedByteArray(bytes.length);for (int i = 0; i < lengthBytes.length; i++) {newBytes[i] = lengthBytes[i];}for (int i = 0; i < bytes.length; i++) {newBytes[i + 4] = bytes[i];}return newBytes;}/*** 获取无符号整型对应的byte[]*/public static byte[] toUnsignedByteArray(int value) {byte[] result = new byte[4];result[0] = (byte) (value >>> 24);result[1] = (byte) (value >>> 16);result[2] = (byte) (value >>> 8);result[3] = (byte) value;return result;}
然后修改服务器的代码。
我们需要自己创建一个编码解码器,用于替换之前的StringDecoder和StringEncoder。
解码器MyDecoder,仅仅是封装了一下Netty自带的长度解码器
public class MyDecoder extends LengthFieldBasedFrameDecoder {public MyDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);}@Overrideprotected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {return super.decode(ctx, in);}
}
编码器MyEncoder,用于把ByteBuf写入byte[]
public class MyEncoder extends MessageToByteEncoder {@Overrideprotected void encode(ChannelHandlerContext ctx, byte[] sendBytes, ByteBuf out) throws Exception {out.writeBytes(sendBytes);}
}
修改GameMain,替换原本的编解码器
bootstrap.childHandler(new ChannelInitializer() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline cp = ch.pipeline();cp.addLast(new MyDecoder(1024 * 1024, 0, 4, 0, 4));//基于长度的解码器:1024*1024, 0, 4, 0, 4cp.addLast(new LengthFieldPrepender(4));//MyEncoder编码后再往开头插入4个字节的消息长度cp.addLast(new MyEncoder());//ByteBuf-->byte[]cp.addLast("handler", handler);}});
修改NettyMessageHandler,我们将从ByteBuf中读取数据,然后根据协议号进行逻辑分发。
@Sharable
@Component
@Slf4j
public class NettyMessageHandler extends SimpleChannelInboundHandler
修改PlayerProtoHandler,使其通过传入的Protobuf协议来进行登录与注册
@Slf4j
@Component
public class PlayerProtoHandler {@Autowiredprivate MongoDbContext mongoDbContext;public ProtoPack.Pack login(PlayerProto.C2SPlayerLogin request) {PlayerProto.Player player = request.getPlayer();log.info("player login userName={}, password={}", player.getPlayerName(), player.getPassword());Query query = new Query();query.addCriteria(Criteria.where("userName").is(player.getPlayerName()));PlayerEntity one = mongoDbContext.getTemplate().findOne(query, PlayerEntity.class);boolean res = true;if (one == null || !one.getPassword().equals(player.getPassword())) {res = false; // 账号密码错误}return new ProtoPack.Pack(ProtoEnum.CmdIdEnum.PLAYER_LOGIN_VALUE, PlayerProto.S2CPlayerLogin.newBuilder().setSuccess(res).build().toByteArray());}public ProtoPack.Pack register(PlayerProto.C2SPlayerRegister request) {PlayerProto.Player player = request.getPlayer();Query query = new Query();query.addCriteria(Criteria.where("userName").is(player.getPlayerName()));PlayerEntity one = mongoDbContext.getTemplate().findOne(query, PlayerEntity.class);boolean res = false;if (one == null) {PlayerEntity entity = new PlayerEntity();entity.setUserName(player.getPlayerName());entity.setPassword(player.getPassword());mongoDbContext.getTemplate().insert(entity);log.info("player register finish.");res = true;}return new ProtoPack.Pack(ProtoEnum.CmdIdEnum.PLAYER_REGISTER_VALUE, PlayerProto.S2CPlayerRegister.newBuilder().setSuccess(res).build().toByteArray());}
}
修改完毕,测试一下。启动客户端输入login得到如下信息

优化完成。
总结一下这里我们做了什么:
- 接入了Protobuf做协议序列化和反序列化。
- 引入了协议号的概念,方便后面做协议逻辑分发。
- 包装了协议,在协议头增加了消息的长度以及协议号。
协议分发注解
为了方便后续开发,我们可以通过注解+协议号进行功能的分发。有点类似@Controller注解和@request注解。
我们新建注解类@CMD
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CMD {int value(); // 协议号
}
修改PlayerProtoHandler.java,在login和register方法上添加CMD注解
/*** 登录*/@CMD(ProtoEnum.CmdIdEnum.PLAYER_LOGIN_VALUE)public ProtoPack.Pack login(PlayerProto.C2SPlayerLogin request) {...}/*** 注册*/@CMD(ProtoEnum.CmdIdEnum.PLAYER_REGISTER_VALUE)public ProtoPack.Pack register(PlayerProto.C2SPlayerRegister request) {...}
先写一个扫描包下所有Class的工具类ClassPathScanner.java
@Slf4j
public class ClassPathScanner {/*** 扫描包** @param basePackage 基础包* @param recursive 是否递归搜索子包* @param excludeInner 是否排除内部类 true->是 false->否* @param checkInOrEx 过滤规则适用情况 true—>搜索符合规则的 false->排除符合规则的* @param classFilterStrs List 自定义过滤规则,如果是null或者空,即全部符合不过滤* @return Set*/public static Set scan(String basePackage, boolean recursive, boolean excludeInner, boolean checkInOrEx,List classFilterStrs) {Set classes = new LinkedHashSet<>();String packageName = basePackage;List classFilters = toClassFilters(classFilterStrs);if (packageName.endsWith(".")) {packageName = packageName.substring(0, packageName.lastIndexOf('.'));}String package2Path = packageName.replace('.', '/');Enumeration dirs;try {dirs = Thread.currentThread().getContextClassLoader().getResources(package2Path);while (dirs.hasMoreElements()) {URL url = dirs.nextElement();String protocol = url.getProtocol();if ("file".equals(protocol)) {log.debug("扫描file类型的class文件....");String filePath = URLDecoder.decode(url.getFile(), "UTF-8");doScanPackageClassesByFile(classes, packageName, filePath,recursive, excludeInner, checkInOrEx, classFilters);} else if ("jar".equals(protocol)) {log.debug("扫描jar文件中的类....");doScanPackageClassesByJar(packageName, url, recursive,classes, excludeInner, checkInOrEx, classFilters);}}} catch (IOException e) {log.error("IOException error:", e);}return classes;}/*** 以jar的方式扫描包下的所有Class文件*/private static void doScanPackageClassesByJar(String basePackage, URL url,final boolean recursive, Set classes, boolean excludeInner, boolean checkInOrEx, List classFilters) {String packageName = basePackage;String package2Path = packageName.replace('.', '/');JarFile jar;try {jar = ((JarURLConnection) url.openConnection()).getJarFile();Enumeration entries = jar.entries();while (entries.hasMoreElements()) {JarEntry entry = entries.nextElement();String name = entry.getName();if (!name.startsWith(package2Path) || entry.isDirectory()) {continue;}// 判断是否递归搜索子包if (!recursive&& name.lastIndexOf('/') != package2Path.length()) {continue;}// 判断是否过滤 inner classif (excludeInner && name.indexOf('$') != -1) {log.debug("exclude inner class with name:" + name);continue;}String classSimpleName = name.substring(name.lastIndexOf('/') + 1);// 判定是否符合过滤条件if (filterClassName(classSimpleName, checkInOrEx, classFilters)) {String className = name.replace('/', '.');className = className.substring(0, className.length() - 6);try {classes.add(Thread.currentThread().getContextClassLoader().loadClass(className));} catch (ClassNotFoundException e) {log.error("Class.forName error:", e);}}}} catch (IOException e) {log.error("IOException error:", e);}}/*** 以文件的方式扫描包下的所有Class文件*/private static void doScanPackageClassesByFile(Set classes,String packageName, String packagePath, boolean recursive, final boolean excludeInner, final boolean checkInOrEx, final List classFilters) {File dir = new File(packagePath);if (!dir.exists() || !dir.isDirectory()) {return;}final boolean fileRecursive = recursive;File[] dirfiles = dir.listFiles(new FileFilter() {// 自定义文件过滤规则@Overridepublic boolean accept(File file) {if (file.isDirectory()) {return fileRecursive;}String filename = file.getName();if (excludeInner && filename.indexOf('$') != -1) {log.debug("exclude inner class with name:" + filename);return false;}return filterClassName(filename, checkInOrEx, classFilters);}});for (File file : dirfiles) {if (file.isDirectory()) {doScanPackageClassesByFile(classes,packageName + "." + file.getName(),file.getAbsolutePath(), recursive, excludeInner, checkInOrEx, classFilters);} else {String className = file.getName().substring(0,file.getName().length() - 6);try {classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));} catch (ClassNotFoundException e) {log.error("IOException error:", e);}}}}/*** 根据过滤规则判断类名*/private static boolean filterClassName(String className, boolean checkInOrEx, List classFilters) {if (!className.endsWith(".class")) {return false;}if (null == classFilters || classFilters.isEmpty()) {return true;}String tmpName = className.substring(0, className.length() - 6);boolean flag = false;for (Pattern p : classFilters) {if (p.matcher(tmpName).find()) {flag = true;break;}}return (checkInOrEx && flag) || (!checkInOrEx && !flag);}/*** @param pClassFilters the classFilters to set*/private static List toClassFilters(List pClassFilters) {List classFilters = new ArrayList();if (pClassFilters != null) {for (String s : pClassFilters) {String reg = "^" + s.replace("*", ".*") + "$";Pattern p = Pattern.compile(reg);classFilters.add(p);}}return classFilters;}}
添加ProtoDispatch类,用于做消息的分发
@Slf4j
@Component
public class ProtoDispatcher {/**指令缓存*/private Map commanders = new HashMap<>();private static class Commander {private final Object o;private final Method method;private final Method protobufParser;public Commander(Object o, Method method, int cmdId) throws NoSuchMethodException {this.o = o;this.method = method;// 协议数据应该转成什么类型Class paramType = method.getParameterTypes()[0];this.protobufParser = paramType.getMethod("parseFrom", byte[].class);}}/*** 加载注解*/public void load(ApplicationContext springContext, Collection classes) {Map newCommanders = new HashMap<>();String err = null;for (Class cls : classes) {try {Object o = springContext.getBean(cls);Method[] methods = cls.getDeclaredMethods();for (Method method : methods) {CMD cmd = method.getAnnotation(CMD.class);if(cmd != null) {if (newCommanders.get(cmd.value()) != null) {err = "协议加载异常:协议号重复 cmd.id = " + cmd.value();log.error(err);}try {newCommanders.put(cmd.value(), new Commander(o, method, cmd.value()));} catch (Exception e) {log.error("协议[" + cls + "]加载出错!!!,cmd=" + cmd.value() + ", method=" + method.getName(), e);throw new RuntimeException(e);}}}} catch (Exception e) {log.error("协议[" + cls + "]加载出错!!!", e);throw new RuntimeException(e);}}//协议号重复直接报错if(err != null){throw new RuntimeException(err);}commanders = newCommanders;}/*** 通过反射调用对应的协议处理器*/public ProtoPack.Pack invoke(int cmd, byte[] bytes) throws InvocationTargetException, IllegalAccessException {Commander commander = commanders.get(cmd);if (commander != null) {long begin = System.currentTimeMillis();GeneratedMessageV3 params = (GeneratedMessageV3) commander.protobufParser.invoke(null, bytes);ProtoPack.Pack res = (ProtoPack.Pack) commander.method.invoke(commander.o, params);long used = System.currentTimeMillis() - begin;log.debug("协议[{}]处理完成,耗时{}ms", cmd, used);return res;} else {throw new RuntimeException("协议号不存在 cmd=" + cmd);}}
}
在GameMain中添加代码,在服务器启动的时候对分发器进行初始化
public static void main(String[] args) {// 初始化Spring...// 启动Netty服务器...// 初始化协议分发器initProtoDispatcher(springContext);log.info("server start!");...//停掉虚拟机System.exit(0);}/*** 初始化协议分发器*/private static void initProtoDispatcher(ApplicationContext springContext) {ProtoDispatcher dispatcher = springContext.getBean(ProtoDispatcher.class);Set protoHandlerClasses = new HashSet<>();Set classes = ClassPathScanner.scan("com.wfgame.func", true, true, false, null);for (Class aClass : classes) {if (aClass.getSuperclass() == BaseProtoHandler.class) {protoHandlerClasses.add(aClass);}}dispatcher.load(springContext, protoHandlerClasses);}
修改NettyMessageHandler.java,将其中我们手动进行消息分发的部分代码进行修改
@AutowiredProtoDispatcher dispatcher;private void doRead(ChannelHandlerContext ctx, Object msg) throws InvalidProtocolBufferException, InvocationTargetException, IllegalAccessException {ByteBuf in = (ByteBuf) msg;byte[] bytes = new byte[in.readableBytes()];in.readBytes(bytes);int cmdId = ProtoPack.getCmdId(bytes);// 协议分发ProtoPack.Pack response = dispatcher.invoke(cmdId, ProtoPack.decode(bytes).getData());ctx.writeAndFlush(response);}
进行测试,能正常进行初始化和消息分发。
总结
本节一共做了这么几件事:
- 引入了game.conf,对游戏的配置做了统一管理。
- 在项目中接入了mongoDb数据库,持久化这一块做了初步的接入。
- 引入了logback.xml,后续可以对项目的日志这一块做更多的优化。
- 接入了Google的ProtoBuffer,方便后续我们进行协议的开发。
- 引入了协议号概念,并且针对协议号做了注解,进行客户端上行协议到具体模块的分发。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
