本章我们需要重新设计移动消息,首先观察一下之前移动可能存在的问题
移动消息存在的问题:
如果现在有两个角色1和2,当角色1从A移动到B时,角色2登录,是不会显示角色1的移动效果的,只有当角色1在下一次移动时才会有显示,并且这种显示由于位置的不同步,会导致画面上的瞬移。
解决方案:
在User类中添加MoveState,当用户移动时,记录用户的移动状态,移动状态包括:起始坐标(X/Y),终止坐标(X/Y),和移动起始时间。
当用户上线时,遍历用户字典,并在谁在场消息处理时(WhoElseIsHereCmdHandler)把其他用户的移动状态推送给上线的用户,当客户端接收到该数据时,按照推送的消息,按时间计算出其他用户移动到的位置,并继续接下来的移动播放。
注意是客户端按照消息的信息模拟出的行走路线哈,接下来上代码:
1.生成新的GameMsgProtocol.java
由于更新了移动消息,增加了更多的信息,此时我们的GameMsgProtocol需要重新生成了,GameMsgProtocol的代码在最下方附录,需要大家重新生成出一个java文件。
再说一下生成GameMsgProtocol.java的方式:
1.下载对应版本的protobuf:https://github.com/protocolbuffers/protobuf/releases/tag/v3.10.0-rc1,这里我使用的是
2.配置protobuf环境变量:将bin目录放到path下
3.在命令行输入:protoc.exe --java_out=${目标目录} .\GameMsgProtocol.proto
比如将GameMsgProtocol.proto放在桌面,并把GameMsgProtocol.java输出到桌面:
1.打开命令行并跳转到桌面:cd ./desktop
2.protoc.exe --java_out=.\ .\GameMsgProtocol.proto
3.使用新生成的GameMsgProtocol.java替换原有的即可
2.增加移动状态类:MoveState
package com.tk.tinygame.herostory.model;
/**
* 移动状态
*/
public class MoveState {
/**
* 起始位置 X
*/
public float fromPosX;
/**
* 起始位置 Y
*/
public float fromPosY;
/**
* 目标位置 X
*/
public float toPosX;
/**
* 目标位置 Y
*/
public float toPosY;
/**
* 开始时间
*/
public long startTime;
}
3.在User类中添加MoveState 属性
package com.tk.tinygame.herostory.model;
public class User {
/**
* 用户 Id
*/
public int userId;
/**
* 影响形象
*/
public String heroAvatar;
/**
* 移动状态
*/
public final MoveState moveState = new MoveState();
}
4.为了方便管理,创建新的package,把User相关的类放进去
5.修改用户移动消息,记录用户移动状态:UserMoveToCmdHandler
处理思路:更新用户字典中的移动状态,并把用户移动的消息广播给所有用户
package com.tk.tinygame.herostory.cmdhandler;
import com.tk.tinygame.herostory.Broadcaster;
import com.tk.tinygame.herostory.model.User;
import com.tk.tinygame.herostory.model.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;
/**
* 用户移动指令处理器
*/
public class UserMoveToCmdHandler implements ICmdHandler<GameMsgProtocol.UserMoveToCmd>{
@Override
public void handle(ChannelHandlerContext ctx, GameMsgProtocol.UserMoveToCmd cmd) {
if (null == ctx
|| null == cmd) {
return;
}
// 获取用户 Id
Integer userId = (Integer)ctx.channel().attr(AttributeKey.valueOf("userId")).get();
if (null == userId) {
return;
}
User existUser = UserManager.getByUserId(userId);
if(null == existUser){
return;
}
long nowTime = System.currentTimeMillis();
//更新用户的移动状态
existUser.moveState.fromPosX = cmd.getMoveFromPosX();
existUser.moveState.fromPosY = cmd.getMoveFromPosY();
existUser.moveState.toPosX = cmd.getMoveToPosX();
existUser.moveState.toPosY = cmd.getMoveToPosY();
existUser.moveState.startTime = nowTime;
GameMsgProtocol.UserMoveToResult.Builder resultBuilder = GameMsgProtocol.UserMoveToResult.newBuilder();
resultBuilder.setMoveUserId(userId);
resultBuilder.setMoveToPosX(cmd.getMoveToPosX());
resultBuilder.setMoveToPosY(cmd.getMoveToPosY());
//添加启动起始,终止时间
resultBuilder.setMoveFromPosX(cmd.getMoveFromPosX());
resultBuilder.setMoveFromPosY(cmd.getMoveFromPosY());
resultBuilder.setMoveStartTime(nowTime);
GameMsgProtocol.UserMoveToResult newResult = resultBuilder.build();
//把用户移动消息发送给所有用户
Broadcaster.broadcast(newResult);
}
}
6.修改谁在场消息,在登录成功后获取其他用户的移动状态:WhoElseIsHereCmdHandler
处理思路:遍历用户字典,获取每一个用户的移动状态,把所有用户的移动状态获取后,推送给客户端
package com.tk.tinygame.herostory.cmdhandler;
import com.tk.tinygame.herostory.model.User;
import com.tk.tinygame.herostory.model.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
/**
* 还有谁在场
*/
public class WhoElseIsHereCmdHandler implements ICmdHandler<GameMsgProtocol.WhoElseIsHereCmd>{
@Override
public void handle(ChannelHandlerContext ctx, GameMsgProtocol.WhoElseIsHereCmd cmd) {
if (null == ctx || null == cmd) {
return;
}
//
// 处理还有谁在场消息
//
GameMsgProtocol.WhoElseIsHereResult.Builder resultBuilder = GameMsgProtocol.WhoElseIsHereResult.newBuilder();
//遍历用户字典
for (User currUser : UserManager.listUser()) {
if (null == currUser) {
continue;
}
GameMsgProtocol.WhoElseIsHereResult.UserInfo.Builder userInfoBuilder
= GameMsgProtocol.WhoElseIsHereResult.UserInfo.newBuilder();
userInfoBuilder.setUserId(currUser.userId);
userInfoBuilder.setHeroAvatar(currUser.heroAvatar);
//构建移动状态
GameMsgProtocol.WhoElseIsHereResult.UserInfo.MoveState.Builder mvStateBuilder
= GameMsgProtocol.WhoElseIsHereResult.UserInfo.MoveState.newBuilder();
mvStateBuilder.setFromPosX(currUser.moveState.fromPosX);
mvStateBuilder.setFromPosY(currUser.moveState.fromPosY);
mvStateBuilder.setToPosX(currUser.moveState.toPosX);
mvStateBuilder.setToPosY(currUser.moveState.toPosY);
mvStateBuilder.setStartTime(currUser.moveState.startTime);
userInfoBuilder.setMoveState(mvStateBuilder);
resultBuilder.addUserInfo(userInfoBuilder);
}
//把用户字典中的用户广播
GameMsgProtocol.WhoElseIsHereResult newResult = resultBuilder.build();
ctx.writeAndFlush(newResult);
}
}
7.代码结构:
8.测试效果:注意地址更换了!step020
至此移动消息处理修改结束,可以正确的显示移动啦,由于消息内容有变化,测试地址一定要更换哦:http://cdn0001.afrxvk.cn/hero_story/demo/step020/index.html?serverAddr=127.0.0.1:12345&userId=1
9.其他重要类中的一些代码,方便大家核查代码
1.CmdHandlerFactory
package com.tk.tinygame.herostory.cmdhandler;
import com.google.protobuf.GeneratedMessageV3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.tk.tinygame.herostory.util.PackageUtil;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 命令处理器工厂类
*/
public final class CmdHandlerFactory {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(CmdHandlerFactory.class);
/**
* 命令处理器字典
*/
static private final Map<Class<?>, ICmdHandler<? extends GeneratedMessageV3>> _handlerMap = new HashMap<>();
/**
* 私有化类默认构造器
*/
private CmdHandlerFactory() {
}
/**
* 初始化
*/
static public void init() {
LOGGER.info("=== 完成命令与处理器的关联 ===");
// 获取包名称
final String packageName = CmdHandlerFactory.class.getPackage().getName();
// 获取 ICmdHandler 所有的实现类
Set<Class<?>> clazzSet = PackageUtil.listSubClazz(
packageName,
true,
ICmdHandler.class
);
for (Class<?> handlerClazz : clazzSet) {
if (null == handlerClazz ||
0 != (handlerClazz.getModifiers() & Modifier.ABSTRACT)) {
continue;
}
// 获取方法数组
Method[] methodArray = handlerClazz.getDeclaredMethods();
// 消息类型
Class<?> cmdClazz = null;
for (Method currMethod : methodArray) {
if (null == currMethod ||
!currMethod.getName().equals("handle")) {
continue;
}
// 获取函数参数类型数组
Class<?>[] paramTypeArray = currMethod.getParameterTypes();
if (paramTypeArray.length < 2 ||
paramTypeArray[1] == GeneratedMessageV3.class ||
!GeneratedMessageV3.class.isAssignableFrom(paramTypeArray[1])) {
continue;
}
cmdClazz = paramTypeArray[1];
break;
}
if (null == cmdClazz) {
continue;
}
try {
// 创建命令处理器实例
ICmdHandler<?> newHandler = (ICmdHandler<?>) handlerClazz.newInstance();
LOGGER.info(
"{} <==> {}",
cmdClazz.getName(),
handlerClazz.getName()
);
_handlerMap.put(cmdClazz, newHandler);
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
System.out.println("hello");
}
/**
* 创建命令处理器
*
* @param cmdClazz 命令类
* @return 命令处理器
*/
static public ICmdHandler<? extends GeneratedMessageV3> create(Class<?> cmdClazz) {
if (null == cmdClazz) {
return null;
}
return _handlerMap.get(cmdClazz);
}
}
2.ICmdHandler
package com.tk.tinygame.herostory.cmdhandler;
import com.google.protobuf.GeneratedMessageV3;
import io.netty.channel.ChannelHandlerContext;
/**
* 命令处理器接口
*
* @param <TCmd>
*/
public interface ICmdHandler<TCmd extends GeneratedMessageV3>{
/**
* 处理命令
*
* @param ctx
* @param cmd
*/
void handle(ChannelHandlerContext ctx, TCmd cmd);
}
3.UserAttkCmdHandler
package com.tk.tinygame.herostory.cmdhandler;
import com.tk.tinygame.herostory.Broadcaster;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;
/**
* 用户攻击指令处理器
*/
public class UserAttkCmdHandler implements ICmdHandler<GameMsgProtocol.UserAttkCmd>{
@Override
public void handle(ChannelHandlerContext ctx, GameMsgProtocol.UserAttkCmd cmd) {
if (null == ctx ||
null == cmd) {
return;
}
// 获取攻击者 Id
Integer attkUserId = (Integer) ctx.channel().attr(AttributeKey.valueOf("userId")).get();
if (null == attkUserId) {
return;
}
// 获取被攻击者 Id
int targetUserId = cmd.getTargetUserId();
GameMsgProtocol.UserAttkResult.Builder resultBuilder = GameMsgProtocol.UserAttkResult.newBuilder();
resultBuilder.setAttkUserId(attkUserId);
resultBuilder.setTargetUserId(targetUserId);
GameMsgProtocol.UserAttkResult newResult = resultBuilder.build();
Broadcaster.broadcast(newResult);
// 减血消息, 可以根据自己的喜好写...
// 例如加上装备加成, 暴击等等.
// 这些都属于游戏的业务逻辑了!
GameMsgProtocol.UserSubtractHpResult.Builder resultBuilder2 = GameMsgProtocol.UserSubtractHpResult.newBuilder();
resultBuilder2.setTargetUserId(targetUserId);
//简单的逻辑,打一下减10血
resultBuilder2.setSubtractHp(20);
GameMsgProtocol.UserSubtractHpResult newResult2 = resultBuilder2.build();
Broadcaster.broadcast(newResult2);
}
}
4.UserEntryCmdHandler
package com.tk.tinygame.herostory.cmdhandler;
import com.tk.tinygame.herostory.Broadcaster;
import com.tk.tinygame.herostory.model.User;
import com.tk.tinygame.herostory.model.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;
/**
* 处理用户入场
*/
public class UserEntryCmdHandler implements ICmdHandler<GameMsgProtocol.UserEntryCmd>{
@Override
public void handle(ChannelHandlerContext ctx, GameMsgProtocol.UserEntryCmd cmd) {
//空值判断
if (null == ctx || null == cmd) {
return;
}
//
// 处理用户入场消息
//
int userId = cmd.getUserId(); //用户id
String heroAvatar = cmd.getHeroAvatar(); //英雄形象
//将登录的用户加入用户字典
User newUser = new User();
newUser.userId = userId;
newUser.heroAvatar = heroAvatar;
UserManager.addUser(newUser);
GameMsgProtocol.UserEntryResult.Builder resultBuilder = GameMsgProtocol.UserEntryResult.newBuilder();
resultBuilder.setUserId(userId);
resultBuilder.setHeroAvatar(heroAvatar);
// 将用户 Id 附着到 Channel
ctx.channel().attr(AttributeKey.valueOf("userId")).set(userId);
// 构建结果并广播
GameMsgProtocol.UserEntryResult newResult = resultBuilder.build();
Broadcaster.broadcast(newResult);
}
}
5.UserManager
package com.tk.tinygame.herostory.model;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用户管理器
*/
public final class UserManager {
/**
* 用户字典
*/
static private final Map<Integer, User> _userMap = new ConcurrentHashMap<>();
/**
* 私有化类默认构造器
*/
private UserManager() {
}
/**
* 添加用户
*
* @param user
*/
static public void addUser(User user){
if(null != user){
_userMap.putIfAbsent(user.userId,user);
}
}
/**
* 移除用户
*
* @param userId
*/
static public void removeByUserId(int userId) {
_userMap.remove(userId);
}
/**
* 列表用户
*
* @return
*/
static public Collection<User> listUser() {
return _userMap.values();
}
/**
* 根据用户ID获取用户
*
* @return
*/
static public User getByUserId(int userId){
return _userMap.get(userId);
}
}
6.PackageUtil
package com.tk.tinygame.herostory.util;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
/**
* 名称空间实用工具
*/
public final class PackageUtil {
/**
* 类默认构造器
*/
private PackageUtil() {
}
/**
* 列表指定包中的所有子类
*
* @param packageName 包名称
* @param recursive 是否递归查找
* @param superClazz 父类的类型
* @return 子类集合
*/
static public Set<Class<?>> listSubClazz(
String packageName,
boolean recursive,
Class<?> superClazz) {
if (superClazz == null) {
return Collections.emptySet();
} else {
return listClazz(packageName, recursive, superClazz::isAssignableFrom);
}
}
/**
* 列表指定包中的所有类
*
* @param packageName 包名称
* @param recursive 是否递归查找?
* @param filter 过滤器
* @return 符合条件的类集合
*/
static public Set<Class<?>> listClazz(
String packageName, boolean recursive, IClazzFilter filter) {
if (packageName == null ||
packageName.isEmpty()) {
return null;
}
// 将点转换成斜杠
final String packagePath = packageName.replace('.', '/');
// 获取类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 结果集合
Set<Class<?>> resultSet = new HashSet<>();
try {
// 获取 URL 枚举
Enumeration<URL> urlEnum = cl.getResources(packagePath);
while (urlEnum.hasMoreElements()) {
// 获取当前 URL
URL currUrl = urlEnum.nextElement();
// 获取协议文本
final String protocol = currUrl.getProtocol();
// 定义临时集合
Set<Class<?>> tmpSet = null;
if ("FILE".equalsIgnoreCase(protocol)) {
// 从文件系统中加载类
tmpSet = listClazzFromDir(
new File(currUrl.getFile()), packageName, recursive, filter
);
} else if ("JAR".equalsIgnoreCase(protocol)) {
// 获取文件字符串
String fileStr = currUrl.getFile();
if (fileStr.startsWith("file:")) {
// 如果是以 "file:" 开头的,
// 则去除这个开头
fileStr = fileStr.substring(5);
}
if (fileStr.lastIndexOf('!') > 0) {
// 如果有 '!' 字符,
// 则截断 '!' 字符之后的所有字符
fileStr = fileStr.substring(0, fileStr.lastIndexOf('!'));
}
// 从 JAR 文件中加载类
tmpSet = listClazzFromJar(
new File(fileStr), packageName, recursive, filter
);
}
if (tmpSet != null) {
// 如果类集合不为空,
// 则添加到结果中
resultSet.addAll(tmpSet);
}
}
} catch (Exception ex) {
// 抛出异常!
throw new RuntimeException(ex);
}
return resultSet;
}
/**
* 从目录中获取类列表
*
* @param dirFile 目录
* @param packageName 包名称
* @param recursive 是否递归查询子包
* @param filter 类过滤器
* @return 符合条件的类集合
*/
static private Set<Class<?>> listClazzFromDir(
final File dirFile, final String packageName, final boolean recursive, IClazzFilter filter) {
if (!dirFile.exists() ||
!dirFile.isDirectory()) {
// 如果参数对象为空,
// 则直接退出!
return null;
}
// 获取子文件列表
File[] subFileArr = dirFile.listFiles();
if (subFileArr == null ||
subFileArr.length <= 0) {
return null;
}
// 文件队列, 将子文件列表添加到队列
Queue<File> fileQ = new LinkedList<>(Arrays.asList(subFileArr));
// 结果对象
Set<Class<?>> resultSet = new HashSet<>();
while (!fileQ.isEmpty()) {
// 从队列中获取文件
File currFile = fileQ.poll();
if (currFile.isDirectory() &&
recursive) {
// 如果当前文件是目录,
// 并且是执行递归操作时,
// 获取子文件列表
subFileArr = currFile.listFiles();
if (subFileArr != null &&
subFileArr.length > 0) {
// 添加文件到队列
fileQ.addAll(Arrays.asList(subFileArr));
}
continue;
}
if (!currFile.isFile() ||
!currFile.getName().endsWith(".class")) {
// 如果当前文件不是文件,
// 或者文件名不是以 .class 结尾,
// 则直接跳过
continue;
}
// 类名称
String clazzName;
// 设置类名称
clazzName = currFile.getAbsolutePath();
// 清除最后的 .class 结尾
clazzName = clazzName.substring(dirFile.getAbsolutePath().length(), clazzName.lastIndexOf('.'));
// 转换目录斜杠
clazzName = clazzName.replace('\\', '/');
// 清除开头的 /
clazzName = trimLeft(clazzName, "/");
// 将所有的 / 修改为 .
clazzName = join(clazzName.split("/"), ".");
// 包名 + 类名
clazzName = packageName + "." + clazzName;
try {
// 加载类定义
Class<?> clazzObj = Class.forName(clazzName);
if (null != filter &&
!filter.accept(clazzObj)) {
// 如果过滤器不为空,
// 且过滤器不接受当前类,
// 则直接跳过!
continue;
}
// 添加类定义到集合
resultSet.add(clazzObj);
} catch (Exception ex) {
// 抛出异常
throw new RuntimeException(ex);
}
}
return resultSet;
}
/**
* 从 .jar 文件中获取类列表
*
* @param jarFilePath .jar 文件路径
* @param packageName 包名称
* @param recursive 是否递归查询子包
* @param filter 类过滤器
* @return 符合条件的类集合
*/
static private Set<Class<?>> listClazzFromJar(
final File jarFilePath, final String packageName, final boolean recursive, IClazzFilter filter) {
if (jarFilePath == null ||
jarFilePath.isDirectory()) {
// 如果参数对象为空,
// 则直接退出!
return null;
}
// 结果对象
Set<Class<?>> resultSet = new HashSet<>();
try {
// 创建 .jar 文件读入流
JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFilePath));
// 进入点
JarEntry entry;
while ((entry = jarIn.getNextJarEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
// 获取进入点名称
String entryName = entry.getName();
if (!entryName.endsWith(".class")) {
// 如果不是以 .class 结尾,
// 则说明不是 JAVA 类文件, 直接跳过!
continue;
}
//
// 如果没有开启递归模式,
// 那么就需要判断当前 .class 文件是否在指定目录下?
//
// 获取目录名称
String tmpStr = entryName.substring(0, entryName.lastIndexOf('/'));
// 将目录中的 "/" 全部替换成 "."
tmpStr = join(tmpStr.split("/"), ".");
if (!recursive) {
if (!packageName.equals(tmpStr)) {
// 如果不是我们要找的包,
continue;
}
} else {
if (!tmpStr.startsWith(packageName)) {
// 如果不是子包,
continue;
}
}
String clazzName;
// 清除最后的 .class 结尾
clazzName = entryName.substring(0, entryName.lastIndexOf('.'));
// 将所有的 / 修改为 .
clazzName = join(clazzName.split("/"), ".");
// 加载类定义
Class<?> clazzObj = Class.forName(clazzName);
if (null != filter &&
!filter.accept(clazzObj)) {
// 如果过滤器不为空,
// 且过滤器不接受当前类,
// 则直接跳过!
continue;
}
// 添加类定义到集合
resultSet.add(clazzObj);
}
// 关闭 jar 输入流
jarIn.close();
} catch (Exception ex) {
// 抛出异常
throw new RuntimeException(ex);
}
return resultSet;
}
/**
* 类名称过滤器
*
* @author hjj2019
*/
@FunctionalInterface
static public interface IClazzFilter {
/**
* 是否接受当前类?
*
* @param clazz 被筛选的类
* @return 是否符合条件
*/
boolean accept(Class<?> clazz);
}
/**
* 使用连接符连接字符串数组
*
* @param strArr 字符串数组
* @param conn 连接符
* @return 连接后的字符串
*/
static private String join(String[] strArr, String conn) {
if (null == strArr ||
strArr.length <= 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < strArr.length; i++) {
if (i > 0) {
// 添加连接符
sb.append(conn);
}
// 添加字符串
sb.append(strArr[i]);
}
return sb.toString();
}
/**
* 清除源字符串左边的字符串
*
* @param src 原字符串
* @param trimStr 需要被清除的字符串
* @return 清除后的字符串
*/
static private String trimLeft(String src, String trimStr) {
if (null == src ||
src.isEmpty()) {
return "";
}
if (null == trimStr ||
trimStr.isEmpty()) {
return src;
}
if (src.equals(trimStr)) {
return "";
}
while (src.startsWith(trimStr)) {
src = src.substring(trimStr.length());
}
return src;
}
}
7.Broadcaster
package com.tk.tinygame.herostory;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
/**
* 广播员
*/
public final class Broadcaster {
/**
* 信道组, 注意这里一定要用 static,
* 否则无法实现群发
*/
static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 私有化类默认构造器
*/
private Broadcaster() {
}
/**
* 添加信道
*
* @param ch
*/
static public void addChannel(Channel ch) {
if (null != ch) {
_channelGroup.add(ch);
}
}
/**
* 移除信道
*
* @param ch
*/
static public void removeChannel(Channel ch) {
if (null != ch) {
_channelGroup.remove(ch);
}
}
/**
* 广播消息,广播给所有用户
*
* @param msg
*/
static public void broadcast(Object msg) {
if (null != msg) {
_channelGroup.writeAndFlush(msg);
}
}
}
8.GameMsgDecoder
package com.tk.tinygame.herostory;
import com.google.protobuf.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定义的消息解码器
*/
public class GameMsgDecoder extends ChannelInboundHandlerAdapter {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgDecoder.class);
/**
* @param ctx
* @param msg
* @throws Exception
* @deprecated 用于解码消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null == ctx || null == msg) {
return;
}
if (!(msg instanceof BinaryWebSocketFrame)) {
return;
}
try {
/**
* 读取消息,获取消息长度、编号和消息体
*/
BinaryWebSocketFrame inputFrame = (BinaryWebSocketFrame)msg;
ByteBuf byteBuf = inputFrame.content();
short i = byteBuf.readShort();// 读取消息的长度
int msgCode = byteBuf.readShort(); // 读取消息编号
byte[] msgBody = new byte[byteBuf.readableBytes()]; //拿到消息体
byteBuf.readBytes(msgBody);
// 获取消息构建器
Message.Builder msgBuilder = GameMsgRecognizer.getMsgBuilderByMsgCode(msgCode);
msgBuilder.clear();
msgBuilder.mergeFrom(msgBody);
//构建消息体
Message cmd = msgBuilder.build();
if (null != cmd) {
ctx.fireChannelRead(cmd);
}
}catch (Exception ex){
// 记录错误日志
LOGGER.error(ex.getMessage(),ex);
}
}
}
9.GameMsgEncoder
package com.tk.tinygame.herostory;
import com.google.protobuf.GeneratedMessageV3;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 游戏消息编码器
*/
public class GameMsgEncoder extends ChannelOutboundHandlerAdapter {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgEncoder.class);
/**
* @param ctx
* @param msg
* @param promise
* @throws Exception
* @deprecated 编码数据
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (null == ctx || null == msg) {
return;
}
if (!(msg instanceof GeneratedMessageV3)) {
super.write(ctx, msg, promise);
return;
}
try{
// 消息编码
int msgCode = GameMsgRecognizer.getMsgCodeByMsgClazz(msg.getClass());
if (-1 == msgCode) {
LOGGER.error(
"无法识别的消息类型, msgClazz = {}",
msg.getClass().getSimpleName()
);
super.write(ctx, msg, promise);
return;
}
// 消息体
byte[] msgBody = ((GeneratedMessageV3) msg).toByteArray();
ByteBuf byteBuf = ctx.alloc().buffer();
byteBuf.writeShort((short) msgBody.length); // 消息的长度
byteBuf.writeShort((short) msgCode); // 消息编号
byteBuf.writeBytes(msgBody); // 消息体
// 写出 ByteBuf
BinaryWebSocketFrame outputFrame = new BinaryWebSocketFrame(byteBuf);
super.write(ctx, outputFrame, promise);
}catch (Exception ex){
LOGGER.error(ex.getMessage(),ex);
}
}
}
10.GameMsgHandler
package com.tk.tinygame.herostory;
import com.google.protobuf.GeneratedMessageV3;
import com.tk.tinygame.herostory.cmdhandler.CmdHandlerFactory;
import com.tk.tinygame.herostory.cmdhandler.ICmdHandler;
import com.tk.tinygame.herostory.model.UserManager;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定义消息处理器
*/
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);
/**
* 信道组
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
if (null == ctx) {
return;
}
try {
super.channelActive(ctx);
//建立长连接后,将信道添加到信道组
Broadcaster.addChannel(ctx.channel());
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
/**
* @param ctx
* @deprecated 处理用户离场消息
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
if (null == ctx) {
return;
}
try {
super.handlerRemoved(ctx);
Broadcaster.removeChannel(ctx.channel());
Integer userId = (Integer) ctx.channel().attr(AttributeKey.valueOf("userId")).get();
if (null == userId) {
return;
}
UserManager.removeByUserId(userId);
GameMsgProtocol.UserQuitResult.Builder resultBuilder = GameMsgProtocol.UserQuitResult.newBuilder();
resultBuilder.setQuitUserId(userId);
GameMsgProtocol.UserQuitResult newResult = resultBuilder.build();
Broadcaster.broadcast(newResult);
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
/**
* @param ctx
* @param msg
* @throws Exception
* @deprecated 处理用户消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null == ctx || null == msg) {
return;
}
LOGGER.info("收到客户端消息, msgClzz={},msgBody = {}",
msg.getClass().getName(),msg);
/**
* 根据消息类型作对应处理
*/
try {
ICmdHandler<? extends GeneratedMessageV3> cmdHandler = CmdHandlerFactory.create(msg.getClass());
System.out.println(cmdHandler.toString());
if (null != cmdHandler) {
cmdHandler.handle(ctx, cast(msg));
}
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
/**
* 转型为命令对象
*
* @param msg
* @param <TCmd>
* @return
*/
static private <TCmd extends GeneratedMessageV3> TCmd cast(Object msg) {
if (null == msg) {
return null;
} else {
return (TCmd) msg;
}
}
}
11.GameMsgRecognizer
package com.tk.tinygame.herostory;
import com.google.protobuf.GeneratedMessageV3;
import com.google.protobuf.Message;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* 消息识别器
*/
public final class GameMsgRecognizer {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgRecognizer.class);
/**
* 消息编号 -> 消息对象字典
*/
static private final Map<Integer, GeneratedMessageV3> _msgCodeAndMsgObjMap = new HashMap<>();
/**
* 消息类 -> 消息编号字典
*/
static private final Map<Class<?>, Integer> _msgClazzAndMsgCodeMap = new HashMap<>();
/**
* 私有化类默认构造器
*/
private GameMsgRecognizer() {
}
/**
* 初始化
*/
static public void init() {
LOGGER.info("=== 完成消息类与消息编号的映射 ===");
// 获取内部类
Class<?>[] innerClazzArray = GameMsgProtocol.class.getDeclaredClasses();
for (Class<?> innerClazz : innerClazzArray) {
if (null == innerClazz ||
!GeneratedMessageV3.class.isAssignableFrom(innerClazz)) {
// 如果不是消息类,
continue;
}
// 获取类名称并小写
String clazzName = innerClazz.getSimpleName();
clazzName = clazzName.toLowerCase();
for (GameMsgProtocol.MsgCode msgCode : GameMsgProtocol.MsgCode.values()) {
if (null == msgCode) {
continue;
}
// 获取消息编码
String strMsgCode = msgCode.name();
strMsgCode = strMsgCode.replaceAll("_", "");
strMsgCode = strMsgCode.toLowerCase();
if (!strMsgCode.startsWith(clazzName)) {
continue;
}
try {
// 相当于调用 UserEntryCmd.getDefaultInstance();
Object returnObj = innerClazz.getDeclaredMethod("getDefaultInstance").invoke(innerClazz);
LOGGER.info(
"{} <==> {}",
innerClazz.getName(),
msgCode.getNumber()
);
_msgCodeAndMsgObjMap.put(
msgCode.getNumber(),
(GeneratedMessageV3) returnObj
);
_msgClazzAndMsgCodeMap.put(
innerClazz,
msgCode.getNumber()
);
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
}
System.out.println("hello");
}
/**
* 根据消息编号获取消息构建器
*
* @param msgCode 消息编码
* @return 消息构建器
*/
static public Message.Builder getMsgBuilderByMsgCode(int msgCode) {
if (msgCode < 0) {
return null;
}
GeneratedMessageV3 defaultMsg = _msgCodeAndMsgObjMap.get(msgCode);
if (null == defaultMsg) {
return null;
} else {
return defaultMsg.newBuilderForType();
}
}
/**
* 根据消息类获取消息编号
*
* @param msgClazz 消息类
* @return 消息编码
*/
static public int getMsgCodeByMsgClazz(Class<?> msgClazz) {
if (null == msgClazz) {
return -1;
}
Integer msgCode = _msgClazzAndMsgCodeMap.get(msgClazz);
if (null == msgCode) {
return -1;
} else {
return msgCode;
}
}
}
12.ServerMain
package com.tk.tinygame.herostory;
import com.tk.tinygame.herostory.cmdhandler.CmdHandlerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 服务器入口类
*/
public class ServerMain {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class);
/**
* 应用主函数
* @param args 参数数组
*/
public static void main(String[] args) {
// 设置 log4j 属性文件
PropertyConfigurator.configure(ServerMain.class.getClassLoader().getResourceAsStream("log4j.properties"));
// 初始化命令处理器工厂
CmdHandlerFactory.init();
//初始化消息识别器
GameMsgRecognizer.init();
EventLoopGroup bossGroup = new NioEventLoopGroup(); //拉客的group
EventLoopGroup workGroup = new NioEventLoopGroup(); //干活的group
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workGroup);
b.channel(NioServerSocketChannel.class); //服务器信道的处理方式
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new HttpServerCodec(), // Http 服务器编解码器
new HttpObjectAggregator(65535), // 内容长度限制
new WebSocketServerProtocolHandler("/websocket"), // WebSocket 协议处理器, 在这里处理握手、ping、pong 等消息
new GameMsgDecoder(),
new GameMsgEncoder(),
new GameMsgHandler() // 自定义的消息处理器
);
}
});
b.option(ChannelOption.SO_BACKLOG,128);
b.childOption(ChannelOption.SO_KEEPALIVE,true);
try {
// 绑定 12345 端口,
// 注意: 实际项目中会使用 argArray 中的参数来指定端口号
ChannelFuture f = b.bind(12345).sync();
if (f.isSuccess()) {
LOGGER.info("服务器启动成功!");
}
// 等待服务器信道关闭,
// 也就是不要立即退出应用程序, 让应用程序可以一直提供服务
f.channel().closeFuture().sync();
} catch (Exception ex) {
//关闭服务器
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
LOGGER.error(ex.getMessage(),ex);
}
}
}
13.log4j.properties
log4j.rootLogger=info,stdout,logFile
log4j.logger.com.heroStory=debug
# ---- stdout ----
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.encoding=UTF-8
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%d{HH:mm:ss,SSS}] [%p] %C{1}.%M --> %m%n
# ---- file ----
log4j.appender.logFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logFile.encoding=UTF-8
log4j.appender.logFile.append=true
log4j.appender.logFile.DatePattern='.'yyyy-MM-dd
log4j.appender.logFile.File=log/all.log
log4j.appender.logFile.layout=org.apache.log4j.PatternLayout
log4j.appender.logFile.layout.ConversionPattern=[%d{HH:mm:ss,SSS}] [%p] %C{1}.%M --> %m%n
附录:GameMsgProtocol_修改后,可以用这个修改一下包名然后生成java文件
syntax = "proto3";
package msg;
option java_package = "com.tk.tinygame.herostory.msg";
// 消息代号
enum MsgCode {
USER_ENTRY_CMD = 0;
USER_ENTRY_RESULT = 1;
WHO_ELSE_IS_HERE_CMD = 2;
WHO_ELSE_IS_HERE_RESULT = 3;
USER_MOVE_TO_CMD = 4;
USER_MOVE_TO_RESULT = 5;
USER_QUIT_RESULT = 6;
USER_STOP_CMD = 7;
USER_STOP_RESULT = 8;
USER_ATTK_CMD = 9;
USER_ATTK_RESULT = 10;
USER_SUBTRACT_HP_RESULT = 11;
USER_DIE_RESULT = 12;
};
//
// 用户入场
///////////////////////////////////////////////////////////////////////
// 指令
message UserEntryCmd {
// 用户 Id
uint32 userId = 1;
// 英雄形象
string heroAvatar = 2;
}
// 结果
message UserEntryResult {
// 用户 Id
uint32 userId = 1;
// 英雄形象
string heroAvatar = 2;
}
//
// 还有谁在场
///////////////////////////////////////////////////////////////////////
// 指令
message WhoElseIsHereCmd {
}
// 结果
message WhoElseIsHereResult {
// 用户信息数组
repeated UserInfo userInfo = 1;
// 用户信息
message UserInfo {
// 用户 Id
uint32 userId = 1;
// 英雄形象
string heroAvatar = 2;
// 移动状态
MoveState moveState = 3;
// 移动状态
message MoveState {
// 起始位置 X
float fromPosX = 1;
// 起始位置 Y
float fromPosY = 2;
// 移动到位置 X
float toPosX = 3;
// 移动到位置 Y
float toPosY = 4;
// 启程时间戳
uint64 startTime = 5;
}
}
}
//
// 用户移动
///////////////////////////////////////////////////////////////////////
// 指令
message UserMoveToCmd {
//
// XXX 注意: 用户移动指令中没有用户 Id,
// 这是为什么?
//
// 起始位置 X
float moveFromPosX = 1;
// 起始位置 Y
float moveFromPosY = 2;
// 移动到位置 X
float moveToPosX = 3;
// 移动到位置 Y
float moveToPosY = 4;
}
// 结果
message UserMoveToResult {
// 移动用户 Id
uint32 moveUserId = 1;
// 起始位置 X
float moveFromPosX = 2;
// 起始位置 Y
float moveFromPosY = 3;
// 移动到位置 X
float moveToPosX = 4;
// 移动到位置 Y
float moveToPosY = 5;
// 启程时间戳
uint64 moveStartTime = 6;
}
//
// 用户退场
///////////////////////////////////////////////////////////////////////
//
// XXX 注意: 用户退场不需要指令, 因为是在断开服务器的时候执行
//
// 结果
message UserQuitResult {
// 退出用户 Id
uint32 quitUserId = 1;
}
//
// 用户停驻
///////////////////////////////////////////////////////////////////////
// 指令
message UserStopCmd {
}
// 结果
message UserStopResult {
// 停驻用户 Id
uint32 stopUserId = 1;
// 停驻在位置 X
float stopAtPosX = 2;
// 停驻在位置 Y
float stopAtPosY = 3;
}
//
// 用户攻击
///////////////////////////////////////////////////////////////////////
// 指令
message UserAttkCmd {
// 目标用户 Id
uint32 targetUserId = 1;
}
// 结果
message UserAttkResult {
// 发动攻击的用户 Id
uint32 attkUserId = 1;
// 目标用户 Id
uint32 targetUserId = 2;
}
// 用户减血结果
message UserSubtractHpResult {
// 目标用户 Id
uint32 targetUserId = 1;
// 减血量
uint32 subtractHp = 2;
}
// 死亡结果
message UserDieResult {
// 目标用户 Id
uint32 targetUserId = 1;
}