简介
Noark是一个游戏服务器端框架,可快速开发出一个易维护、易扩展且稳定高能的游戏服务器,让开发者专注于业务功能的开发
实现了配置注入,协议映射,模板加载,数据存储,异步事件,延迟任务,内部指令等功能模块
从而达到了松散耦合的效果,提高了系统的可重用性、可维护性以及可扩展性
精心设计过的它大大简化了网络编程和多线程编程,众多的工具类库就是为了解决开发中那些重复劳动而产生的框架
优点:
使用简单,学习成本低
功能强大,很容易写出性能优秀的服务
十分灵活,并且可与常用技术无缝衔接
安装
Gradle
implementation "xyz.noark:noark-game:3.1.18.Final"
当前需要Jdk1.8,Noark版本最新已是3.1.18了引入Noark,按照历史惯例,先来一个Hello Kitty...
0x01Hello Kitty
第一个游戏服务器Demo,来开始我们的ABC三步走
A、Application应用启动入口
在【com.company.slg】包下创建一个入口类
package com.company.slg;
import xyz.noark.game.Noark;public class GameServerApplication {public static void main(String[] args) { Noark.run(GameServerBootstrap.class, args);}
}
B、Bootstrap启动引导入口在【com.company.slg】包下创建一个引导启动类,继承BaseServerBootstrap
package com.company.slg;
import xyz.noark.game.bootstrap.BaseServerBootstrap;public class GameServerBootstrap extends BaseServerBootstrap {@Override protected String getServerName() { return "game-server";}
}
C、Configuration配置中心这个不是必选项,用于配置第三方服务类
package com.company.slg;
import xyz.noark.core.annotation.Configuration;@Configurationpublic class GameServerConfiguration {}启动游戏服务器直接运行main方法,一个简单的游戏服务器就跑起来了
2018-08-16 18:23:38.178 [main] INFO AbstractServerBootstrap.java:62 - starting game-server service...
2018-08-16 18:23:38.181 [main] DEBUG NoarkIoc.java:47 - init ioc, packages=com.company.slg2018-08-16 18:23:38.504 [main] INFO ReloadManager.java:41 - loading template data. checkValidity=true2018-08-16 18:23:38.504 [main] INFO ReloadManager.java:47 - load template data success.2018-08-16 18:23:38.504 [main] INFO ReloadManager.java:50 - check template data...2018-08-16 18:23:38.505 [main] INFO ReloadManager.java:52 - check template success.2018-08-16 18:23:38.505 [delay-event] INFO DelayEventThread.java:41 - 延迟任务调度线程开始啦...2018-08-16 18:23:38.505 [main] INFO HttpServer.java:72 - game http server start on 80802018-08-16 18:23:38.606 [main] INFO HttpServer.java:93 - game http server start is success.2018-08-16 18:23:38.606 [main] INFO NettyServer.java:119 - game tcp server start on 95272018-08-16 18:23:38.607 [main] INFO NettyServer.java:128 - game tcp server start is success.game-server is running, interval=427.21872 ms2018-08-16 18:23:38.607 [main] INFO AbstractServerBootstrap.java:76 - game-server is running, interval=427.21872 ms2018-08-16 18:23:38.609 [main] INFO AbstractServerBootstrap.java:166 - :: Noark :: 3.1.18.Final U u _ _ | |"| /"_ /U /" uU | _" u |"|/ / |___"/u <| | |> | | | | / / | |_) |/ | ' / U_| / U| | |u.-,_| |_| | / _ | _ < U/| . \u ___) | |_| _| _)-___/ /_/ _ |_| _ |_|_ |____/ || \,-. \ \ >> // \_,-,>> \,-._// \ (_") (_/ (__) (__) (__)(__) (__).) (_/(__)(__)源码下载0x02协议映射
负责完成协议请求到控制器的映射功能。
系统内置了一套简单的封包结构
包长(short)+ 协议编号(int) + 内容(Json)
BEFORE DECODE (306 bytes) AFTER DECODE (306 bytes)
+--------+------------+---------------+ +--------+------------+---------------+| length | opcode | Json Data |----->| length | opcode | Json Data || 0xFFFF | 0xFFFFFFFF | (300 bytes) | | 0xFFFF | 0xFFFFFFFF | (300 bytes) |+--------+------------+---------------+ +--------+------------+---------------+看不懂,没关系,先把协议跑通再说...1.先创建一个登录协议结构体,也就是一个标准JavaBean了
package com.company.slg.login;
public class LoginRequest {private String username;private String password;... 省略Get/Set方法
}
2.创建处理协议映射的控制器package com.company.slg.login;
import static xyz.noark.log.LogHelper.logger;import xyz.noark.core.annotation.Controller;import xyz.noark.core.annotation.controller.ExecThreadGroup;import xyz.noark.core.annotation.controller.PacketMapping;@Controller(threadGroup = ExecThreadGroup.ModuleThreadGroup)public class LoginController {@PacketMapping(opcode = 1001, state = State.CONNECTED)public void login(LoginRequest request) { logger.info("登录请求 username={}, password={}", request.getUsername(), request.getPassword());}
}
@Controller标识此类为一个协议映射的控制器,threadGroup参数为选择当前逻辑以模块为单位划分来处理,具体线程划分请参照Noark之线程模型@PacketMapping标识此方法为一个协议映射处理方法,opcode参数就是此协议的编号,state参数选择Connected,刚刚链接的状态登录方法目前什么都没有做只是简单的打印了一下请求账号和密码重启服务器,我们来写一个简单的测试客户端3.测试Socket协议
package com.company.slg;
import java.net.Socket;import com.alibaba.fastjson.JSON;import com.company.slg.login.LoginRequest;import xyz.noark.core.util.ByteArrayUtils;public class SocketTest {public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 9527); // 接头暗号,具体个性化功能请参考后续文档 socket.getOutputStream().write("socket".getBytes()); socket.getOutputStream().flush(); Thread.sleep(100); // 构建登录协议 LoginRequest request = new LoginRequest(); request.setUsername("abc"); request.setPassword("12356789"); // 模拟发送一个登录协议 send(socket, 1001, request);}private static void send(Socket socket, int opcode, Object protocal) throws Exception { byte[] body = JSON.toJSONString(protocal).getBytes(); // 包长是一个Short,就是协议编号的长度+协议的长度 socket.getOutputStream().write(ByteArrayUtils.toByteArray((short) (body.length + 4))); socket.getOutputStream().write(ByteArrayUtils.toByteArray(opcode)); socket.getOutputStream().write(body); socket.getOutputStream().flush();}
}
一个再简单不过的Socket链接了,接头暗号功能先不讨论,忽略就好,先来看一下服务器端的运行日志2018-08-17 16:29:17.941 [nioEventLoopGroup-4-1] INFO NettyServerHandler.java:48 - 发现客户端链接,channel=[id: 0xb81048ab, L:/127.0.0.1:9527 - R:/127.0.0.1:55129]
2018-08-17 16:29:17.954 [nioEventLoopGroup-4-1] DEBUG SocketInitializeHandler.java:43 - Socket链接...2018-08-17 16:29:19.107 [business-1] INFO LoginController.java:33 - 登录请求 username=abc, password=123567892018-08-17 16:29:19.107 [business-1] INFO AsyncTask.java:52 - handle protocal(opcode=1001),delay=0.285446 ms,exe=0.135813 ms2018-08-17 16:29:19.108 [nioEventLoopGroup-4-1] INFO NettyServerHandler.java:53 - 客户端断开链接,channel=[id: 0xb81048ab, L:/127.0.0.1:9527 ! R:/127.0.0.1:55129]链接>>判定类型>>登录协议处理日志>>协议执行日志>>断开链接协议映射功能就这么简单的实现了
源码下载
0x03配置文件
上面网络已跑通了,但要修改端口等配置呢?
在类路径下创建配置文件[application.properties]
服务器对外Tcp端口
network.port=10001
重启服务器,观察日志2018-08-17 17:02:08.342 [main] INFO NettyServer.java:119 - game tcp server start on 10001
2018-08-17 17:02:08.343 [main] INFO NettyServer.java:128 - game tcp server start is success.服务器端口已切换到10001了, 其他Noark系统默认的配置请参考Noark默认配置清单0x04模板加载
载入策划配置的模板数据了,Noark内置了一种CSV格式的模板解析器,简单方便,让我们来看个稀奇
配置中心GameServerConfiguration类中添加CSV模板解析器,参数templatePath为模板文件放置位置,后面那个Tab符,CSV文件中的分隔符
@Value("template.path")
private String templatePath;@Beanpublic CsvTemplateLoader templateLoader() {return new CsvTemplateLoader(templatePath, ' ');
}
配置文件也要配置上template.path参数template.path=E:\slg\slg-design\trunk\00数值配置\data
Gradle引导CSV解析工程implementation "xyz.noark:noark-csv:3.1.18.Final"
编码模板配置类package com.company.slg.chat;
import xyz.noark.core.annotation.tpl.TplAttr;import xyz.noark.core.annotation.tpl.TplFile;@TplFile(value = "Chat.tpl")public class ChatTemplate {@TplAttr(name = "Id")private int id;/** 频道名称 */@TplAttr(name = "Name")private String name;/** 最低发言等级 */@TplAttr(name = "MinLevel")private int minLevel;/** 发言间隔(单位:秒) */@TplAttr(name = "WordCd")private int wordCd;/** 所需道具 */@TplAttr(name = "Item")private String item;/** 消息长度限制 */@TplAttr(name = "WordLimit")private int wordLimit;... 省略Get/Set方法
}
编码模板管理类package com.company.slg.chat;
import java.util.Map;import xyz.noark.core.annotation.Service;import xyz.noark.game.template.AbstractTemplateManager;@Servicepublic class ChatTemplateManager extends AbstractTemplateManager {private Mapchat;@Overridepublic String getModuleName() { return "聊天系统";}@Overridepublic void loadData() { this.chat = templateLoader.loadAll(ChatTemplate.class, ChatTemplate::getId);}public ChatTemplate getChatTemplate(Integer id) { return chat.get(id);}
}
重启服务器,发现Noark会自动载入CSV文件了2018-08-17 17:17:05.572 [main] INFO ReloadManager.java:41 - loading template data. checkValidity=true
2018-08-17 17:17:05.572 [main] INFO ReloadManager.java:43 - [聊天系统] loading template.2018-08-17 17:17:05.585 [main] INFO ReloadManager.java:45 - [聊天系统] load OK.2018-08-17 17:17:05.585 [main] INFO ReloadManager.java:47 - load template data success.关于模板复杂属性的配置,请参考模板转化器为什么要选择CSV作为模板文件,请参考聊一聊策划配置表问题
0x05数据存储
数据存储,Noark采用了JPA风格的编码方式.
Gradle
implementation "xyz.noark:noark-orm:3.1.18.Final"
implementation "com.alibaba:druid:1.0.27"implementation "mysql:mysql-connector-java:5.1.40"配置数据源server.id=100
Mysql配置
data.mysql.ip=192.168.51.234
data.mysql.port=3306data.mysql.user=rootdata.mysql.password=Huiyu@123data.mysql.db=slg-game-${server.id}@Value("data.mysql.ip")
private String mysqlIp;@Value("data.mysql.port")private int mysqlPort;@Value("data.mysql.db")private String mysqlDB;@Value("data.mysql.user")private String mysqlUser;@Value("data.mysql.password")private String mysqlPassword;@Bean
public DataAccessor dataAccessor() {DruidDataSource dataSource = new DruidDataSource();dataSource.setDriverClassName("com.mysql.jdbc.Driver");dataSource.setUsername(mysqlUser);dataSource.setPassword(mysqlPassword);dataSource.setUrl(String.format("jdbc:mysql://%s:%d/%s?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false", mysqlIp, mysqlPort, mysqlDB));dataSource.setInitialSize(4);dataSource.setMinIdle(4);dataSource.setMaxActive(8);dataSource.setPoolPreparedStatements(false);MysqlDataAccessor accessor = new MysqlDataAccessor(dataSource);accessor.setStatementExecutableSqlLogEnable(true);accessor.setStatementParameterSetLogEnable(true);accessor.setSlowQuerySqlMillis(1);// 执行时间超过1秒的都要记录下.return accessor;
}
@Bean
public AsyncWriteService asyncWriteService() {return new DefaultAsyncWriteServiceImpl();
}
创建玩家信息实体类package com.company.slg.player;
import java.util.Date;
import xyz.noark.core.annotation.PlayerId;import xyz.noark.core.annotation.orm.Column;import xyz.noark.core.annotation.orm.Entity;import xyz.noark.core.annotation.orm.Id;import xyz.noark.core.annotation.orm.Table;@Entity
@Table(name = "player_info")public class PlayerInfo {@Id@PlayerId@Column(name = "username", nullable = false, comment = "账号", length = 64)private String username;@Column(name = "password", nullable = false, comment = "密码", length = 64)private String password;@Column(name = "name", nullable = false, comment = "名称", length = 128)private String name;@Column(name = "level", nullable = false, defaultValue = "0", comment = "玩家等级")private int level;@Column(name = "exp", nullable = false, defaultValue = "0", comment = "玩家经验值")private int exp;@Column(name = "online_time", nullable = false, comment = "上次上线时间", defaultValue = "2018-07-06 05:04:03")private Date onlineTime;@Column(name = "offline_time", comment = "上次下线时间")private Date offlineTime;@Column(name = "create_time", nullable = false, comment = "创建时间", defaultValue = "2018-07-06 05:04:03")private Date createTime;@Column(name = "modify_time", nullable = false, comment = "修改时间", defaultValue = "2018-07-06 05:04:03")private Date modifyTime;... 省略Get/Set方法
}
创建数据访问类.package com.company.slg.player;
import xyz.noark.core.annotation.Repository;
import xyz.noark.orm.repository.UniqueCacheRepository;@Repository
public class PlayerInfoRepository extends UniqueCacheRepository<PlayerInfo, String> {}这就完成了数据的存储功能,下面我们来改写一下登录逻辑.public class LoginController {
@Autowiredprivate PlayerInfoRepository playerInfoRepository;@PacketMapping(opcode = 1001, state = State.CONNECTED)public void login(LoginRequest request) { // 从缓存中取,如果没有,会自动从Mysql中取... PlayerInfo player = playerInfoRepository.cacheGet(request.getUsername()); if (player == null) { logger.info("账号不存在 username={}", request.getUsername()); } else if (!Md5Utils.encrypt(request.getPassword()).equalsIgnoreCase(player.getPassword())) { logger.info("密码不正确 password={}", request.getPassword()); } else { logger.info("登录成功 username={}, password={}", request.getUsername(), request.getPassword()); }}
}
重启服务器,发现Noark会自动为我们创建好player_info表2018-08-17 18:06:02.567 [main] WARN AbstractSqlDataAccessor.java:243 - 实体类[class com.company.slg.player.PlayerInfo]对应的数据库表不存在,准备自动创建表结构,SQL如下:
CREATE TABLEplayer_info
(username
VARCHAR(64) UNIQUE NOT NULL COMMENT '账号',password
VARCHAR(64) NOT NULL COMMENT '密码',name
VARCHAR(128) NOT NULL COMMENT '名称',level
INT(11) NOT NULL DEFAULT 0 COMMENT '玩家等级',exp
INT(11) NOT NULL DEFAULT 0 COMMENT '玩家经验值',online_time
DATETIME NOT NULL DEFAULT '2018-07-06 05:04:03' COMMENT '上次上线时间',offline_time
DATETIME NULL COMMENT '上次下线时间',create_time
DATETIME NOT NULL DEFAULT '2018-07-06 05:04:03' COMMENT '创建时间',modify_time
DATETIME NOT NULL DEFAULT '2018-07-06 05:04:03' COMMENT '修改时间',PRIMARY KEY (username
)) ENGINE=InnoDB DEFAULT CHARSET=utf8;运行Socket测试登录协议,由于刚创建的表,所以没有任何账号 SELECT username,password,name,level,exp,online_time,offline_time,create_time,modify_time FROM player_info WHERE username=abc
2018-08-17 18:07:02.953 [business-1] INFO LoginController.java:44 - 账号不存在 username=abc2018-08-17 18:07:02.953 [business-1] INFO AsyncTask.java:52 - handle protocal(opcode=1001),delay=0.21093 ms,exe=15.611207 ms源码下载0x06异步事件
用于多模块解耦功能,当完成一个动作时,向外抛出一个事件,由关心的模块自己监听处理.
引入事件管理器,发布一个上线事件
@Autowired
private EventManager eventManager;// 假装他登录成功了...
eventManager.publish(new OnlineEvent(1234));自己监听@EventListener(OnlineEvent.class)
public void handleOnlineEvent(OnlineEvent event) {logger.info("{} 上线了....", event.getPlayerId());
}
重启服务器,与测试Socket2018-08-17 18:18:36.797 [business-2] INFO LoginController.java:61 - 1234 上线了....
2018-08-17 18:18:36.797 [business-2] INFO AsyncTask.java:52 - handle event(OnlineEvent),delay=0.458517 ms,exe=0.146028 ms源码下载0x07延迟任务
Noark也提供了一套延迟执行的任务,就是带有延迟功能的事件,统一了API
编码一个延迟事件
public class OfflineEvent extends AbstractDelayEvent implements PlayerEvent {
private Long playerId;public OfflineEvent(long playerId) { this.playerId = playerId;}@Overridepublic Long getPlayerId() { return playerId;}
}
发布延迟事件// 模拟10秒后下线事件
OfflineEvent event = new OfflineEvent(1234);event.setId(123456);// 唯一编号event.setEndTime(DateUtils.addSeconds(new Date(), 10));eventManager.publish(event);@EventListener(OfflineEvent.class)
public void handleOfflineEvent(OfflineEvent event) {logger.info("{} 下线了....", event.getPlayerId());
}
重启服务器与测试Socket2018-08-17 18:30:10.057 [business-2] INFO LoginController.java:70 - 1234 上线了....
2018-08-17 18:30:10.058 [business-2] INFO AsyncTask.java:52 - handle event(OnlineEvent),delay=0.950687 ms,exe=0.092845 ms2018-08-17 18:30:20.057 [business-3] INFO LoginController.java:75 - 1234 下线了....2018-08-17 18:30:20.057 [business-3] INFO AsyncTask.java:52 - handle event(OfflineEvent),delay=0.235568 ms,exe=0.14092 ms上线与下线日志之间时间刚刚好是10秒