MCStatus库写查询MC服务器信息插件的一些随记

⚠️ 本文最后更新于2024年04月18日,已经过了376天没有更新,若内容或图片失效,请留言反馈

暑假要重新开服了,又刚好看到个 Dinnerbone 写的 Python 库,用来请求 MC 服务器信息的 -> (在这里) <-,于是就顺手写个查服务器的插件。没想到写这玩意比想象中要复杂多了。

玩家列表获取方式的替代

MCStatus 库里服务器用 JavaServer 类和 BedrockServer 类表示,下以 JavaServer 类为例。一般服务器会以 host:port 这样基本的 IP 当访问链接,如“114.514.19.19:810”,直接用官方项目页面给的例子就可以顺利访问服务器信息,如:

from mcstatus import JavaServer

address = f"114.514.19.19:810"
server = JavaServer.lookup(address)

"""ping"""
ping = server.ping()

"""status"""
status = server.status()

s_players = status.players
online_players_count = s_players.online  # 在线玩家数
max_players_count = s_players.max  # 最大玩家数

version = status.version
server_version = version.name  # 服务端版本
server_protocol = version.protocol  # 服务端协议

description = status.description  # 服务器介绍
favicon = status.favicon  # 服务器图标 (base64)
latency = status.latency  # 延迟

"""query"""
query = server.query()

q_players = query.players
online_players_count = q_players.online  # 在线玩家数
max_players_count = q_players.max  # 最大玩家数
names = q_players.names  # 名称列表

software = query.software
q_version = software.version  # 版本
brand = software.brand
plugins = software.plugins  # 插件列表

motd = query.motd  # 标语
map = query.map  # 地图?

raw = status.raw  # 服务器响应全数据,也包括mod、玩家列表等,字典类型
...

差不多能用得上的信息就是这些了,但是实际操作起来就开始出问题了:

首先是 server.query() 这个函数总是报超时错,导致query那一串属性全部没法获得,插件列表、玩家列表之类 -> (看这里) <-,所以我们可以用以下方法作为替代来获取玩家列表:

...
raw = status.raw
player_list = [_['name'] for _ in raw['players']['sample']]
uuid_list = [_['id'] for _ in raw['players']['sample']]

服务器链接的问题

解决了玩家列表的问题,又出现新问题了。除了 host:port 这样的 IP 地址,有些服务器使用的是 SRV 链接 -> (看这里) <-,用上面的 lookup(address) 方法有时会出现 OSError 或是 IOError -> (看这里) <-。对这种链接我方法比较简单粗暴,就是将其原先的 host:port 形式反解析出来,然后按原方法继续操作:

...
import dns.resolver
from loguru import logger

host = f"some.srv.address"
srv_request = "_minecraft._tcp"
request = f"{srv_request}.{host}"

try:
    answers = dns.resolver.resolve(request, 'SRV')
except dns.resolver.NXDOMAIN as e:
    logger.error(f"DNS 解析出错:query 名不存在 {e}")
    raise
except dns.resolver.YXDOMAIN as e:
    logger.error(f"DNS 解析出错:query 名过长 {e}")
    raise
except dns.resolver.NoAnswer as e:
    logger.error(f"DNS 解析出错:请求不含所需结果 {e}")
    raise
except dns.resolver.NoNameservers as e:
    logger.error(f"DNS 解析出错:解析服务器无空闲 {e}")
    raise
except dns.resolver.Timeout as e:
    logger.error(f"DNS 解析出错:链接超时 {e}")
    raise

try:
    answers
except NameError:
    print(f"{request} 中无 SRV 记录")
    raise
else:
    answer = answers[0]
    port = answer.port
    host = str(answer.target).rstrip(".")
    return host, int(port)
...

如此一来将 SRV 链接原先的 host:port 形式解析出来了,但如果直接代入上面的 server.status() 方法,有时仍然会报 OSError 或是 IOError,有时也可能出现 ConnectionAbortedError -> (看这里) <- 错误。这里我们拆开 status() 方法,调用该方法时会默认进行一次 test_ping,所以我们只需要手动跳过这次 test_ping 即可:

...
from mcstatus.address import Address
from mcstatus.pinger import ServerPinger
from mcstatus.protocol.connection import TCPSocketConnection

address = Address(host, port)
pinger = ServerPinger(TCPSocketConnection(address), address=address)
pinger.handshake()
status = pinger.read_status()
...

这里我们得到的 status 即之后的用法基本和上面相同。

返回值的微妙差异

然而,解决了链接问题,我在处理返回值时又有了惊人发现:

原先的 lookup() 处理常规 IP 得到的 status - (1)

和下面的跳过 test_ping 处理 SRV 链接得到的 status - (2)

它们返回的 raw 字典,对同一内容值的键命名却不同

status1: 对于 forge 服,对应的键叫做 “forgeData”,里面记录着 forge 信息和 mod 信息;mod 信息对应的键叫做 “mods”,其中每一项 mod (字典格式)有两个键,分别是 modIdversion

status2: 与 status1 完全相同的 forge 信息,对应的键却叫做 “modinfo”;mod 信息对应的键却又叫变成了 “modList”,里面每一项 mod 的两个键又变成了 modIdmodmarker

status1:
{
  "forgeData": {
    "mods": [
      {"modId": "xxx", "modmarker": "xxx"},
      ...
    ],
    ...
  }
  ...
}

status2:
{
  "modinfo": {
    "modList": [
      {"modId": "xxx", "version": "xxx"},
      ...
    ],
    ...
  }
  ...
}

虽然键名不同,但里面的内容确是相同的。

By Number_Sir On