65.9K
CodeProject 正在变化。 阅读更多。
Home

游戏服务器查询库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (20投票s)

2004年11月20日

7分钟阅读

viewsIcon

240785

downloadIcon

5114

一个可以查询几乎所有游戏服务器类型的库。

Sample Image - GameServerInfo.jpg

引言

大多数游戏服务器都支持状态查询,以获取服务器配置或玩家数量等信息。有些程序会查询主服务器以接收所有游戏服务器的 IP 并列出它们。最受欢迎的程序有 Gamespy、The All Seeing Eye 和 kQuery。

我无意再开发一个功能相同的程序,而是想创建一个库,该库可以轻松处理服务器查询,并为您的特定用途提供其信息。

背景

状态查询使用特定的协议:最常见的有 Doom3、Gamespy、Gamespy v2、Half-Life、Quake3、Source (Half-Life 2) 和 The All Seeing Eye (ASE)。这些协议基本提供相同的信息,但其结构不同。通常,有三个部分

信息

基本信息,如服务器名称、地图名称、最大玩家数和密码保护。

规则

详细的服务器信息,包含游戏特定数据,如团队或响应时间。

参与者

服务器上所有玩家的列表。这些信息差异很大:不同的协议只有玩家姓名是共同的,根据协议的不同,还包括玩家的分数、ping 或团队。

协议概述

以下是不同协议及其工作原理的简要概述

Doom3

此协议目前仅用于所谓的“游戏”,但可能会用于基于相同引擎的即将发布的游戏中。请求以 0xFF 0xFF 开头,后跟一个简单的 getInfo。服务器响应以一个大块的形式出现,并由 0x00 分隔。这使得响应看起来像 Key0x00Value – 服务器信息和玩家信息由 0x00 0x00(双 NULL)分隔。

Gamespy

Gamespy 是最古老、使用最广泛的协议之一。其结构非常简单:要获取服务器信息,只需发送一个简单的 \info\;获取规则,发送 \rules\;获取玩家,发送 \players\。您也可以通过发送类似 \infos\\players\\rules\ 的内容来组合这些请求。

响应看起来像 \Key\Value\Key\Value\,玩家信息通过一个整数进行扩展以进行分组。它们看起来像 \player_0\Name\frags_0\10\ping_0\100\。这很重要,因为有些服务器会发送所有 \player_x\ 标志,然后是所有 \frags_x\ 等。

Gamespy v2

顾名思义,Gamespy v2 是 Gamespy 协议的修订版。它与第一个版本的区别在于更多地使用字节值而不是字符串来管理信息。所有请求都以 0xFE 0xFD 0x00 开头,后跟一个用作 ping 值的字符串。现在我们附加三个字节,代表我们想要了解的信息:第一个字节用于服务器信息,第二个用于规则,第三个用于玩家。您可以将它们设置为 0x00 表示否,或 0xFF 表示是。响应的结构与 Doom3 相同。

半衰期 (Half-Life)

请求以 0xFF 0xFF 0xFF 0xFF 开头,后跟 detailsrulesplayers,因此它与 Gamespy v2 非常相似,但使用的是字符串。响应的结构与 Doom3 相同。

雷神之锤 3 (Quake3)

请求类似于 Half-Life:我们以 0xFF 0xFF 0xFF 0xFF 开头,后跟字符串 getstatus。响应包含多行文本中的所有信息,并且可以通过简单的字符串解析进行处理。第一行可以忽略,第二行以与 Gamespy 相同的格式(\Key\Value\Key\Value\)提供服务器信息。以下行以 Scores Ping „Playername“ 的形式表示玩家信息,每行一个玩家。

Source (半衰期 2)

请求结构与之前的 Half-Life 协议几乎相同:只是字符串被单个字节替换。查询现在以 0xFF 0xFF 0xFF 0xFF 开头,后跟 0x54 用于服务器信息,0x55 用于规则,0x56 用于玩家信息。响应的结构与 Doom3 相同。

全视之眼 (ASE)

与同名程序相关的协议是最复杂也是最少使用的协议。请求是一个简单的 s,但与其他所有协议不同。查询必须发送到服务器端口 + 123,所以不要发送到游戏服务器端口。响应稍微复杂一些,因为它没有分隔符:结构是“字节 字符串 字节 字符串”,其中字节表示下一个字符串的长度(字节本身包含在内)。服务器和玩家信息之间的分隔符是一个空字符串,因此它是字节本身的长度 - 一个简单的 0x01。玩家信息看起来相似,但以一个额外的字节开头,使用标志来指示包含哪些信息。我不会进一步描述此协议,目前唯一使用此协议的游戏是 GTA 和 Farcry。

有关所有这些协议的优秀文档和示例可以在新闻组 dev.int64dev.kquery 中找到,我在创建此库时使用了它们。

Using the Code

使用该库非常简单。首先,将其包含在您的项目中,然后像所示那样调用它

GameServer server = new GameServer( "192.168.0.15", 27960, GameType.Quake3 );
Server.QueryServer(); // this may take a while

QueryServer() 从服务器获取所有信息。根据超时设置,这可能需要一些时间。您可以使用 Timeout 属性更改此设置。

Server.Timeout = 1500; // in milliseconds

基本信息可通过属性和集合获取,具体取决于您正在查询的协议。并非所有值都已设置。以下是所有这些的图表

使用 Players 属性,您可以获得一个包含以下数据的 PlayerCollection

Parameters 属性包含一个 StringCollection,其中包含服务器的所有信息。这些值很大程度上取决于所使用的协议!

连接到服务器

完整的连接和解析过程通过抽象协议类处理。这是主要的连接部分

protected void Connect( string host, int port )
{
    _serverConnection = new Socket( AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp );
    _serverConnection.SetSocketOption( SocketOptionLevel.Socket, 
        SocketOptionName.ReceiveTimeout, _timeout );
    IPAddress ip;
    try
    {
        ip = IPAddress.Parse( host );
    }
    catch( System.FormatException )
    {
        ip = Dns.Resolve( host ).AddressList[0];
    }
    _remoteIpEndPoint = new IPEndPoint( ip, port );
}

服务器通信

通信始终使用由 Socket.SendTo()Socket.ReceiveFrom() 方法提供的 UDP。

_readBuffer = new byte[100 * 1024]; // 100kb should be enough
EndPoint _remoteEndPoint = (EndPoint)_remoteIpEndPoint;
_packages = 0;
int read = 0, bufferOffset = 0;

// Request
_sendBuffer = System.Text.Encoding.Default.GetBytes( request );
_serverConnection.SendTo( _sendBuffer, _remoteIpEndPoint );
...
read = _serverConnection.ReceiveFrom( _readBuffer, ref _remoteEndPoint );

大多数服务器发送最大长度为 1400 到 1500 字节的包。对于长状态信息,这不足以在一个包中发送。通常,它们适合两个包。UDP 是一种无状态连接,因此我们必须实现如何处理这些多包以及如何合并它们。

半衰期 (Half-Life)

在 Half-Life 中,这些 UDP 包以一个 9 字节长的头部开头,与单包响应不同。第九个位置告诉我们响应包含多少个包,以及我们当前正在处理的是哪个包。

byte[] _tempBuffer = new byte[100 * 1024];
read = _serverConnection.ReceiveFrom( 
  _tempBuffer, ref _remoteEndPoint );

int packets = ( _tempBuffer[8] & 15 );
int packetNr = ( _tempBuffer[8] >> 4 ) + 1;

if ( packetNr < packets )
{
    Array.Copy( _readBuffer, 9, _tempBuffer, read, bufferOffset );
    _readBuffer = _tempBuffer;
}
else
{
    Array.Copy( _tempBuffer, 9, _readBuffer, bufferOffset, read );
}

Gamespy v1 和 v2

所有 Gamespy 响应都采用键/值方案构建,因此我们是否以正确的顺序合并它们并不重要。协议本身足够智能,总是会在分隔符处而不是在值内部进行拆分,因此我们可以简单地将它们附加到 readBuffer 的末尾。

read = _serverConnection.ReceiveFrom( _readBuffer, bufferOffset, 
    ( _readBuffer.Length - bufferOffset ), 
      SocketFlags.None, ref _remoteEndPoint );

解析响应

解析由 NULL 字节 (0x00) 分隔的字符串的主要部分

protected string ReadNextParam()
{
    string temp = "";
    for ( ; _offset < _readBuffer.Length; _offset++ )
    {
        if ( _readBuffer[_offset] == 0 )
        {
            _offset++;
            break;
        }
        temp += (char)_readBuffer[_offset];
    }
    return temp;
}

字符串分隔的响应甚至更简单

AddParams( ResponseString.Split( '\\' ) );
//...
protected void AddParams( string[] parts )
{
    if ( !IsOnline )
    {
        return;
    }
    string key, val;
    for ( int i = 0; i < parts.Length; i++ )
    {
        if ( parts[i] == "" )
        {
            continue;
        }
        key = parts[i++];
        val = parts[i];

        // Gamespy uses this
        if ( key == "final" )
        {
            break;
        }
        if ( key == "querid" )
        {
            continue;
        }

        _params[key] = val;
    }
}

我避免解释如何解析 ASE 协议,因为它无法 100% 工作。我缺少一些好的文档,而且似乎除了我之外,很多人也认为它不值得花精力。

链接

我对这些人感激不尽。欲了解更多信息,请查看以下页面

摘要

我学到了一些关于 UDP 协议的知识,并使用了以前从未使用过的类和方法,例如 BitConverter(),所以我认为这项工作是值得的。我希望这个库对您未来的项目有所帮助。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.