2004年RuneScape在56k拨号网络中适应多人在线角色扮演游戏
在2004年,我在一台56k调制解调器上玩了太多的RuneScape,而这台调制解调器在妈妈接电话的那一刻就死了。一个3D世界,服务器上可容纳几千名玩家,同时屏幕上有几十个玩家 - 在浏览器中,以每秒5千字节的速度工作。它成功了。让我们从一个单一的步骤开始跟踪,看看是如何实现的。作为一个孩子,我太忙于采集亚麻和杀死哥布林,没时间思考这一切是如何运作的。然而,答案是对字节的不浪费进行持续的、几乎是强迫性的练习。所以,让我们点击我们站立位置的北方一个方块,追踪每一个字节从那次点击,经过服务器,直到另一位玩家的屏幕。中央喷泉,瓦罗克广场 方法论 # 这篇文章的细节来自反编译的2004年RuneScape 2客户端。片段是从该反编译中粗略翻译而来,经过排版整理以提高可读性,但逻辑保持不变。核心原则在不同版本中并不完全相同,但大多数原则一直延续到RuneScape Classic(2001年)到当今的RuneScape 3,当然还有Old School RuneScape。 约束 # 让我们看看Jagex当时所面临的一些约束。带宽。一台56k调制解调器以每秒56千比特的速率进行下行同步,上行速率更低,减去任何协议开销和线路噪音。假设为5 KB/s的下行速度和远低于此的上行速度。2000年时英国家庭已可以使用宽带,但直到2000年代末,大多数英国家庭才有宽带连接,因此很多玩家仍在使用拨号网络。 2004年,Java小程序在浏览器中运行。Java小程序在安全沙箱中运行,这意味着不能使用原始原生套接字和UDP。每个字节通过一个单独的TCP连接传输,按照顺序且每个段都有额外开销。600毫秒的服务器周期。RuneScape游戏服务器以大约600毫秒的离散周期(或更新)向前推进。每个周期,对于每位玩家,服务器都必须计算出该玩家现在可以看到的所有内容,并在下一个周期之前将其发送出来。密码层,简单地说 # 在登录握手完成后,在发送任何游戏数据包之前,会设置一个小的加密层。这一层并不是为了节省字节;它是堆栈中唯一的加密(除了在登录握手中的某些RSA加密),它之所以存在是因为它保护的操作码是后续所有章节依赖的东西。每个数据包以一个“操作码”字节开始:一个小整数,表示这是什么类型的数据包。这个操作码(只有这个操作码)使用一种称为ISAAC的流密码进行加密。存在两个流 - 一个用于客户端到服务器的流,另一个用于反向流。两边都需要这两个流:客户端加密即将发送的内容,并解密刚刚到达的内容,服务器以镜像的方式执行同样的操作(对于每个连接的玩家)。两个流都由一个共享的四个整数的密钥产生。客户端自行生成其中两个整数;另外两个由服务器作为握手的一部分提供。然后服务器到客户端的流使用相同的种子,每个字中加50 - 足够防止两个方向共享同一个密钥流:this.outboundCipher = new ISAAC(seed); for (int index = 0; index < 4; index++) { seed[index] += 50; } this.inboundCipher = new ISAAC(seed); 出站加密的方法很简单:public void putOpcode(int opcode) { this.putByte(opcode + this.outboundCipher.value()); } 而入站时则为镜像操作:this.currentOpcode = (this.currentOpcode - this.inboundCipher.value()) & 0xFF; 所以数据包主体并没有被加密,只有操作码被加密。正如我们稍后所看到的,操作码告诉你如何读取数据包的其余部分,以及一个数据包的结束位置和下一个启动的位置。如果没有它,主体只是一个字节墙,因此加密这一字节是对第三方数据包解析器的最便宜的防御。 发送步行请求 # 我们将看看单击北方一个方块时发生了什么,以及这如何被传输到服务器。在任何网络活动发生之前,客户端运行一个广度优先搜索,使用本地碰撞地图建立从你所在位置到你点击位置的路径(在这种情况下,这是一次简单的搜索),然后为服务器读写数据包。路径寻找是标准的,因此我不会在这里深入讨论。数据包的第一部分是操作码,后面是一个包含数据包主体长度的单字节。正如你所看到的,数据包中包含的字节数量取决于路径的大小,因此这个“长度”字节可以让服务器知道要读取多远。并不是所有的数据包都有这个长度字节,只有包含一些可变大小主体的数据包。起始位置占用4个字节(两个短整型),每个后续航点增量占用2个字节,最后还有一个字节表示是否按下了Ctrl键。因此主体长度为4 + 2 * (pathLength - 1) + 1。this.outboundStream.putOpcode(ClientToServerOpcodes.WALK_TILE); this.outboundStream.putByte(4 + 2 * (pathLength - 1) + 1); 数据包包含第一个航点的绝对位置
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡