网站系统修改,wordpress海外建站,做一个免费网站的流程,山东泰山队深圳队自定义注解 前言正文创建注解创建类扫描工具创建ProtoDispatcher类初始化Dispatcher协议的逻辑分发dispatcher使用注解标记方法测试 结语 前言
在前面几节我们将Login服的大体架构搭建了起来#xff0c; 具体流程是这样的#xff1a;
客户端上传protobuf协议到LoginServerL… 自定义注解 前言正文创建注解创建类扫描工具创建ProtoDispatcher类初始化Dispatcher协议的逻辑分发dispatcher使用注解标记方法测试 结语 前言
在前面几节我们将Login服的大体架构搭建了起来 具体流程是这样的
客户端上传protobuf协议到LoginServerLoginServer的NettyServer接收数据将数据发送到ConnectActorConnectActor根据协议号对不同的协议使用不同的Protobuf类解包然后调用不同的方法。
当我们收到不同的协议号我们添加了不同的if判断条件来反序列化协议再根据不同的协议号调用不同的方法。 当我们的业务逻辑越发复杂协议越来越多就会导致if分支变多不用很多时间这个类就会变得又臭又长且多人开发时会有代码提交冲突的问题。 为了解决这个问题我们需要有分而治之的思想。使用自定义注解反射可以将这部分工作变得简单且无脑。
正文
本节我们的目标是创建一个协议分发类里面存放一张映射表将协议号与对应的方法记录在里面。 当收到一条协议便根据协议号找到对应的Method。 再根据Method获取第二个参数的类型我们默认第一个参数为玩家数据第二个参数为客户端上行的protobuf数据。获得参数类型就可以使用protobuf进行反序列化。 最后通过反射的方式进行方法调用。
接下来看笔者一步步实现。
创建注解
在common下添加dispatch包创建CMD注解
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface CMD {// 协议号 ProtoEnumMsg.CMD.IDint value();
}RetentionPolicy.RUNTIME 表示运行时也需要用到该注解我们会在代码中扫描使用了该注解描述的方法。 ElementType.METHOD 表示它用于描述方法。 int value(); 用于存放协议号起名叫value方便我们后面写注解时可以不用写属性名。
创建类扫描工具
为了扫描出使用该注解描述的方法我们需要扫描所有的类。 在utils目录下创建ClassScannerUtil
/*** 类扫描工具*/
public class ClassScannerUtils {public static SetClass? getClasses(String packageName) throws IOException, URISyntaxException, ClassNotFoundException {ClassLoader classLoader Thread.currentThread().getContextClassLoader();assert classLoader ! null;String path packageName.replace(., /);EnumerationURL resources classLoader.getResources(path);ListFile directories new ArrayList();while (resources.hasMoreElements()) {URL resource resources.nextElement();directories.add(new File(resource.toURI()));}SetClass? classes new HashSet();for (File directory : directories) {classes.addAll(findClasses(directory, packageName));}return classes;}private static ListClass? findClasses(File directory, String packageName) throws ClassNotFoundException {ListClass? classes new ArrayList();if (!directory.exists()) {return classes;}File[] files directory.listFiles();if (files null) {return classes;}for (File file : files) {if (file.isDirectory()) {assert !file.getName().contains(.);classes.addAll(findClasses(file, packageName . file.getName()));} else if (file.getName().endsWith(.class)) {classes.add(Class.forName(packageName . file.getName().substring(0, file.getName().length() - 6)));}}return classes;}}
逻辑比较简单传入一个包名遍历获取该目录下的所有.class结尾的文件。
创建ProtoDispatcher类
Slf4j
Component
public class ProtoDispatcher {private final MapInteger, ProtoWorker workerMap new HashMap();/*** 载入分发数据*/public void load(SetClass? classes) throws NoSuchMethodException {for (Class? clz : classes) {if (clz.getSuperclass() ! BaseProtoHandler.class) {continue;}Object protoHandler SpringUtils.getBean(clz);Method[] methods clz.getDeclaredMethods();for (Method method : methods) {CMD annotation method.getAnnotation(CMD.class);if (annotation null) {continue;}int cmdId annotation.value();if (workerMap.containsKey(cmdId)) {// 出现重复cmdIdString err cmdId cmdId is duplicate.;throw new RuntimeException(err);}workerMap.put(cmdId, new ProtoWorker(cmdId, protoHandler, method));}}}/*** 分发协议* param cmdId 协议号* param data 协议内容* param obj 玩家数据* return 要返回给客户端的Pack*/public Pack dispatch(int cmdId, byte[] data, Object obj) throws InvocationTargetException, IllegalAccessException {ProtoWorker protoWorker workerMap.get(cmdId);if (protoWorker null) {log.warn(not find proto worker. cmdId{}, cmdId);return null;}long startTime System.currentTimeMillis();GeneratedMessageV3 protoMsg (GeneratedMessageV3) protoWorker.getProtobufDecode().invoke(null, data);Pack pack (Pack) protoWorker.getMethod().invoke(protoWorker.getHandler(), obj, protoMsg);long usedTime System.currentTimeMillis() - startTime;if (usedTime 1000L) { // 协议处理太久log.warn(proto worker slowly. cmdId {}, used {}, cmdId,usedTime);}return pack;}}
load方法传入我们扫描出来的类筛选出继承于BaseProtoHandler的类它会将每个类中使用CMD注解描述的方法提取出来存入workerMap中。
BaseProtoHandler是个abstract类他里面没有任何逻辑用于管理所有协议接受处理类。
package org.common.handler;
/*** 协议处理基类*/
public abstract class BaseProtoHandler {
}当有协议进入调用dispatch会自动将byte[] data按照对应处理方法的第二个参数类型进行反序列化。具体看worker代码 /*** 协议处理方法*/
public class ProtoWorker {// 协议idprivate final int cmdId;// 协议处理类的对象private final Object handler;// 协议处理的方法private final Method method;// protobuf解析方法private final Method protobufDecode;public ProtoWorker(int cmdId, Object handler, Method method) throws NoSuchMethodException {this.cmdId cmdId;this.handler handler;this.method method;Class? parameterType method.getParameterTypes()[1];this.protobufDecode parameterType.getMethod(parseFrom, byte[].class);}public int getCmdId() {return cmdId;}public Object getHandler() {return handler;}public Method getMethod() {return method;}public Method getProtobufDecode() {return protobufDecode;}
}由于我们确定方法的第二个参数一定是Protobuf协议数据而Protobuf生成的类中自带有parseFrom的方法可以将byte数组反序列化成Protobuf数据对象我们就可以使用反射的方式自动反序列化。
这一波是结合了项目开发规范的代码优化。
初始化Dispatcher
修改LoginMain的initServer启动服务时搜索项目目录下的所有类并传入ProtoDispatcher进行初始化。
Overrideprotected void initServer() {...// 协议转发器初始化SetClass? classes;try {classes ClassScannerUtils.getClasses(org.login);ProtoDispatcher protoDispatcher SpringUtils.getBean(ProtoDispatcher.class);protoDispatcher.load(classes);} catch (IOException | URISyntaxException | ClassNotFoundException | NoSuchMethodException e) {throw new RuntimeException(e);}log.info(LoginServer start!);}协议的逻辑分发dispatcher
修改ConnectActor移除注册登陆的ifelse分支改为使用ProtoDispatcher进行协议分发。 /*** 客户端上行数据*/private BehaviorBaseMsg onClientUpMsg(ClientUpMsg msg) throws InvocationTargetException, IllegalAccessException {Pack decode PackCodec.decode(msg.getData());log.info(receive client up msg. cmdId {}, decode.getCmdId());byte[] data decode.getData();ProtoDispatcher dispatcher SpringUtils.getBean(ProtoDispatcher.class);Pack pack dispatcher.dispatch(decode.getCmdId(), data, this);if (pack ! null) {this.ctx.writeAndFlush(PackCodec.encode(pack));}return this;}使用注解标记方法
我们修改LoginProtoHandler类使其继承于BaseProtoHandler。 并且将注册登录两个方法使用CMD注解标记。
/**1. Player相关协议处理*/
Slf4j
Component
public class LoginProtoHandler extends BaseProtoHandler {CMD(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE)public Pack onPlayerRegisterMsg(ConnectActor actor, PlayerMsg.C2SPlayerRegister up) {log.info(player register, accountName {}, password {}, up.getAccountName(), up.getPassword());...PlayerMsg.S2CPlayerRegister.Builder builder PlayerMsg.S2CPlayerRegister.newBuilder();...return new Pack(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE, builder.build().toByteArray());}CMD(ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE)public Pack onPlayerLoginMsg(ConnectActor actor, PlayerMsg.C2SPlayerLogin up) {...PlayerMsg.S2CPlayerLogin.Builder builder PlayerMsg.S2CPlayerLogin.newBuilder();...return new Pack(ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE, builder.build().toByteArray());}
}两个细节
使用Component注解标记类因为我们的dispatcher通过Spring获取handler的单例对象并通过该对象进行方法调用因此使用Component将其生命周期托管给Spring。CMD(ProtoEnumMsg.CMD.ID.xx)因为我们对CMD的参数命名为value因此使用注解不需要带入参数名如CMD(value ProtoEnumMsg.CMD.ID.xx).回参改为回Pack由ConnectActor进行消息回传。
基于这几点我们将所有的业务逻辑独立在了ProtoHandler中后续业务开发不再需要考虑如何反序列化如何回传消息如何将协议号与方法映射。
测试
启动LoginServer启动ClientClient控制台输入login_test1_123456 可以看到登录服输出了登录协议相关日志。
结语
本节笔者使用自定义注解反射解决了开发新协议时需要添加if…else…分支的问题同时也使得业务开发人员可以更加专注于业务逻辑开发减少其开发新协议需要修改的文件数量在多人协同时是非常有益且高效的。 但是这也带来了问题使用CMD注解的方法其传参的规则就定下来参数0为玩家数据参数1为protobuf数据而这个规则需要由开发人员口口相传或者整理一份新员工开发文档中作为项目开发规范。若是不熟悉代码且经验不足的开发人员可能会在传参上犯下错误。
但是总的来说这么做还是利大于弊的未来我们进行游戏逻辑服的开发会涉及大量的协议交互使用dispatcher可以很大程度上节约我们的时间提高我们的效率。