关于在Bash中从头开始编写Minecraft服务器的思考

2022-02-17 11:30:45

在过去的一年左右,我';我一直在考虑在Bash中编写一个Minecraft服务器作为一种思维练习。我曾经用经典的协议(2009年的协议)尝试过,但我很快意识到没有';这并不是在bash中正确解析二进制数据的一种方法。以下面的代码示例为例:

函数a(){read-n2 uwu echo";$uwu";|xxd}这将把两个字节读入一个变量,然后将它们传递给`xxd',后者将显示数据的十六进制转储。

一切';很好,直到我们传递一个空字节(0x00)。Bash不仅忽略字符串中的空字节,还不忽略';不提供任何方法来检测是否发生了空字节。考虑到第一号议定书';我试图实现的是严格的二进制,这可能会严重损坏数据。

一月底的一个雨夜,我';我有一个启示。如果我颠倒了函数的顺序呢?如果二进制数据从未到达一个变量(或者更准确地说,是一个替换),只是停留在管道中,它能传递空字节吗?

答案是肯定的!经过一些迭代之后,我决定使用'dd'传递给'xxd',而不仅仅是'xxd',因为这样我可以微调要读取的字节数。

#$len变量在前面被赋值,基于一个类似的read function a=$(dd count=$len bs=1 status=none | xxd-p)这给了我一个十六进制字符串,我可以在这个字符串上进行模式匹配、模式替换、数据提取。。。还有更多。发送回复可以类似地进行,使用xxd和#39;s倒车开关。

`ncat`用于收听Minecraft';默认的TCP端口。它在收到传入连接后启动主shell脚本(`mc.sh`)。

注意:以下部分主要是我在Bash中实现数字转换例程的漫谈;如果你对此不感兴趣,可以跳过它。

Minecraft服务器要运行,首先应该实现的是服务器列表Ping数据包,而不是因为它';这是必需的(见鬼,你的服务器无法正确回复,你仍然可以加入游戏),但因为它';这是最容易解决的第一个问题。它有助于您熟悉核心协议概念,例如数据类型:

大多数数据类型的实现都很简单,但有些数据类型比其他数据类型更让我头疼——尤其是IEEE754浮点数(稍后会有更多介绍),以及所谓的VarInt/VarLong数。那些人可能熟悉MQTT协议,因为他们';我们只是LEB128编码的一个修改版本。

LEB128是一种整数压缩方案。通过将一个字节拆分为1个信号位和7个数据位,该方案存储数字长度。如果第一位为0,则该字节为最后一位;除此之外,还有';在这个字节之后是另一个字节。如果你的大多数数字在0到127之间或256到16383之间,那么这是一个很好的方案,否则就是';“买一个字节,得到一个自由”的情况,因为本来可以放在一个字节中的数字会被一位推到下一个字节。

#从src/int.sh#int2varint(int)函数int2varint(){local a local b local c local out=$(printf';%02x';";$1";)如果[$1-lt 128]];然后:以利夫[$1-lt 16384];然后a=$($1%128))b=$($1/128))out=$(printf";%02x";$(a+128))$(printf";%02x";$b)elif[$1-lt$(128*128*128));然后a=$($1%128))c=$($1/128)%128))b=$($1/16384))out=$(printf";%02x";$(a+128))$(printf";%02x";$(c+128))$(printf";%02x";$b)fi echo-n"$出去";}我';我在将参考实现转换为Bash时遇到了问题,所以我使用了足够多的协议,从头开始编写自己的协议。我发现它基本上是一个挖沟机中的模和除法,我在上面的代码片段中利用了这一点。

我对解码器采取了一种更现代的方法,使用AND,然后将结果相乘——类似于引用的方法。

LEB128肯定不是';它不是最难实现或最烦人的(这一个是IEEE754浮点);我仍然不知道';I don’我不喜欢它被散布在协议内部的随机位置,与常规整数(和长整数)交织,在某些情况下甚至是有符号的短整数。

我';我不是数学爱好者。当我看到Python发出的指数符号时,我尖叫着跑开了。这可能是我讨厌实现这些浮点转换器的主要原因。我赢了';不要深入探讨这种格式的具体工作原理——相反,我建议你查看这个维基百科页面。

基本实现需要一个循环,其中有';对结果施加负幂;Bash没有';我天生就不支持消极力量,这让我不得不去寻找一个能做到这一点的工具。

我在DukDukEk中发现的一个建议是使用Perl,但我认为这是作弊。或者,尝试使用“bc”,但似乎要么没有';根本不支持电源,或者busybox版本不支持。真倒霉

当我准备放弃的时候,我被提醒凯特曾经在awk做过一个情节规划。当然,awk有能力吗~~甚至可能是超级牛的力量事实证明确实如此!

$echo和#39' | awk和#39;{print(2**-1)}';0.5有了这些知识,我草草画出了一个工作实现,并将其附加到从播放器移动数据包解码的数据中。在一次试运行中,客户机发送了大约50-100个这样的数据包,每个数据包有三个双倍(X、Y、Z)。结果是转换功能太慢了,服务器没有';在几分钟后,我无法完成这项工作——这对于实时游戏来说是相当不可接受的。

降低响应时间的最简单解决方案是减少对外部二进制文件(如awk)的调用量。由于我的大部分工作负载已经在bash`for`循环中,所以我将循环移到了`awk`中,这样就节省了对awk的数十次调用。

# (...)asdf=$(cut-c 13-<;<;<;$val | sed-E';s/,&;/g;s/,//&#tr-d';n&#awk-F,'+{power u count 1x=0;对于(i=1;i<;=NF i){x=(x+($i*(2**power u count u))power打印(x+1)';#(...) 转换仍然很慢(在我的Xeon E5-2680v2上需要约10毫秒),但这在bash脚本中是意料之中的。相比之下,旧版本大约需要350毫秒,但我没有';我没有可靠的测量数据来证明这一点~~还是快了35倍,哇~~

最后,莫强自己编的东西!Position是一个64位长的值,其中三个坐标并排存储:X获得26个最高有效位,Z获得26个中间位,Y获得12个最低有效位。我';我不太喜欢这种奇怪的数据类型,但在Bash中实现起来非常简单,因为它有所有需要的位移位运算符。

这种数据类型最糟糕的地方在于它没有';我其实没怎么习惯。大约一半的数据包将X、Y和Z坐标存储为单独的双变量。这意味着:

如果假设我们';We’我们只需要不超过3000000的数字(默认情况下,世界边界在哪里)

我有点明白为什么会这样';是这样的,但我还是不';我不喜欢现在的状态。不管怎么说,正常的服务器通信都使用zlib,而您实际上赢了';I don’描述一个块中的一个位置永远不需要超过两位(最多三位)的十进制精度。

最后是';这是NBT格式,也是Mojang Hatsune Miku自己制作的内部产品。NBT类似于JSON,但用于二进制数据。与JSON不同,它被滥用来存储超出规范的任意数据——例如,Mojang将可变长度的比特流存储为一个长数组;如果这样的数组不是';t long-aligned,甚至字节对齐,最后一个long用零填充。

有一次我';我几乎完全实现了一个NBT解析器实现,但我觉得不值得费心去完成它。由于我广泛使用tmpfs作为项目目录,以及系统崩溃,代码目前已丢失。

所有的数学问题都解决了,接下来就是“真正有趣”的部分。我在Twitter上记录了我的一些冒险经历,但那条线索只是对实际开发过程的一个粗略了解。还有,让';假设我们已经有了服务器Ping数据包,它';这是一个让游戏可以加入的问题。

为了让客户端加入您的服务器,它必须完成握手过程并发送一些额外的数据包(区块、玩家位置、库存、加入游戏)。这一过程中最大的两个障碍是Join Game数据包和Chunk数据包中的数据结构。

join游戏会发送一些初始元数据:player';s实体ID、游戏模式、一些关于世界的信息,以及自~1.16以来的a";Dimension编解码器";。这是一种NBT化合物,包含以下字段:

Dimension编解码器部分是实现的一大难题。出于我的目的,我决定从一个普通服务器检索NBT字段。它';它是这个实现中唯一的二进制blob,虽然它可以重新实现,但我不';我看不出有任何理由重新实施我不知道的事情';不一定需要(或想要)定制。

乍一看,这个包裹看起来又大又吓人!如果上面链接中的表与本文并排打开,我请您想象一下,这些巨大的位集字段中的每一个实际上都是“0x00”,而您没有';根本不需要发送块实体字段。这就给我们留下了X、Y、高度图(它们是“b000000010”的奇特编码重复,几乎可以是任何东西),以及不祥的数据字段。没那么可怕吧?

数据字段实际上是块段的数组。一个块段是16×16×16块,多个块段可以堆叠在一起创建一个块。出于我们的目的,这个数组只有一个元素,只是为了简化代码。

块段包含块计数、块状态容器和biome容器。这两个容器都使用调色板结构对可能的块值进行编码——这意味着在生成真正的块数据之前,服务器必须从";本地和#34;区块ID,至";全球和#34;阻止ID。其目的是在一个小空间内挤压尽可能多的数据——一个块定义可以小到4位。

在我看来,维基页面没有';我解释得不够好,不能很快理解;这是另一幅画:

对我来说,我发现发送这些字段数据管理的最简单方法是使用8位(而不是最小的4位)块定义长度。这将给我一个惊人的256个可能的调色板条目,从所有可用的块中选择。然后,编写实际的区块数据就像发送引用这些调色板条目的十六进制数一样简单。4位调色板也同样简单(一个十六进制字符串表示的字节是两个字符,每个字符表示4位,所以“0x01”将表示两个块——一个id为0,另一个id为1),但它会将我限制为每个块16个块。

该标准实际上允许从每个块4位到9bpb的任何内容,否则它假设一个15bpb的直接调色板映射——我也不知道为什么它不是';t字节对齐。

在我的实现中,biome调色板的工作方式有点不同——它只发送一个空调色板,然后将biome ID 0x01(minecraft:plains)直接映射到区块内的所有区域。这是基于我对香草如何工作的逆向工程——我怀疑包中这部分的现有文档是不正确的,因为我';我要么得到太多数据,要么每次都丢失几个字节。

到目前为止,我们只有一块普通的,没有任何特别的东西。我当然想做一些演示,展示服务器可以做的*更多*而不仅仅是加载和显示块,但我没有';我不想为我的每个演示创建一个单独的源代码树。我的解决方案是一系列我称之为“hooks”的可重写函数,以及服务器加载自己代码的选项。这允许任何事情,从改变世界的外观,到连接pkt_效果,让玩家在移动鼠标时发出滴答声。下面我附上一个简单的(未优化的)";插件";这会从默认调色板中生成一个包含随机块的块,这会产生一个奇怪的结果';这在视觉上有点有趣。

#!/usr/bin/env bash#地图。sh-简单地图修改展示函数hook_chunks(){chunk_headerfor((i=0;i<;4096;i++);dochunk+=";$(printf';%02x';$(随机%30)))"donechunk#u footeecho和#34$块和#34>$TEMP/world/0000000000000000 pkt_chunk FFFFFFFFFFFFFF00pkt_chunk FFFFFFFF00000000pkt_chunk 00000000 FFFFFF00pkt_chunk 00000000 pkt_chunk 00000000000000000000 pkt_chunk 00000000000000 pkt_chunk 00000001 FFFFFF00pkt_chunk 00000001 00000000 pkt_chunk 00000001它';这是一个简单的高分游戏,它把你扔到一块随机放置的石头和矿石上。挖出最有价值的矿石,直到计时器耗尽!

众所周知,Bash不擅长处理十进制数。它';对于整数,s*ok*(只要你不';对它们做太多高等数学运算),但处理十进制数的唯一方法是在输入时将其相乘,然后在正确的输出位置放置一个点。正因为如此,大多数(如果不是全部的话?)巫术处理的数字是整数。

多人游戏没有';真的不行吗?我的意思是,有点像,但我从来没有真正花时间来完成它和擦亮它。

... 这意味着它必须使用可怕的黑客在线程之间进行通信。目前,大多数全局数据存储在“/dev/shm/witchcraft”下,内部称为“$TEMP”。

巫术很慢,尤其是在多线程之间的数据交换方面。唐';I don’我不希望能够发送大量数据,生成和发送16个实体块可能需要一秒钟的时间。

巫术目前只有在安装了最新的BusyBox(1.35.0)的情况下才能运行。我没有';我没有用GNU coreutils测试它,但我预计它会赢';不行。

答:巫术内部ID在src/palette中定义。sh,可以在"中重新定义;插件";。可以从服务器获取内部ID映射到的外部ID。查看有关数据生成器的参考页。

答:selfisekai提出了这个名字,可能是因为我';我是个女巫,我觉得很棒*

大家在2022年2月15日16:40:40乐于助人在2022年2月15日16:59:22这个项目非常酷,但你的字体选择让它非常难以阅读。这个黑底灰的评论框简直是疯了!:D

萨菲尔2022年2月15日17:10:48哦,天哪,这是诅咒。比我在bash中看到的HTTP(s)服务器更重要。。。我喜欢~!。。。等等,这是minecraft字体。与之前的评论一致,灰色上的黑色很难理解

《新读者》于2022年2月15日17:48:56作为一个对bash知之甚少的人,这本书读起来非常有趣。我也喜欢这个网站!:)

2022年2月15日17:49:01 josé2022年2月15日18:06:37 I';我几个月前就有了这个想法,但我没有';我不认为这是可能的。这太棒了!干得好。

2022年2月15日阿图尔,2022年2月15日18:15:27莉莉,2022年2月15日18:17:05预取,2022年2月15日18:22:08拉斐尔,2022年2月15日18:49:27丹尼尔,2022年2月15日18:49:40凯尔杜,2022年2月15日19:20:46那';这太疯狂了。我在C++中编写了一个小型MC网络实现,在我开始看到它们如何随机地改变不同版本的数据包后,放弃了。我没有';我不想跟上。但在bash for MC中这样做太疯狂了。我回去为MC编写了一个小型反向代理服务器。

西奥:2022年2月15日19:36:33你的网站很棒。你的帖子很棒。你的一切都很好。继续工作,它';这绝对值得。。。

比金2022年2月15日19:39:06我喜欢这个。如果我能做点有价值的事,我会告诉你的。感谢这个有趣的非传统项目。

2022年2月15日,20:03:07米凯尔的节目很酷!请问花了多少时间?我不';我真的不知道协议有多复杂,或者每个测试需要多长时间,比如你是否需要重启客户端等等。

2022年2月15日的anon,2022年2月15日的20:53:12,2022年2月21日的21:16:27这是一篇关于这一过程的精彩文章。学习编码的最佳方法之一是做或看";如果我做了这件愚蠢无意义的事";然后看到所有的陷阱

2022年2月15日的eggmtf,2022年2月15日21:19:43的hyperupcall,2022年2月15日21:25:36我做了很多Bash和lemmie告诉你,这真是太糟糕了!有人会说';它被诅咒了,但我觉得它很酷——谢谢分享^w^

2022年2月15日,22:03:16,为"的黑客们欢呼;在Bash";,漂亮的字体让我想起一些VGA的东西。

2022年2月15日,22:17:18,2022年2月16日,01:34:09,a nat you don';我不知道2022年2月16日02:19:17 chip at 16.02.2022,2022年2月16日02:49:23 ayo,2022年2月16日06:12:39 trekkie1701c,2022年2月16日06:16:55,你可以通过BCMath将方程传输到bash脚本中,在其中巧妙地处理浮点数。如果我';我不得不这么做,它';s相当于:var=$(echo";scale=9;$num1/$num2";|bc),其中9是小数位数和/是操作数,可以用任何数字替换。这可能效率很低,但它确实有效。

2022年2月16日08:19:20安诺2022年2月16日11:11:57米2022年2月16日11:21:37这正是我喜欢读的那种疯狂的东西!期待更多这样的事情!:3.

Charles Duffy于2022年2月16日13:13:19在bash中完全可以读写包含NUL的流——诀窍是将它们存储为数组(终端元素包含最后一个NUL之后的所有内容),而不是字符串。而且,echo总体上是一个令人憎恶的东西,甚至描述它的POSIX规范也说应该使用printf——寻找Stéphane Chazelas对";为什么printf比echo更好" 在unix上。斯塔克交换。通用域名格式

2022年2月16日的RJM、2022年2月16日的19:12:40 RSEA、2022年2月16日的20:00:24 Leiger Gaming、2022年2月16日的22:23:12 Wensz、2022年2月16日的22:42:09通过评论,您同意将会话cookie存储在您的设备上;P