上节代码优化
在上一节代码中,还有几个点可以进行优化。
-
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;
// template
private 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对日志打印逻辑进行修改。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志格式 -->
<property name="CONSOLE_STR"
value="%date{ISO8601}[%highlight(%level)] Class:%class Line:%L - %m %n"/>
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_STR}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
</root>
<logger name="org.mongodb.driver.cluster" level="INFO"/>
<logger name="org.mongodb.driver.protocol.command" level="INFO"/>
</configuration>
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 {
@Autowired
private MongoDbContext mongoDbContext;
public String login() {
List<PlayerEntity> 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<Object> {
@Autowired
PlayerProtoHandler playerProtoHandler;
/**
* 读取数据
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
this.doRead(ctx, msg);
}
private void doRead(ChannelHandlerContext ctx, Object msg) {
log.info("received msg = : " + msg);
String response = "";
if (msg.equals("login\n")) {
response = playerProtoHandler.login();
} else if (msg.equals("register\n")) {
response = playerProtoHandler.register();
}
ctx.writeAndFlush(response + "\n");
}
启动我们的客户端,分别输入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;
// 玩家相关协议 前缀 110
PLAYER_LOGIN = 11001; // 玩家登录
PLAYER_REGISTER = 11002; // 玩家注册
}
再创建一个protoc.bat批处理指令
@echo off
for %%f in (*.proto) do (
protoc --proto_path=. --java_out="../../java" %%~nf%%~xf
echo %%~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);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
return super.decode(ctx, in);
}
}
编码器MyEncoder,用于把ByteBuf写入byte[]
public class MyEncoder extends MessageToByteEncoder<byte[]> {
@Override
protected void encode(ChannelHandlerContext ctx, byte[] sendBytes, ByteBuf out) throws Exception {
out.writeBytes(sendBytes);
}
}
修改GameMain,替换原本的编解码器
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected 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, 4
cp.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<Object> {
@Autowired
PlayerProtoHandler playerProtoHandler;
/**
* 读取数据
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
this.doRead(ctx, msg);
}
private void doRead(ChannelHandlerContext ctx, Object msg) throws InvalidProtocolBufferException {
ByteBuf in = (ByteBuf) msg;
byte[] bytes = new byte[in.readableBytes()];
in.readBytes(bytes);
// 读取协议号
int cmdId = ProtoPack.getCmdId(bytes);
ProtoPack.Pack request = ProtoPack.decode(bytes);
ProtoPack.Pack response = null;
if (cmdId == ProtoEnum.CmdIdEnum.PLAYER_LOGIN_VALUE) {// 登录
response = playerProtoHandler.login(PlayerProto.C2SPlayerLogin.parseFrom(request.getData()));
} else if (cmdId == ProtoEnum.CmdIdEnum.PLAYER_REGISTER_VALUE) {// 注册
response = playerProtoHandler.register(PlayerProto.C2SPlayerRegister.parseFrom(request.getData()));
}
ctx.writeAndFlush(response);
}
}
修改PlayerProtoHandler,使其通过传入的Protobuf协议来进行登录与注册
@Slf4j
@Component
public class PlayerProtoHandler {
@Autowired
private 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<java.lang.String> 自定义过滤规则,如果是null或者空,即全部符合不过滤
* @return Set
*/
public static Set<Class> scan(String basePackage, boolean recursive, boolean excludeInner, boolean checkInOrEx,
List<String> classFilterStrs) {
Set<Class> classes = new LinkedHashSet<>();
String packageName = basePackage;
List<Pattern> classFilters = toClassFilters(classFilterStrs);
if (packageName.endsWith(".")) {
packageName = packageName
.substring(0, packageName.lastIndexOf('.'));
}
String package2Path = packageName.replace('.', '/');
Enumeration<URL> 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<Class> classes, boolean excludeInner, boolean checkInOrEx, List<Pattern> classFilters) {
String packageName = basePackage;
String package2Path = packageName.replace('.', '/');
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> 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 class
if (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<Class> classes,
String packageName, String packagePath, boolean recursive, final boolean excludeInner, final boolean checkInOrEx, final List<Pattern> classFilters) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
final boolean fileRecursive = recursive;
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义文件过滤规则
@Override
public 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<Pattern> 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<Pattern> toClassFilters(List<String> pClassFilters) {
List<Pattern> classFilters = new ArrayList<Pattern>();
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<Integer, Commander> 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<Class> classes) {
Map<Integer, Commander> 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<Class> protoHandlerClasses = new HashSet<>();
Set<Class> 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,将其中我们手动进行消息分发的部分代码进行修改
@Autowired
ProtoDispatcher 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,方便后续我们进行协议的开发。
- 引入了协议号概念,并且针对协议号做了注解,进行客户端上行协议到具体模块的分发。