暑假要重新开服了,又刚好看到个 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 (字典格式)有两个键,分别是 modId
和 version
status2: 与 status1 完全相同的 forge 信息,对应的键却叫做 “modinfo
”;mod 信息对应的键却又叫变成了 “modList
”,里面每一项 mod 的两个键又变成了 modId
和 modmarker
:
status1:
{
"forgeData": {
"mods": [
{"modId": "xxx", "modmarker": "xxx"},
...
],
...
}
...
}
status2:
{
"modinfo": {
"modList": [
{"modId": "xxx", "version": "xxx"},
...
],
...
}
...
}
虽然键名不同,但里面的内容确是相同的。