Noark入门之极速体验

Stella981
• 阅读 623

官方网站 www.noark.xyz

开源地址 https://gitee.com/xiaoe/noark3

简介

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;
@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]

#服务器对外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;
@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
#Mysql配置
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 ms
2018-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秒

源码下载

目标

我们的目标:稳定、高性能、可扩展、易维护、提高开发效率,我们没有要取代谁,也没有要超越谁,我们只做我们自己。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
PPDB:今晚老齐直播
【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这