开源软件名称:Limitart
开源软件地址:https://gitee.com/HankXV/Limitart
开源软件介绍:
快速开始Limitart鼓励一种可热更新 且依赖注入 的项目搭建方式,具体可以参考project-starter 和project-logic 一.通信1.二进制通信快速开始 //创建一个消息 public class BinaryMessageDemo extends BinaryMessage { public String content = "hello limitart!"; @Override public short id() { return BinaryMessages.createID(0X00, 0X01); } } 为这个消息创建处理器 @Mapper public class BinaryManagerDemo { public void doMessageDemo(@Request BinaryMessageDemo msg) { System.out.println(msg.content); } } //让消息工厂实例化注册消息处理器 注意:这里可以调用Router.create("[包名]","[自定义实例]")的接口来配合脚本加载器(ScriptLoader)或单例注入(Singletons)来初始化 Router router = Router.empty().registerMapperClass(BinaryManagerDemo.class); 配置服务器实体 BinaryEndPoint.server() .router(router) .build() .start(AddressPair.withPort(8888)); 开启客户端连接并发送消息 BinaryEndPoint.client() .router(Router.empty()).onConnected((s, state) -> { if (state) { try { s.writeNow(new BinaryMessageDemo()); } catch (Exception e) { } } }).build().start(AddressPair.withIP("127.0.0.1", 8888)); 服务器日志+结果 [main] INFO BinaryMessageFactory - register msg BinaryMessageDemo at BinaryManagerDemo[main] INFO AbstractNettyServer - Limitart-Binary-Server nio init[nioEventLoopGroup-2-1] INFO AbstractNettyServer - Limitart-Binary-Server bind at port:8888[nioEventLoopGroup-3-1] INFO AbstractNettyServer - /127.0.0.1:54062 connected!hello limitart! 消息编码 消息长度(short,包含消息体长度+2)+消息ID(short)+消息体 2.Google Protobuf通信快速开始创建跟1无异,只是入口变为ProtobufEndPoint 3.简单HTTP通信开始 HTTPEndPoint.builder().onMessageIn((s, i) -> { if (i.getUrl().equals("/limitart")) { return "hello limitart!".getBytes(StandardCharsets.UTF_8); } return null; }).build().start(AddressPair.withPort(8080)); 通过浏览器访问 http://127.0.0.1:8080/limitart 得到结果 hello limitart! 二.热加载1.脚本源码模式所有脚本都需要在基础代码里声明接口并继承 public interface Script<KEY> { KEY key(); } 其中的KEY为此脚本的唯一标识,是基础代码找到脚本实例的钥匙,通过在基础代码中埋点调用此脚本的方法。 public interface HelloScript extends Script<Integer> { void sayHello(); } public class HelloScriptImpl implements HelloScript { @Override public void sayHello() { System.out.println("hello script!!!"); } @Override public Integer key() { return 1; } } 这里写一个简单的脚本,声明sayHello方法,并编写一个实现类,放在demo/script文件夹下。然后我们启动一个定时重加载(也可以不定时,通过手动来调用重加载)的脚本加载器来加载demo/script目录下的所有脚本。 FileScriptLoader<Integer> loader = new FileScriptLoader<>("./demo/script", 5); while (true) { Thread.sleep(1000); HelloScript script = loader.getScript(1); script.sayHello(); } 输出的结果如下 现在我们修改输出的内容如下,多了一段reload字符串 public class HelloScriptImpl implements HelloScript { @Override public void sayHello() { System.out.println("hello script!!!reload!!!!"); } @Override public Integer key() { return 1; } } 输出的结果如下,证明代码重新加载成功了 hello script!!!reload!!!! 通常这种热加载脚本的模式是把性能要求不高且修改频率高的逻辑剔出来,方便不停机修改逻辑,降低维护成本。应用的场景:游戏中活动逻辑的修改,副本逻辑的修改等。 2.agent模式agent模式是利用了jvm提供的接口,用java的api直接附加的jvm上进行相应操作的一种技术(有些破解java程序也是使用这个技术,比如IDEA)。 这种模式直接加载jar包,需要引用sun.tools本地包,使用起来也是非常简单,他的模型就是一个基础的程序,我们叫他启动器(bootstraper)。他启动的时候会去加载一个jar包,在这个jar包里不存在需要持久化的数据,大部分都为逻辑(可以理解为脚本的集合)。 因为模型分为两部分:启动器+脚本jar包,所以这里我们先创建脚本jar包,脚本包里面不再由main函数为入口,而是一个自定义的类,他继承自RedefinableModule,如下 public class ScriptEntrance extends RedefinableModule { private HelloRedefine helloRedefine = new HelloRedefine(); @Override public void onStart(RedefinableApplicationContext context) { System.out.println("onStart"); helloRedefine.hello(); } @Override public void onRedefined(RedefinableApplicationContext context) { System.out.println("onRedefined"); helloRedefine.hello(); } @Override public void onDestroy(RedefinableApplicationContext context) { System.out.println("onDestroy"); } } 其中HelloRedefine为本jar包里的一个逻辑,里面的方法很简单 public class HelloRedefine { public void hello(){ System.out.println("hello"); } } 我们的目的是修改这个方法后,让他打印出修改后的内容,所以我们在onStart和onRedefine的时候打印一次,也就是在初次加载和重加载的时候分别打印,他们应该是不同的结果(除非未修改脚本) 我们把这个逻辑工程打包为script.jar 接下来再创建启动器工程,通过创建RedefinableApplication来加载jar包 RedefinableApplication demo = new RedefinableApplication(new File("e://script.jar").toURI(),"top.limitart.redefinable.ScriptEntrance"); demo.run(args); 执行run方法,打印结果如下 然后我们马上修改HelloRedefine里的hello方法并重新打包 public class HelloRedefine { public void hello(){ System.out.println("hello,redefine!!"); } } 调用RedefinableApplication的reload方法实现重加载 打印结果如下 onRedefinedhello,redefine!! 证明我们重加载成功了。 关于重加载的注意事项,比如是否可以修改数据结构,是否可以增加方法,删除方法,请关注jvm中关于类加载相关的内容。 三.简单单例(Singleton)IOC容器首先IOC容器参考spring或者guice,本项目中的单例是最简单的一个实现,因为针对的应用场景也非常简单,比如:我有各种管理器,其中之一是玩家管理器,他只是做数据缓存和玩家的存取,整个项目中不会有其他实例,他就是一个单例而已。而且我并不想在其他地方调用的时候手动从某个地方去获取这个管理器,类似:Managers.getPlayerManager(),这样做代码非常冗余。这个时候我们就需要他自动注入这个管理器,随用随声明。 @Singleton public class SingletonA { @Ref SingletonB singletonB; public void say() { System.out.println(singletonB); } } @Singleton public class SingletonB { @Ref SingletonA singletonA; @Ref SingletonC singletonC; @Ref SingletonD singletonD; public void say() { System.out.println(singletonA); System.out.println(singletonC); System.out.println(singletonD); } } @Singleton public class SingletonC { @Ref SingletonC singletonC; @Ref public SingletonC(SingletonA singletonA, SingletonB singletonB) { System.out.println("construct C"); } } @Singleton public class SingletonD { @Ref public SingletonD(SingletonC singletonC) { System.out.println("construct D"); } @Ref public void setSingletonA(SingletonA singletonA) { System.out.println(singletonA); } } 上面的单例A,B,C,D分别演示了字段注入,构造注入,方法注入和循环引用。声明单例的关键字为:@Singleton,声明注入的关键字为:@Ref。 创建单例容器 new Singletons.Builder() .withPackage("top.limitart", SingletonDemo.class.getClassLoader()) .bind(SingletonDemo.class) .build() .instance(SingletonDemo.class) .say(); 你可以扫描你项目的package名称来加载全部单例,也可以在build前手动绑定(bind)一个单例。可以通过instace方法获取实例手动调用。调用结果如下 construct Cconstruct Dtop.limitart.singleton.SingletonA@f107c50top.limitart.singleton.SingletonA@f107c50top.limitart.singleton.SingletonC@51133c06top.limitart.singleton.SingletonD@4b213651top.limitart.singleton.SingletonB@4241e0f4 了解更详细内容,可以点击这里 四.并发和并行1.线程(消息队列或异步任务)在游戏服务器或中间件服务器中都不可避免的需要根据任务类型将任务派发到不同线程中执行,比如大计算任务(统计计算等)、大IO任务(数据库、网络等交互)。还比如,需要把N个用户放在一个线程交互,避免数据发生多线程问题,或者在RPG游戏中,保证同地图在相同线程中(注:不是一个地图一个线程,可能是N个地图分一个线程)。在此项目中,我们使用TaskQueue创建一个线程来异步处理任务,TaskQueue实现了Executor接口并增加了cron表达式的调度,如下: TaskQueue queue = TaskQueue.create("test"); queue.execute(()-> System.out.println("execute")); queue.submit(()-> System.out.println("submit")).get(); queue.schedule(()-> System.out.println("schedule"), 1, TimeUnit.SECONDS); queue.schedule("cron1", "0 22 14 11 12 ? 2018", () -> System.out.println("tick1")); queue.schedule("cron1", "*/5 * * * * ? *", () -> System.out.println("tick2")); queue.schedule("cron1", "0 * * * * ? *", () -> System.out.println("tick3")); 了解思路可以点击这里 2.线程组有些按逻辑划分的线程是需要一组线程来操作的,这里有一个简单的固定数量线程组TaskQueueGroup(我们并不推荐自动增长和自动销毁的线程容器,开销太大,不好把控)。下图创建有5个线程的线程组,并轮询执行任务 TaskQueueGroup group = new TaskQueueGroup("limitart",5,s->TaskQueue.create(s)); group.next().execute(()-> System.out.println("execute1")); group.next().execute(()-> System.out.println("execute2")); group.next().execute(()-> System.out.println("execute3")); group.next().execute(()-> System.out.println("execute4")); 3.线程切换模型有时候我们一个网络会话Session会去持有一个Netty的Channel或其他类型的网络链接,我们想使用Session.getChannel()之类的方式直接拿到这个链接实例(这样更加面向对象),而不是通过一个ChannelManager或者ChannelSet用某个key来获取。但是我们担心持有了Channel实例会影响网络那边的链接释放导致内存泄漏。 还有一个例子,比如我们有个游戏手柄对象,有N多玩家对象,玩家需要来抢占手柄,并且我能通过玩家来拿到手柄对象,手柄同时只有一个玩家或没有 具体的例子,在RPG游戏中,玩家需要切换地图,玩家同时只能占有一张地图或者不占有地图,我们需要切换玩家的地图线程,编程过程中我们可以直接通过玩家拿到他在哪个地图,而不是通过玩家去地图管理器去拿。 这些例子的目的如下 1.让使用者明确知道需要资源的切换 2.方便调用者面向对象的思维,拿到什么就可以拿到他相关的东西,而不需要其他的集合容器或工具。 3.不关心资源释放,拿不到时说明他没有了,比如玩家下线了、地图人物登出、网络断线等 此项目中定义了这个模型,名为Actor(占用者),线程资源占用者为TaskQueueActor,是Actor的具体实现之一。Netty网络链接的占用者NettySessionActor。 拿NettySessionActor占用Netty网络链接举例 BinaryEndPoint.builder(true) .router( Router.empty(BinaryMessage.class, BinaryRequestParam.class) .registerMapperClass(MessageMapper.class)) .onConnected(//当链接连接或断开时 (s, b) -> { if (b) { role.joinWhenFree(//role占用这个网络链接 s, () -> {System.out.println("join session success!"); NettySession<BinaryMessage> where = role.where();//获取当前网络链接实例 where.writeNow();//写出消息 }, Throwable::printStackTrace); } else { role.leave(s);//当网络链接断开,释放 System.out.println("leave session success"); } }) .build() .start(AddressPair.withPort(7878)); 其中,在onConnected回调里(s为网络链接实例,b为布尔型,表示连接或断开),我们可以让这个role去占用这个网络链接(joinWhenFree),后续可以直接调用role.where()来使用,当网络链接断开时,可以调用role.leave()来取消占用。想知道role是否有网络链接很简单,判断role.where()是否为空就行了。 顺带一提,这种方式在处理玩家顶号也是极好的。 资源占用是用弱引用WeakRefHolder实现的。 4.线程切换模型扩展这个模型其实可以用到任何一对一且不强关心资源释放的场景,比如玩家-地图,玩家-角色,角色-房间,玩家-帮会等等,项目中不再出现额外的资源管理器,只需要关心当前我拿到的对象他有什么,他可以做什么就行了。 五.基础包1.唯一ID很多场景我们需要生成唯一ID来标识对象的唯一性,方便做hash查找。比如玩家ID、房间ID。 区域的定义:我们不需要生成全球唯一ID,所以我们要根据不同的区域来实现该区域中的唯一ID。此项目中为AreaID,AreaID包含两个参数 majorID,主ID,范围1-255 minorID,次ID,范围1-65535 可以理解为省和市的关系。 唯一ID生成器:UniqueID,如果你不在意区域,可以用默认,也可以指定区域来区分不同区服,比如可以通过渠道编号+区服编号来组成AreaID。下面是使用方法,假设渠道ID为255,区服为1 int areaID = AreaID.areaID(Byte.MAX_VALUE,1); byte channelID = AreaID.majorID(areaID); int serverID = AreaID.minorID(areaID); System.out.println("key:" + areaID + ",channel:" + channelID + ",server:" +serverID); for (int i = 0; i < 10000000; ++i) { long createUUID = UniqueID.nextID(areaID); System.out.println(Thread.currentThread().getName() + ":" + createUUID); } 2.计数器有时候我们讨厌写a+=1,a-=1,a+=12,a-=22等,写多了都不知道哪是哪了,这里提供了IntCounter和LongCounter,虽然很简单,但是能帮助理清逻辑,后面集合包里的计数Map更是对加快开发起了重要帮助,这里不赘述。 具体可以点击这里 3.比较器函数生成器有时候写compare函数真的是让人头晕,是该返回1,0,-1?或者在long比较的时候越界了?这种情况只有在运行期才会体现出来,等出问题了就很头疼,这里提供了一个比较函数生成器CompareChain,参考的Apache的实现,使用方法很有过程化编程的风格。 private static final Comparator<Bean> COMPARATOR = CompareChain.build( (o1, o2) -> CompareChain.start(o2.price * o2.item.getNum(), o1.price * o1.item.num)); 4.持有器单个:Alone,两个:Couple,三个:Triple Alone,单个对象持有器,相当于包装一层,可用于某些回调中省去声明为final的麻烦(当然你要确定这个对象不会有多线程问题)。 Couple,相当于一对key-value。某些函数我需要返回一对数据,可以用这个。 Triple,同Couple 5.其他Condition:简单版Guava的PreconditionSingleton:单例的包装实现SmoothRandomWeight:平滑加权随机SmoothRobinWeight:平滑加权轮询URIScheme:通过资源定位符URI加载资源,文件,网络等WeakRefHolder:弱引用包装BinaryMeta:二进制元数据,二进制序列化的封装Environment:通过Properties文件加载key-value配置label包:各种标识性注解,非空@NotNull,可能空@Nullable,线程安全@ThreadSafe,非线程安全@ThreadUnsafe,调用者谨慎@CallerSensitive,枚举专用接口@EnumInterface,耗时操作@LongRunningfunction包:帮助函数式开发的各种函数式接口 六.集合1.排行榜需要排序的结构我们要实现RankObj接口,比如我们对道具来排序(拍卖行经常用) public static class Bean implements RankMap.LongRankObj { private long id; private Item item; private int price; public Bean(long id, Item item, int price) { this.item = item; this.price = price; this.id = id; } public Bean copy() { return new Bean(this.id, this.item.copy(), this.price); } @Override public Long key() { return id; } @Override public String toString() { return "Bean{" + "id=" + id + ", item=" + item + ", price=" + price + '}'; } } public static class Item { private int num; public Item copy() { return new Item(num); } public Item(int num) { this.num = num; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override |
请发表评论