一种使用 /transfer 的轻量化子服方案

李华是一个生电服的玩家,他总是在想,要是服务器里面有创造实验室就好了。 小叶是一个喜欢 PvP 的玩家,他总是在想,要是服务器里面有一个能干脆利落开启决斗的地方就好了。 作为这个服务器的 OP,这些场景对你来说不陌生。如果把这两个问题抛在 Minecraft 服务器社区里,相信绝大多数的回答都是「开一个新的子服务器并且建立跨服群组」,但是,这真的值得吗? 显然,这不值得。如果只是为了这两样功能启动一个跨服群组,你将会面临以下问题: 额外的维护成本: 使用群组服则基本排除了面板服务器部署方案的可能性,并且一个没有适当配置过的群组服务器会引起诸多问题,例如:玩家使用 /server 跳过验证关卡,子服务器的端口意外的暴露在外网等等。 资源需求上升: 启动额外的子服 + 代理程序则意味着你需要更多的资源。Java 主流 GC 的策略通常都会为了速度而将堆所占的空间悉数预分配,这导致即使程序使用的内存并没有那么多也会造成较高的 RSS. **资源分配不灵活:**上一个问题并不能简单的通过减少堆的大小解决。就以创造实验室为例,平时服务器内的人并不多,但一旦到了演示/大型机器实验时,较小的堆空间将会给 GC 带来相当的压力,并且无法利用到分配到其他(类似情况的,比如 PvP 竞技场)子服的空闲资源。 …… 光是想到从单端变群组,大多数服主就已经汗流浃背了。那么,本文所述的基于 transfer 机制的方案是如何规避这些问题的呢,他与其他类似方案又有何优劣之分呢? 基于世界的子服划分方式 这个情境中使用群组方案,归根结底是为了做服务器区域之间的划分。而 Minecraft 自带的多世界机制就已经很好的解决了活动空间的划分问题:没有传送门的情况下,玩家并不能随意大小跨。 早在 2013 年,PerWorldPlugins 就已经实现了将插件的生效范围降级到世界层次。通过切割插件在整个服务器中的作用域,这些世界得以模拟「服务器的子服」,使用此插件的用户可以通过限定不同世界里启用的插件来模拟多服务器的效果。而在约 2020 年,Valorin 的 DuelTime / Dantiao (付费) 将这个概念进一步推进,完全实现了本文引入部分时提到的「干脆利落开启决斗的地方」的设想。 然而,这样实现引入了许多需要额外维护的状态:生存服的玩家不能将物品带到创造服,他们也不希望在 PvP 服丢掉自己一身的好装备。插件需要小心翼翼的维护好这些状态,在跨世界转移的时候将他们的背包数据暂存起来,甚至有必要储存到硬盘上以防 崩服/断电/插件自身异常 导致数据丢失(数据无价!)。 并且,除了物品数据,还有许多问题需要考虑,比如玩家身上的 scoreboard / team 数据,其他插件储存的玩家相关的状态数据都可能会导致数据切割不干净。甚至如果这些插件涉及到储存功能,例如类末影箱类插件,邮箱等,被滥用的后果不堪设想。 而本文所述的方案使用 transfer 特性解决这些问题。 Transfer 是什么 /transfer 命令 是一项在 1.20.5 才被加入的新特性。如果你不知道他,只需要看这条命令的语法就可略知一二: /transfer <hostname> <port> [players] ...

July 17, 2024 · 5 min · 1018 words

「9+」 Java 编译器插件教程 101

写了上万行 Java 代码,相比你已经对 Java “木纳呆板"的语法恨之入骨了。 那么,有没有一种可能,我是说可能,我们可以给 Java 编译器写插件? 如果你要在面包店里买面包,你最好跟店员说你要买哪块面包。本文所指的 Java 编译器 均为 OpenJDK 自带的那个 javac,而不是其他的前端编译器实现。 前言 自从 Java 8 起,Oracle 就将插件系统引入了 javac 中,因此是可以写 Javac 插件的。 虽然写一个 Javac 插件并且修改代码是可能的,但是这方面的资料很少(国内就更少了),并且大多内容重复(教你写个 HelloWorld 然后就结束),其次 javac 的东西也不是很好摸,毕竟不是公开 API( JDK 9+ 开始已经不暴露在外了)。 恰逢今年还没写技术类的博客,就拿来磨磨刀吧。 先从插件说起 上文刚刚提到了一个词,插件。它其实正是 com.sun.source.util.Plugin。通过这个类,我们可以让 Javac 在编译时加载我们的代码。 加载了代码,那么做什么呢?所以我们立一个目标:给所有 @Jsonized 标注过的类都生成一个可以输出 json 的 toString() 方法。 什么意思呢?就比如说这样一个类: public class MyResponse { private final String message; private final boolean success; public MyResponse(String message, boolean success){ this.message = message; this.success = success; } } 然后你想把它序列化成 Json。 var response = new MyResponse("\"Success!\" Took me 114514 yr 1919810 m",false).toString(); response.equals(""" {"message": "\"Success!\" Took me 114514 yr 1919810 m", "success": false} """); 又快又便捷,还不需要第三方类库,这就是我们的目标。 ...

July 6, 2022 · 7 min · 1436 words

浅谈 Java 虚拟机是如何标识垃圾的

Java 作为一门 VM 语言,它的垃圾回收机制确实帮我们省了很多事情,我们不再需要去"手动管理内存的分配和释放",只需要交给 VM 来做就好了。 然而,真的是这样吗?即使有神一般高性能的垃圾回收器,我们写代码时仍然需要注意它是如何标记垃圾对象的,因为垃圾回收器并不是万能的,仍然有一些工作需要程序员自己完成。 本文试图通俗易懂的讲解 JVM 上标记垃圾的方法,如有错误请在评论区指正。 两种标记垃圾的方式 或许你曾听闻过 引用计数法,也就是 一个对象被引用时计数器 + 1 ,解除引用时计数器 - 1,当计数器为 0 时将会被 GC,看起来非常可行。 但是这种方法没有被 Java 采用,因为他有两个显而易见的问题: 循环引用问题 如果一个对象内部引用了另一个 引用这个对象的 对象,那么计数器将永远不会为 0 计数器的维护问题 引用计数器的值会以极快的速度更新,更新任务变得繁重 或许因此,Java 采用了 可达性分析 的方法对垃圾进行标记。 可达性分析 可达性分析的思路很简单。 他从一组叫 GC Root 的引用出发,递归搜索出所有能被到达的节点作为存活的对象,而此外那些没有被搜索到的对象就会被标记 将被清理。 途中,被蓝色尖头指向的对象将不会被清除,因为他们间接或者直接的被 GC Root 引用。而旁边没有被 GC Root 引用的两个对象将会被清除,无论他们之间有什么关系。 不久,因为 Garbage F 和他的朋友 Garbage E 没有来自 GC Root 的直接/间接引用,他们就会被 gc 回收掉了。 想想看,如果在这个图中 Object C 建立了到 Garbage F 的一个引用,会发生什么? ...

November 5, 2021 · 1 min · 182 words

从零开始的编译器生涯

近日一屑高二学生无聊动手写起了编译器….这是他的珍贵作战记录 0x01 理论基础 我摊牌,我没有看任何编译原理相关的书籍,因此这篇文章并不能作为严格的参考资料,甚至很多地方可能是错误的。 编译器,编译器,就是把高级语言的代码编译成另一种形式(class,asm,二进制,IR),而他在编译成另一种形式之前大概需要过这么个流程: Lex -> Parse -> Compile 接下来逐步讲解这个过程。 Lexer 就是分词器,输入用户提供的代码接着把他分成 tokens,也就是 tokenstream。 你肯定看不懂上面那句话的意思,让我们来点实例: a.value 里的那个 ArrayList 就是一个 token stream,str 是被解析的代码。不难发现,语句被 Lexer 按顺序进行了分类以及数据的分割,如 a 被识别为了一个 Identify (标记)。 因此也可以归纳出来 Token 大致的代码长啥样: @AllArgsConstructor @Getter public class Token { private int line; private Type type; private String content; } Parser Lexer 从源码中提取出 token stream 后将会交给 Parser 处理,它负责对 token stream 进行解析,生成一个 AST (Abstract Syntax Tree),也就是 抽象语法树。 这张图直观的描述了这一过程,你可以看到它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。 接着,AST 将会丢给代码生成器用于生成代码,但是一般会先对 AST 进行优化,例如 常量折叠 Static Analyzing 但在这之前,我们还有一些问题要解决。 其实这玩意我是和 Parser 写一块的 试想一下,如果有这样一行代码: ...

October 1, 2021 · 7 min · 1396 words

使用 cproxy 对程序进行透明代理

在 Windows 下,说到 透明代理 ,你可能会想到 Proxifier , SSTap, Mellow 但在 Linux 下,你可能会想到 proxychains 然后再想到 proxychains 不能代理 static-linked 的程序。 为什么 ProxyChains(NG) 不能代理静态连接的程序 ProxyChains NG 通过一个预加载的共享库来hook到网络相关的函数(比如: connect,getaddrinfo)上,接着他就可以转发这些流量。 缺点似乎显而易见,如果只是通过 LD_PRELOAD 来进行透明代理,那么这招对于静态连接的程序是无用的(比如说 Go 程序..) 其次,ProxyChains 只支持 TCP,这可能导致无法转发 DNS 流量 -> DNS 流量泄漏。 cproxy 是什么 cproxy 也是透明代理,作用和 proxychains 类似。 原理上与 proxychains 不同,它使用 cgroup 进行对程序的代理 ,不过配置起来还有点麻烦 本文将会教您在您的Linux PC上使用 cproxy,截至本文发布之前,cproxy似乎只支持 Linux。 下载 cproxy 官方推荐的方法是: 直接用 cargo cargo install cproxy 如果没有 cargo 可以在 Release 上下载压缩包。 解压压缩包,可能会发现有两个可执行文件,不过似乎只留一个就好了。 安装 cproxy 先来试一试 cproxy! ...

March 13, 2021 · 1 min · 131 words