CC的小站

  • 首页
  • 友链
  • 小游戏
  • 关于 / 留言板

mc的通信协议及其实现

发表于 2022-02-23 |

简介

本文是本人基于对wiki.vg的一些理解,以及从他人学到的代码来实现与mc服务器间通信协议的理解及实现。
同时包含了开发mCClient时遇到的一些问题,关于mCClient可以看前面的文章。
实现时使用Java,目前仅仅实现了使用UUID和AccessToken进行正版验证,本文不包含使用微软验证获取UUID和AccessToken。
本文具有一定的时效性,仅针对757协议版本(MC1.18)。

文章仅个人理解,不排除有一些错误的理解(虽然大概能实现对应的功能),欢迎大家多多指教。

提示:为方便阅读,可点击进入文章,使用左侧的书签。

正文

一、总览

通信的流程

由外层向内层来看:加密/解密 > 压缩/解压 > 数据包 > 数据。
和计算机网络的分层差不多一个意思,几层东西各自逐步实现,互不干预。

关键点:加密/解密的实现、数据包的压缩与解压、数据包的格式、数据类型的实现。
搞懂了这四点,这个方面基本上就没问题了(如果只为离线模式准备的,不需要第一点)。

建立连接

按照wiki上的就行,

  1. 客户端连接到服务器
  2. C → S:握手状态=2
  3. C → S:登录开始
  4. S → C:加密请求
  5. 客户端认证
  6. C → S:加密响应
  7. 服务器身份验证,都启用加密
  8. S → C:设置压缩(可选,启用压缩)
  9. S → C:登录成功

ps:离线模式不需要4,5,6。

建立连接后,每过一定时间服务器会向你发送保活数据包。收到之后需要及时应答,不然就给你算超时掉线了。

关键点:接收/发送数据包。

正版验证

关于使用微软验证我还没有搞,所以现在假设已经从微软验证那里搞到了你的UUID和AccessToken。

其实只需要把UUID、AccessToken和你要登录的服务器的ID,post到官方的网站。
等到服务器决定让不让你进来时,服务器再去官方的网站查查你是不是要进来。
如果上面post成功了(里面的东西没问题),服务器就会让你加入游戏,反之则加入失败。

关键点:请求格式、服务器ID的获取。

二、具体细节

数据格式

目前还没用到多少数据格式,其实总共应该也没多少。
只说几个特殊的,剩下的直接用DataInputStream/DataOutputStream里面的方法就行。

String

格式:UTF-8
在他前面会有一个VarInt标记字符串的大小。
后面跟上字符串的byte[]就行了。

UUID

long mostSig = readLong();
long leastSig = readLong();
return new UUID(mostSig, leastSig);

VarInt和VarLong

这个直接看wiki就行,上面已经给出了伪代码。

数据包的格式

有两种数据包:1、无压缩的数据包,2、带压缩的数据包
下面先说两者都有的元素:数据包长度,数据包ID,数据。最后再说两者具体的格式。

数据包长度

无论你是否启用压缩、是否真正压缩,数据包前面都会加上此数据包的长度,是数据包最前面的 一个VarInt。表明当前数据包(不包括数据包长度本身)的大小。

数据包ID

紧贴在数据前面的 一个VarInt,用来区分不同功能的包。发送时ID比数据早发送。

注意:不同的包在不同的游戏状态下的ID可能一样的,需要先确定当前的游戏模式。
例如:“登录成功”的数据包会将连接状态切换为play。这时对数据包ID的判断就需要变一下了,不能还按Login状态的来。

数据

按照wiki上所给格式,使用上面所说的各种数据类型,从上到下依次写入/读取即可。最后生成一个byte[]作为数据。

无压缩的数据包

还未启用压缩时(建立连接的第8步之前)或不启用压缩时,使用的数据包。

格式:数据包长度 + 数据包ID + 数据

数据包长度 = 数据包ID的长度 + 数据的长度

带压缩的数据包

一旦服务器向你发送了Set Compression数据包(建立连接的第8步)。(你和服务器)后续的数据包都会启用zlib压缩(只压缩ID和数据),采用“带压缩的数据包”的格式(无论其是否真正发生了压缩)。

格式:数据包长度 + [ 没压缩时的( ID及数据 )] 的长度 + (数据包ID + 数据)的压缩包 或 (数据包ID + 数据)

数据包长度

数据包长度 = { 没压缩时的ID及数据的长度 } 的长度 + [ 压缩后的 ( ID及数据 ) ] 的长度

没压缩时的ID及数据的长度

一个VarInt。表明(ID及数据)压缩前/解压后的长度,及上图的“压缩包原长”

注意:在(ID及数据)的长度小于阈值(由Set Compression得到)的时候“没压缩时的ID及数据的长度”为0,且数据包第三部分为“数据包ID + 数据”,不会被压缩。就是上图黄线所表示的那样。

ID及数据的压缩包 或 ID及数据

有关使用哪个 的判断方法,上面那个“注意”已经提到了。

关于压缩/解压缩下面会说。

数据包的压缩/解压缩

其实主要是解压就行了,解压从服务器发过来的压缩包。给服务器发的包可以偷个懒,直接不压缩(大部分情况下你的数据不会超过阈值)。

解压缩

使用“Inflater”这个类来解压缩。

private final Inflater inflater = new Inflater();

byte[] zip = new byte[len - VarOutputStream.checkVarIntSize(dataLen)];
packetBuf.readFully(zip);
byte[] unzip = new byte[dataLen];
inflater.setInput(zip);
inflater.inflate(unzip);
inflater.reset();
packetBuf = new VarInputStream(new ByteArrayInputStream(unzip));
id = packetBuf.readVarInt();
packetData = new byte[dataLen - 1];
packetBuf.readFully(packetData);

解释:

  • len:数据包长度
  • dataLen:数据长度
  • dataLen - 1 里的“1”:ID的长度
  • checkVarIntSize(<VarInt>):返回Varint的长度

主要就是中间那三行Inflater类的方法。

压缩

使用“Deflater”这个类就能压缩。用处不大,就没有写了。

加密/解密(离线模式不需要)

首先得搞清楚设置服务器与你设置加密的流程。

设置加密的流程

  1. S → C:服务器ID、服务器公钥、验证令牌
  2. C:生成一个共享密钥
  3. C → S:使用服务器公钥加密的 共享密钥和验证令牌

此时你和服务器都已经(秘密地)知道了共享密钥,后面的通信都会使用共享密钥进行加密。

为什么能“秘密地”?(选读)

第一次接触这种东西,感觉好神奇。下面的只是我自己的理解,肯定不太严谨。

服务器会生成一对钥匙,一个公钥,一个密钥。公钥是谁都可以知道的,密匙只有服务器自己知道。由公钥加密的内容,只能用密匙解密。

任何人往服务器发东西,都得先使用公钥加密。这样一来,只有拥有密钥的服务器可获取你发送的内容。现在就实现了你到服务器单向的保密。

客户端会生成一个共享密匙,用服务器公钥加密发给服务器。现在只有你和服务器知道这个共享密匙,你们就可以使用这个共享密匙给对话加密、解密了。

加密流程的对应实现

  1. 获取到服务器的加密请求后,读取服务器编号、服务器公钥、验证令牌。(服务器编号是给正版验证使用的,下面再说)
  2. 使用项目里Tool类的方法获取共享密匙。
    public static SecretKey generateSecretKey() {
        try {
            KeyGenerator keygenerator = KeyGenerator.getInstance("AES");
            keygenerator.init(128);
            return keygenerator.generateKey();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
  3. 按照协议给把共享密钥和验证令牌装到包里再用公钥加密返给服务器。
    public static PublicKey byteToPublicKey(byte[] p_13601_) {
        EncodedKeySpec encodedkeyspec = new X509EncodedKeySpec(p_13601_);
        try {
            KeyFactory keyfactory = KeyFactory.getInstance("RSA");
            return keyfactory.generatePublic(encodedkeyspec);
        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
    //看到“p_13601_”这个参数名,读过mc源码的同学应该能想到了,Tool类就来自mc的源码。虽然那里不叫这名。

剩下的加密/解密使用Cipher类就差不多了,这里就不多介绍了,项目里的代码应该能看懂。

接收/发送数据包

可以参照总览里的流程:

  • 接收服务器发送的数据:解密 > 解压 > 根据包的ID进行对应的分析 > 获取数据

  • 往服务器发送数据:设置数据 > 加上包的ID > 压缩 > 加密

剩下的东西就是Java里I/O流的事情了。

正版验证

这个我只搞了通过uuid,token进行验证。

在建立连接的第5步,向官方的验证网站发送post请求。

https://sessionserver.mojang.com/session/minecraft/join

内容是一串json:
{
“accessToken” : “<accessToken>” ,
“selectedProfile” : “<player’s uuid without dashes>” ,
“serverId” : “<serverHash>”
}
post要加上Content-Type: application/json这个参数。

serverId的获取

String serverID = (new BigInteger(Tool.digestData(serverId, publickey, secretkey))).toString(16);

public static byte[] digestData(String s, PublicKey publicKey, SecretKey secretKey) throws Exception {
    return digestData(s.getBytes("ISO_8859_1"), secretKey.getEncoded(), publicKey.getEncoded());
}

private static byte[] digestData(byte[]... bytes) throws Exception {
    MessageDigest messagedigest = MessageDigest.getInstance("SHA-1");

    for (byte[] aByte : bytes) {
        messagedigest.update(aByte);
    }
    return messagedigest.digest();
}

serverId(方法第一个参数)是来自加密请求那里的服务器ID,publickey和secretkey上面已经说过了。

结语

写到这里,东西介绍的应该差不多了,希望能帮助到你。

感谢

感谢下面这些资料对我的帮助:

  • wiki.vg
  • Another-Minecraft-Chat-Client

mCClient

发表于 2022-02-23 |

简介

支持正版服务器的命令行MC登陆器

关键词:Java、支持正版验证、命令行(无图形界面)、仅支持协议757(MC 1.18)、简单

· 使用Java编写,许多代码来自另一开源项目 Another-Minecraft-Chat-Client,mCClient算是它的简化版本吧,只是加上了对正版的支持。

· 目前仅支持使用UUID及AccessToken进行正版验证,还没搞微软那个登录,如何获取UUID和AccessToken可以看看下面的说明。

· 本人Java功力不深,这个东西更多是一个半成品或者是一个示例,只有较少的功能。

现阶段的具体功能

· 设置服务器的host、port、protocol及玩家的username、uuid、token(AccessToken) 。

· 使用设置上面好的参数登录正版/离线模式的服务器(离线模式可用不填uuid和token,username也可以任意填写)。

· 获取服务器的信息(就是MC服务器列表),这个可能信息会多一点,可以看到目前在线的玩家(应该吧)。

· 获取游戏内的消息,向游戏发送消息,没有给消息(一串Json)进行分析,看起来会有一点乱。

· 在玩家生命值、饥饿值发生变化时,返回数值。

· 当玩家死亡时,自动重生。同时返回提示。

· 保存玩家的参数到其文件夹内自动生成的json文档(mCClient.json),下次就能直接使了。

如何使用

· 下载 mCClient_1.1.zip ,并解压。

· 双击 “GoGo.bat” 即可进入软件。

· 输入”help”获取比较简明的说明,应该就能直接看懂了。

关于正版验证的UUID及AccessToken的获取

我平常是使用HMCL登陆器的(一个开源的MC登陆器,可以去Github上看看)。在用微软账号授权后,它的认证系统可以帮助我们获取我们的UUID及AccessToken。

在与启动器同一文件夹内的hmcl.json内就能看到。

具体实现

可以去参考 wiki.vg ,以及 列表 内的其他开源客户端。我也会单开一个文来说说对应的实现。

不足之处请多多包涵。 2022/2/23

ps:

因为里面搞中文注释会没法编译,搞英文又看不懂,所以就没搞注释了。
里面那些注释应该都来自上面提到的“AMCC”,那个比较高级(就是没法支持正版验证,不然我就直接使那个了,当然也就没有这个了),起码有UI,从里面copy、学到了不少。
正版验证后的加密对话,那部分是从mc源码学到的,做mod的那个项目里就能看到。
我也不太会用Git,不太敢直接在人家那里搞的,再加上他那个有点难懂,于是就单搞了这个,比较简单明了的版本,比较适合大家从里面学习mc的相关协议(学我的Java还是算了吧)。

题外话:

这个东西断断续续搞了一个寒假,主要是想在玩的生存服务器里能不用开游戏就能挂机生产东西。
我这人三分钟热度,在wiki那里卡了半天就去干别的去了。
但是这几天又想了起来,于是开始在GitHub上找到了一些代码,硬着头皮稍微有了点理解。
主要还是太浮躁,看不进去别人的代码。英语不好也是硬伤。

mc自动搭台子的mod更新到1.18了

发表于 2022-01-22 |

从之前的1.16更到了1.18,就有几个类名变了变,没太大难度。

加了个自由大小模式(虽然只能造小于128*128的,但应该够用了)

代码放到Github上了,传送门:>点我<

1.16版本之前的介绍:>点我<

<--1…8910…31-->

CC2001

93 日志
GitHub BiliBili
© 2025 CC2001
由 Hexo 强力驱动
|
主题 — NexT.Pisces