Skip to content

Commit

Permalink
- re-added support for UDP based master server queries
Browse files Browse the repository at this point in the history
  • Loading branch information
PredatH0r committed Jun 14, 2017
1 parent 66a1b42 commit 4e34900
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 204 deletions.
6 changes: 3 additions & 3 deletions QueryMaster/QueryMaster/MasterQuery.cs
Expand Up @@ -26,11 +26,11 @@ public class MasterQuery
/// <returns>Master server instance</returns>
public static MasterServer GetMasterServerInstance(EngineType type)
{
MasterServer server = null;
MasterServerUdp server = null;
switch (type)
{
case EngineType.GoldSource: server = new MasterServer(GoldSrcServer); break;
case EngineType.Source: server = new MasterServer(SourceServer); break;
case EngineType.GoldSource: server = new MasterServerUdp(GoldSrcServer); break;
case EngineType.Source: server = new MasterServerUdp(SourceServer); break;
default: throw new FormatException("An invalid EngineType was specified.");
}
return server;
Expand Down
181 changes: 3 additions & 178 deletions QueryMaster/QueryMaster/MasterServer.cs
@@ -1,11 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Xml.Serialization;

namespace QueryMaster
{
Expand All @@ -16,69 +11,14 @@ namespace QueryMaster
/// <param name="endPoints">Server Sockets</param>
public delegate void MasterIpCallback(ReadOnlyCollection<Tuple<IPEndPoint,ServerInfo>> endPoints, Exception error);

#region WebAPI XML response mappings: class Response, Message
public class Message
{
/// <summary>
/// IP:port
/// </summary>
public string addr { get; set; }
public ushort gameport { get; set; }
public long steamid { get; set; }
public string name { get; set; }
public int appid { get; set; }
public string gamedir { get; set; }
public string version { get; set; }
public string product { get; set; }
public string region { get; set; }
public int players { get; set; }
public int max_players { get; set; }
public int bots { get; set; }
public string map { get; set; }
public bool secure { get; set; }
public bool dedicated { get; set; }
public string os { get; set; }
public string gametype { get; set; }
}

[XmlRoot("response")]
public class Response
{
[XmlArray("servers")]
[XmlArrayItem("message")]
public Message[] Servers;
}

#endregion

class XWebClient : WebClient
{
public XWebClient()
{
this.Proxy = null;
}

protected override WebRequest GetWebRequest(Uri address)
{
var req = base.GetWebRequest(address);
req.Timeout = 5000;
return req;
}
}

/// <summary>
/// Provides methods to query master server.
/// An instance can only be used for a single request and is automatically disposed when the request completes or times out
/// </summary>
public class MasterServer
public abstract class MasterServer
{
const string SteamWebApiKey = "B7D245299F6F990504A86FF91EC9D6BD"; // create an account and get a steam web api key at http://steamcommunity.com/dev/apikey

private readonly IPEndPoint endPoint;

public MasterServer(IPEndPoint endPoint)
public MasterServer()
{
this.endPoint = endPoint;
this.Retries = 5;
this.GetAddressesLimit = 1000;
}
Expand All @@ -101,122 +41,7 @@ public MasterServer(IPEndPoint endPoint)
/// </summary>
public int Retries { get; set; }

/// <summary>
/// Gets a server list from the Steam master server.
/// The callback is invoked from a background thread every time a batch of servers is received.
/// The end of the list is marked with an IPEndPoint of 0.0.0.0:0
/// In case of a timeout or an exception, the callback is invoked with a NULL parameter value.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the object is still busy handling a previous call to GetAddresses</exception>
public void GetAddresses(Region region, MasterIpCallback callback, IpFilter filter)
{
ThreadPool.QueueUserWorkItem(x =>
{
#if true
try
{
using (var cli = new XWebClient())
{
var filters = MasterUtil.ProcessFilter(filter);
var url = $"https://api.steampowered.com/IGameServersService/GetServerList/v1/?key={SteamWebApiKey}&format=xml&filter={filters}&limit={GetAddressesLimit}";
var xml = cli.DownloadString(url);
var ser = new XmlSerializer(typeof (Response));
var resp = (Response) ser.Deserialize(new StringReader(xml));
var endpoints = new List<Tuple<IPEndPoint,ServerInfo>>();
foreach (var msg in resp.Servers)
{
try
{
int i = msg.addr.IndexOf(':');
if (i > 0)
{
var info = ConvertToServerInfo(msg);
endpoints.Add(new Tuple<IPEndPoint, ServerInfo>(info.EndPoint, info));
}
}
catch
{
}
}
callback(new ReadOnlyCollection<Tuple<IPEndPoint,ServerInfo>>(endpoints), null);
}
}
catch(Exception ex)
{
callback(null, ex);
}
#else
var udpSocket = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, ProtocolType.Udp);
udpSocket.SendTimeout = 500;
udpSocket.ReceiveTimeout = 500;
udpSocket.Connect(endPoint);
byte[] recvData = new byte[1400];
try
{
var nextSeed = SeedEndpoint;
int totalCount = 0;
do
{
var curSeed = nextSeed;
var endpoints = Util.RunWithRetries(() => SendAndReceive(udpSocket, recvData, region, filter, curSeed), this.Retries);
if (endpoints == null)
{
callback(null);
break;
}
ThreadPool.QueueUserWorkItem(y => callback(endpoints, null));
totalCount += endpoints.Count;
nextSeed = endpoints.Last();
} while (!nextSeed.Equals(SeedEndpoint) && totalCount < GetAddressesLimit);
}
catch (Exception ex)
{
callback(null, ex);
}
finally
{
try { udpSocket.Close(); }
catch { }
}
#endif
});
}

private ServerInfo ConvertToServerInfo(Message msg)
{
var si = new ServerInfo();
si.Address = msg.addr;
si.Bots = (byte)msg.bots;
si.Description = msg.gametype;
si.Directory = msg.gamedir;
si.Environment = msg.os == "w" ? "Windows" : msg.os == "l" ? "Linux" : msg.os;
si.Extra.GameId = msg.appid;
si.Extra.Port = msg.gameport;
si.Extra.SteamID = msg.steamid;
si.GameVersion = msg.version;
si.Id = msg.appid < 0x10000 ? (ushort) msg.appid : (ushort)0;
si.IsSecure = msg.secure;
si.Map = msg.map;
si.MaxPlayers = (byte)msg.max_players;
si.Name = msg.name;
si.Ping = 0;
si.Players = msg.players;
si.ServerType = msg.dedicated ? "Dedicated" : "Listen";
return si;
}

private ReadOnlyCollection<IPEndPoint> SendAndReceive(Socket udpSocket, byte[] recvData, Region region, IpFilter filter, IPEndPoint seed)
{
var msg = MasterUtil.BuildPacket(seed.ToString(), region, filter);
udpSocket.Send(msg);

int len = udpSocket.Receive(recvData, 0, recvData.Length, SocketFlags.None);
byte[] data = new byte[len];
Array.Copy(recvData, data, data.Length);
return MasterUtil.ProcessPacket(data);
}
public abstract void GetAddresses(Region region, MasterIpCallback callback, IpFilter filter);

}
}
83 changes: 83 additions & 0 deletions QueryMaster/QueryMaster/MasterServerUdp.cs
@@ -0,0 +1,83 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace QueryMaster
{
/// <summary>
/// Provides methods to query master server.
/// An instance can only be used for a single request and is automatically disposed when the request completes or times out
/// </summary>
public class MasterServerUdp : MasterServer
{
private readonly IPEndPoint endPoint;
private readonly IPEndPoint SeedEndpoint = new IPEndPoint(0, 0);

public MasterServerUdp(IPEndPoint endPoint)
{
this.endPoint = endPoint;
}

/// <summary>
/// Gets a server list from the Steam master server.
/// The callback is invoked from a background thread every time a batch of servers is received.
/// The end of the list is marked with an IPEndPoint of 0.0.0.0:0
/// In case of a timeout or an exception, the callback is invoked with a NULL parameter value.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the object is still busy handling a previous call to GetAddresses</exception>
public override void GetAddresses(Region region, MasterIpCallback callback, IpFilter filter)
{
ThreadPool.QueueUserWorkItem(x =>
{
var udpSocket = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, ProtocolType.Udp);
udpSocket.SendTimeout = 500;
udpSocket.ReceiveTimeout = 1000;
udpSocket.Connect(endPoint);
byte[] recvData = new byte[1400];
try
{
var nextSeed = SeedEndpoint;
int totalCount = 0;
do
{
var curSeed = nextSeed;
var endpoints = Util.RunWithRetries(() => SendAndReceive(udpSocket, recvData, region, filter, curSeed), this.Retries);
if (endpoints == null)
{
callback(null, null);
break;
}
ThreadPool.QueueUserWorkItem(y => callback(endpoints, null));
totalCount += endpoints.Count;
nextSeed = endpoints.Last().Item1;
} while (!nextSeed.Equals(SeedEndpoint) && totalCount < GetAddressesLimit);
}
catch (Exception ex)
{
callback(null, ex);
}
finally
{
try { udpSocket.Close(); }
catch { }
}
});
}

private ReadOnlyCollection<Tuple<IPEndPoint, ServerInfo>> SendAndReceive(Socket udpSocket, byte[] recvData, Region region, IpFilter filter, IPEndPoint seed)
{
var msg = MasterUtil.BuildPacket(seed.ToString(), region, filter);
udpSocket.Send(msg);

int len = udpSocket.Receive(recvData, 0, recvData.Length, SocketFlags.None);
byte[] data = new byte[len];
Array.Copy(recvData, data, data.Length);
var endpoints = MasterUtil.ProcessPacket(data);
return new ReadOnlyCollection<Tuple<IPEndPoint, ServerInfo>>(endpoints.Select(ep => new Tuple<IPEndPoint, ServerInfo>(ep, null)).ToList());
}
}
}

0 comments on commit 4e34900

Please sign in to comment.