簡介java
Noark是一個遊戲服務器端框架,可快速開發出一個易維護、易擴展且穩定高能的遊戲服務器,讓開發者專一於業務功能的開發mysql
實現了配置注入,協議映射,模板加載,數據存儲,異步事件,延遲任務,內部指令等功能模塊sql
從而達到了鬆散耦合的效果,提升了系統的可重用性、可維護性以及可擴展性數據庫
精心設計過的它大大簡化了網絡編程和多線程編程,衆多的工具類庫就是爲了解決開發中那些重複勞動而產生的框架編程
優勢:json
使用簡單,學習成本低bootstrap
功能強大,很容易寫出性能優秀的服務緩存
十分靈活,而且可與經常使用技術無縫銜接服務器
安裝網絡
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;
@Configuration
public 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.slg
2018-08-16 18:23:38.504 [main] INFO ReloadManager.java:41 - loading template data. checkValidity=true
2018-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 8080
2018-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 9527
2018-08-16 18:23:38.607 [main] INFO NettyServer.java:128 - game tcp server start is success.
game-server is running, interval=427.21872 ms
2018-08-16 18:23:38.607 [main] INFO AbstractServerBootstrap.java:76 - game-server is running, interval=427.21872 ms
2018-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=12356789
2018-08-17 16:29:19.107 [business-1] INFO AsyncTask.java:52 - handle protocal(opcode=1001),delay=0.285446 ms,exe=0.135813 ms
2018-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]
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;
@Bean
public 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;
@Service
public class ChatTemplateManager extends AbstractTemplateManager {
private Map<Integer, ChatTemplate> chat; @Override public String getModuleName() { return "聊天系統"; } @Override public 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
data.mysql.ip=192.168.51.234
data.mysql.port=3306
data.mysql.user=root
data.mysql.password=Huiyu@123
data.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 {
@Autowired private 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 TABLE player_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=abc
2018-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());
}
重啓服務器,與測試Socket
2018-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; } @Override public 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());
}
重啓服務器與測試Socket
2018-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秒