使用Netty4实现基本的消息分发
示例工程代码
可从附件下载
具体的说明和用法在后面介绍
需求与目的
一个游戏服务端需要处理各种业务逻辑,每一种业务逻辑都对应着一个请求消息和一个响应消息。那么服务端需要把这些不同的消息自动分发到对应的业务逻辑中处理。
最简单的处理方式就是根据请求消息中的type字段,使用switch case来进行分别处理,但这种方式随着消息的增多,显现了一些坏味道:长长的一大坨不太好看;如果要添加新的消息、新的逻辑,或者去掉新的消息、新的逻辑,在代码上不但要修改这些消息和逻辑,还不得不修改这长长的一坨swtich case,这样的修改显得很多余。
所以我们的目的就是把消息分发这块的代码自动化,在增加、修改、删除消息和逻辑的时候不需要再对消息分发的代码再做修改,从而使得修改的代码最小化。
实现原理
在实现中,使用了注解(annotation)
- package com.company.game.dispatcher.annotation;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- /**
- * 修饰消息类和业务逻辑执行类
- * msgType指定对应的类型,从1开始计数
- * @author xingchencheng
- *
- */
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface UserMsgAndExecAnnotation {
- short msgType();
- }
唯一的字段msgType代表了消息类型,这是客户端与服务端的约定,这里我们从1开始计数。
当我们要增加一个加法消息时,就使用这个注解来修饰我们的请求消息类:
- package com.company.game.dispatcher.msg;
- import com.company.game.dispatcher.annotation.UserMsgAndExecAnnotation;
- /**
- * 加法请求消息类
- *
- * @author xingchencheng
- *
- */
- @UserMsgAndExecAnnotation(msgType = MsgType.ADD)
- public class UserAddRequest extends RequestMsgBase {
- private double leftNumber;
- private double RightNumber;
- public UserAddRequest() {
- super(MsgType.ADD);
- }
- public double getLeftNumber() {
- return leftNumber;
- }
- public void setLeftNumber(double leftNumber) {
- this.leftNumber = leftNumber;
- }
- public double getRightNumber() {
- return RightNumber;
- }
- public void setRightNumber(double rightNumber) {
- RightNumber = rightNumber;
- }
- }
为什么要这样修饰呢?先从服务端的解码(decode)说起,实例代码中,一个请求消息是这样规定的:
0-1字节表示整个消息的长度(单位:字节)
2-3字节代表消息类型,对应annotation的msgType
余下的是消息的json字符串(UTF-8编码)
我们需要根据2-3字节表示的msgType得到对应请求消息类的class对象,用这个class对象来序列化json字符串,得到具体的请求对象。那么怎么根据msgType得到class对象呢?这就是为什么要使用annotation的原因。
在服务端程序启动前,会执行下面的处理:
- // msgType->请求、响应类的class对象
- private static Map<Short, Class<?>> typeToMsgClassMap;
- // 根据类型得到对应的消息类的class对象
- public static Class<?> getMsgClassByType(short type) {
- return typeToMsgClassMap.get(type);
- }
- /**
- * 初始化typeToMsgClassMap
- * 遍历包com.company.game.dispatcher.msg
- * 取得消息类的class文件
- *
- * @throws ClassNotFoundException
- * @throws IOException
- */
- public static void initTypeToMsgClassMap()
- throws ClassNotFoundException, IOException {
- Map<Short, Class<?>> tmpMap = new HashMap<Short, Class<?>>();
- Set<Class<?>> classSet = getClasses("com.company.game.dispatcher.msg");
- if (classSet != null) {
- for (Class<?> clazz : classSet) {
- if (clazz.isAnnotationPresent(UserMsgAndExecAnnotation.class)) {
- UserMsgAndExecAnnotation annotation = clazz.getAnnotation(UserMsgAndExecAnnotation.class);
- tmpMap.put(annotation.msgType(), clazz);
- }
- }
- }
- typeToMsgClassMap = Collections.unmodifiableMap(tmpMap);
- }
程序初始化了一个映射,在指定的包找到请求的消息类的class,读取class上的annotation,保存到一个Map中,这样在后续就可以根据这个Map来根据msgType得到class对象了。
再给出解码器的实现:
- package com.company.game.dispatcher.codec;
- import java.util.List;
- import com.company.game.dispatcher.util.ClassUtil;
- import com.company.game.dispatcher.util.GsonUtil;
- import com.google.gson.Gson;
- import io.netty.buffer.ByteBuf;
- import io.netty.channel.ChannelHandlerContext;
- import io.netty.handler.codec.ByteToMessageDecoder;
- /**
- * 解码器
- * 客户端和服务端均有使用
- * 0-1字节表示整个消息的长度(单位:字节)
- * 2-3字节代表消息类型,对应annotation
- * 余下的是消息的json字符串(UTF-8编码)
- *
- * @author xingchencheng
- *
- */
- public class MsgDecoder extends ByteToMessageDecoder {
- @Override
- protected void decode(ChannelHandlerContext ctx, ByteBuf buf,
- List<Object> list) throws Exception {
- if (buf.readableBytes() < 2) {
- return;
- }
- Gson gson = GsonUtil.getGson();
- short jsonBytesLength = (short) (buf.readShort() - 2);
- short type = buf.readShort();
- byte[] tmp = new byte[jsonBytesLength];
- buf.readBytes(tmp);
- String json = new String(tmp, "UTF-8");
- Class<?> clazz = ClassUtil.getMsgClassByType(type);
- Object msgObj = gson.fromJson(json, clazz);
- list.add(msgObj);
- }
- }
解码完成后,程序进入到服务端的handler中:
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, Object msgObject)
- throws Exception {
- // 分发消息给对应的消息处理器
- Dispatcher.submit(ctx.channel(), msgObject);
- }
Dispatcher代码如下:
- package com.company.game.dispatcher;
- import io.netty.channel.Channel;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import com.company.game.dispatcher.exec.BusinessLogicExecutorBase;
- import com.company.game.dispatcher.msg.RequestMsgBase;
- import com.company.game.dispatcher.util.ClassUtil;
- /**
- * 抽象了分发器
- * 多线程执行
- * 某个消息对象msgObject指定某个业务逻辑对象executor
- * submit到线程池中
- * @author xingchencheng
- *
- */
- public class Dispatcher {
- private static final int MAX_THREAD_NUM = 50;
- private static ExecutorService executorService =
- Executors.newFixedThreadPool(MAX_THREAD_NUM);
- public static void submit(Channel channel, Object msgObject)
- throws InstantiationException, IllegalAccessException {
- RequestMsgBase msg = (RequestMsgBase) msgObject;
- Class<?> executorClass = ClassUtil.getExecutorClassByType(msg.getType());
- BusinessLogicExecutorBase executor =
- (BusinessLogicExecutorBase) executorClass.newInstance();
- executor.setChannel(channel);
- executor.setMsgObject(msgObject);
- executorService.submit(executor);
- }
- }
我们看到,在代码中也是根据msgType取得了对应的一个class对象,并new了一个对象出来,交给了线程池进行并发执行,这个对象就是业务逻辑处理器对象,它实现了Runnable接口,进行一些业务逻辑上的处理。根据msgType取得class对象的映射过程跟前面提到的映射原理是相同的,可以参见代码。贴出业务逻辑处理器对象的代码:
- package com.company.game.dispatcher.exec;
- import com.company.game.dispatcher.annotation.UserMsgAndExecAnnotation;
- import com.company.game.dispatcher.msg.MsgType;
- import com.company.game.dispatcher.msg.UserAddRequest;
- import com.company.game.dispatcher.msg.UserAddResponse;
- /**
- * 具体的业务逻辑
- * 实现加法
- *
- * @author xingchencheng
- *
- */
- @UserMsgAndExecAnnotation(msgType = MsgType.ADD)
- public class UserAddExecutor extends BusinessLogicExecutorBase {
- public void run() {
- UserAddResponse response = new UserAddResponse();
- if (this.msgObject instanceof UserAddRequest) {
- UserAddRequest request = (UserAddRequest) this.msgObject;
- double result = request.getLeftNumber() + request.getRightNumber();
- response.setResult(result);
- response.setSuccess(true);
- } else {
- response.setSuccess(false);
- }
- System.out.println("服务端处理结果:" + response.getResult());
- channel.writeAndFlush(response);
- }
- }
注意,它也得用annotation来修饰。
思路大致就是如此,如果要增加一个请求,在示例代码中,需要做3件事情:
- 在MsgType添加一个类型
- 添加请求相应消息类
- 添加业务逻辑处理器类
而不需要修改消息分发的代码。
示例项目的说明和使用
- 工程可在文章开头的github中或附件得到
- 项目使用Maven3构建,构建的结果是一个jar,可通过命令行分别运行服务端和客户端
- 仅仅是个示例,并没有过多的考虑异常处理,性能等方面
- 没有单元测试和其他测试
提供了命令行工具,帮助信息如下:
服务端启动命令:
客户端启动命令:
结语
本文的描述未必清晰,更好的方法是直接看代码。
关于消息分发想必还有更好的方法,这里只是抛砖引玉,希望路过的各位能提供更好的方法一起参考。
使用Netty4实现基本的消息分发的更多相关文章
- cocos creator主程入门教程(六)—— 消息分发
五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 本篇开始介绍游戏业务架构相关的内容.在游戏业务层,所有需要隔离的系统和模块间通信都可以通过消息分发解耦. ...
- 深入详解美团点评CAT跨语言服务监控(四)服务端消息分发
这边首先介绍下大众点评CAT消息分发大概的架构如下: 图4 消息分发架构图 分析管理器的初始化 我们在第一章讲到服务器将接收到的消息交给解码器(MessageDecoder)去做解码最后交给具体的消费 ...
- 一个可以代替冗长switch-case的消息分发小框架
在项目中,我需要维护一个应用层的字节流协议.这个协议的每条报文都是一个字节数组,数组的头两个字节表示消息的传送方向,第三.四个字节表示消息ID,也就是消息种类,再往后是消息内容.时间戳.校验码等……整 ...
- Android 消息分发机制
Android 中针对耗时的操作,放在主线程操作,轻者会造成 UI 卡顿,重则会直接无响应,造成 Force Close.同时在 Android 3.0 以后,禁止在主线程进行网络请求. 针对耗时或者 ...
- RabbitMQ消息队列(六):使用主题进行消息分发[转]
在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity(严重级别)的log.但是,这也是它之所以叫做简单日志 ...
- RabbitMq初探——消息分发
消息分发 前言 我们在用到消息队列的场景,一般是处理逻辑复杂,耗时,所以将同步改为异步处理,接入队列,下游处理耗时任务. 队列消息数量很大,且下游worker进程(消费者)处理耗时长,所以就有了任务的 ...
- RabbitMQ消息分发轮询和Message Acknowledgment
一.消息分发 RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费. 多个消费者可以订阅同一个Q ...
- delphi VCL研究之消息分发机制-delphi高手突破读书笔记
1.VCL 概貌 先看一下VCL类图的主要分支,如图4.1所示.在图中可以看到,TObject是VCL的祖先类,这也是Object Pascal语言所规定的.但实际上,TObject以及TObject ...
- RabbitMQ基本用法、消息分发模式、消息持久化、广播模式
RabbitMQ基本用法 进程queue用于同一父进程创建的子进程间的通信 而RabbitMQ可以在不同父进程间通信(例如在word和QQ间通信) 示例代码 生产端(发送) import pika c ...
随机推荐
- Android实现动态改变屏幕方向(Landscape & Portrait)
1.AndroidManifest.xml: <activity> android:screenOrientation="portrait" ... 2.xx.java ...
- Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法(转)
Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法http://www.cnblogs.com/zdz8207/archive/2013/02/27/android- ...
- 解题:ZJOI 2013 K大数查询
题面 树套树,权值线段树套序列线段树,每次在在权值线段树上的每棵子树上做区间加,查询的时候左右子树二分 本来想两个都动态开点的,这样能体现树套树在线的优越性.但是常数太大惹,所以外层直接固定建树了QA ...
- fzyjojP2931 乱搞
其实很简单(第二个不知是啥) 贡献独立 其实第一种就是考虑一个点在哈夫曼树上的期望深度是多少 因为精度要求较高 所以要高精小数加,高精小数除以低精整数
- 虚拟机中在centos6.7环境下安装eclipse
采用的是在线安装的方式,所以省去了很多配置环境变量的步骤,经过以下5步. 1, yum install eclipse 2, 安装xmanager -> windows下远程eclipse可 ...
- python 中的 %s,%r,__str__,__repr__
1.%s,%r的区别 在进行格式化输出时,%r 与 %s 的区别就好比 repr() 函数处理对象与 str() 函数处理对象的差别. %s ⇒ str(),比较智能: %r ⇒ repr(),处理较 ...
- bzoj千题计划121:bzoj1033: [ZJOI2008]杀蚂蚁antbuster
http://www.lydsy.com/JudgeOnline/problem.php?id=1033 经半个下午+一个晚上+半个晚上 的 昏天黑地调代码 最终成果: codevs.洛谷.tyvj上 ...
- php检测文件编码方法[非完美]
关于文件编码的检测,百度一下一大把都是,但是确实没有能用的. 很多人建议 mb_detect_encoding 检测,可是不知为何我这不成功,什么都没输出. 看到有人写了个增强版,用 BOM 判断的, ...
- [转载]Javascript 同步异步加载详解
http://handyxuefeng.blog.163.com/blog/static/4545217220131125022640/ 本文总结一下浏览器在 javascript 的加载方式. 关键 ...
- 实现asp.net的文件压缩、解压、下载
很早前就想做文件的解压.压缩.下载 了,不过一直没时间,现在项目做完了,今天弄了下.不过解压,压缩的方法还是看的网上的,嘻嘻~~不过我把它们综合了一下哦.呵呵~~ 1.先要从网上下载一个icsharp ...