VB.NET: rilevare un server di Quake3 su una rete locale
Classica applicazione inutile ma divertente da sviluppare :-)
Qualche giorno fa ho iniziato a chiedermi come il mio amato Quake3 Arena lavorasse per ricercare i server per il gioco in multiplayer sulla rete locale.
Dopo una breve ricerca su Google mi sono imbattuto in questo articolo che spiega a grandi linee come funziona il protocollo utilizzato dal motore di Q3A: tutto basato su UDP (scelta obbligata, per la maggiore velocità) e a dire il vero abbastanza semplice e lineare.
Quindi, per rilevare se su una macchina remota sia attivo un server è sufficiente forgiare un pacchetto UDP come descritto sopra, inviarlo sulla porta 27960 e attendere una eventuale risposta (non all'infinito, essendo UDP un protocollo 'connection-less').
Con queste informazioni la realizzazione di una semplice funzione in VB.NET è cosa da poco:
[sourcecode language='vb']
Imports System.Net
Public Function CheckServer(ByVal hostaddress As String)
Dim _UdpClient As New System.Net.Sockets.UdpClient
Dim client As New Sockets.Socket(Sockets.AddressFamily.InterNetwork, Sockets.SocketType.Dgram, Sockets.ProtocolType.Udp)
client.ReceiveTimeout = 5
client.Connect(IPAddress.Parse(hostaddress), 27960)
Dim bytCommand As Byte() = System.Text.Encoding.ASCII.GetBytes("xxxxxgetstatus")
bytCommand(0) = Byte.Parse("255")
bytCommand(1) = Byte.Parse("255")
bytCommand(2) = Byte.Parse("255")
bytCommand(3) = Byte.Parse("255")
bytCommand(4) = Byte.Parse("02")
Dim remoteEndPoint As New IPEndPoint(IPAddress.Any, 0)
Dim pret As String = client.Send(bytCommand, socketFlags:=Sockets.SocketFlags.None)
Dim bufferRec(65000) As Byte
Try
client.Receive(bufferRec)
Return System.Text.Encoding.ASCII.GetString(bufferRec)
Catch ex As Exception
Return ""
End Try
End Function
[/sourcecode]
Da notare che ho settato manualmente il timeout della connessione e 'trappato' l'errore di connessione, verificando in questo modo se il server sia in funzione o meno.
La funzione mi restituisce una stringa contenente (qualora sia attivo un server sulla macchina esaminata) una serie di informazioni sulla partita in corso; nel caso la connessione vada in timeout, restituisce una stringa vuota.
Ora so verificare se su un determinato sistema stia girando Q3A in modalità multiplayer, il passo successivo è ripetere questa procedura per tutti quelli presenti sulla mia rete locale.
Per farlo questo mi sono affidato a una soluzione 'sporca' ma funzionale: utilizzo il comando 'net view' di windows, ne analizzo il risultato ottenendo un elenco di indirizzi:
[sourcecode language='vb']
Public Function GetIpAddresses()
Dim addresses As New ArrayList
Dim MyAdd As String = Dns.GetHostByName(Dns.GetHostName()).AddressList(0).ToString
Dim psi As System.Diagnostics.ProcessStartInfo = New System.Diagnostics.ProcessStartInfo()
psi.FileName = ("C:\WINDOWS\System32\cmd.exe")
psi.Arguments = "/c net view > lista.txt"
psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
Application.DoEvents()
System.Diagnostics.Process.Start(psi)
Dim sr As System.IO.StreamReader = Nothing
Dim run As Boolean = False
While run = False
Application.DoEvents()
Try
System.Threading.Thread.Sleep(1000)
sr = New System.IO.StreamReader(Application.StartupPath & "\lista.txt")
run = True
Catch ex As Exception
run = False
End Try
End While
While sr.ReadLine().StartsWith("--") True
Application.DoEvents()
End While
Dim str As String = ""
Dim comp(64) As String
Dim i As Integer = 0
While str.StartsWith("The") True
Application.DoEvents()
str = sr.ReadLine
comp(i) = str.Split(Char.Parse(" "))(0)
comp(i) = comp(i).Substring(2, comp(i).Length - 2)
If comp(i) = "e" Then
Application.DoEvents()
comp(i) = Nothing
End If
i = i + 1
End While
sr.Close()
sr = Nothing
For Each s As String In comp
If s Nothing Then
If s.ToUpper Dns.GetHostName.ToUpper Then
addresses.Add(Dns.GetHostByName(s).AddressList(0).ToString)
End If
End If
Next
Return addresses
End Function
[/sourcecode]
a questo punto non mi resta che sottoporre l'array di indirizzi restituito dalla funzione GetIpAddresses a CheckServer e, se viene rilevato un server, leggere i dati restituiti e formattarli adeguatamente:
[sourcecode language='vb']
Dim lista As ArrayList = GetIpAddresses()
For Each riga As String In lista.ToArray
Dim server As String = CheckServer(riga)
If server "" Then
Dim ServerName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "sv_hostname") + 1)
Dim MapName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "mapname") + 1)
Dim FragLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "fraglimit") + 1)
Dim TimeLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "timelimit") + 1)
Dim Version As String = server.Split("\")(Array.IndexOf(server.Split("\"), "version") + 1)
risultato = risultato & "--- Trovato Server ---" & vbCrLf
risultato = risultato & "Ip Address:" & riga & vbCrLf
risultato = risultato & "Server Name: " & ServerName & vbCrLf
risultato = risultato & "Mappa: " & MapName & vbCrLf
risultato = risultato & "FragLimit: " & FragLimit & vbCrLf
risultato = risultato & "TimeLimit: " & TimeLimit & FragLimit & vbCrLf
risultato = risultato & "Versione: " & Version & FragLimit & vbCrLf
TextBox1.Text = TextBox1.Text & risultato
End If
Application.DoEvents()
Next
End Sub
[/sourcecode]
Ho completato il tutto con il codice necessario a ridurre l'applicazione nella system tray (facendo ripetere la procedura di scan ogni 5 secondi) e a far riapparire un form qualora la ricerca fornisca esito positivo.
Il sorgente completo è scaricabile da QUI.
(e sperate che non finisca mai tra le mani del vostro capoufficio!) :-D

Dopo una breve ricerca su Google mi sono imbattuto in questo articolo che spiega a grandi linee come funziona il protocollo utilizzato dal motore di Q3A: tutto basato su UDP (scelta obbligata, per la maggiore velocità) e a dire il vero abbastanza semplice e lineare.
To query a server is very simple. Send a connectionless (UDP) packet with 4 OOB header bytes (0xff) and the text string getstatus. There are many sites which contain a thorough description of this so I won't go into details.
Quindi, per rilevare se su una macchina remota sia attivo un server è sufficiente forgiare un pacchetto UDP come descritto sopra, inviarlo sulla porta 27960 e attendere una eventuale risposta (non all'infinito, essendo UDP un protocollo 'connection-less').
Con queste informazioni la realizzazione di una semplice funzione in VB.NET è cosa da poco:
[sourcecode language='vb']
Imports System.Net
Public Function CheckServer(ByVal hostaddress As String)
Dim _UdpClient As New System.Net.Sockets.UdpClient
Dim client As New Sockets.Socket(Sockets.AddressFamily.InterNetwork, Sockets.SocketType.Dgram, Sockets.ProtocolType.Udp)
client.ReceiveTimeout = 5
client.Connect(IPAddress.Parse(hostaddress), 27960)
Dim bytCommand As Byte() = System.Text.Encoding.ASCII.GetBytes("xxxxxgetstatus")
bytCommand(0) = Byte.Parse("255")
bytCommand(1) = Byte.Parse("255")
bytCommand(2) = Byte.Parse("255")
bytCommand(3) = Byte.Parse("255")
bytCommand(4) = Byte.Parse("02")
Dim remoteEndPoint As New IPEndPoint(IPAddress.Any, 0)
Dim pret As String = client.Send(bytCommand, socketFlags:=Sockets.SocketFlags.None)
Dim bufferRec(65000) As Byte
Try
client.Receive(bufferRec)
Return System.Text.Encoding.ASCII.GetString(bufferRec)
Catch ex As Exception
Return ""
End Try
End Function
[/sourcecode]
Da notare che ho settato manualmente il timeout della connessione e 'trappato' l'errore di connessione, verificando in questo modo se il server sia in funzione o meno.
La funzione mi restituisce una stringa contenente (qualora sia attivo un server sulla macchina esaminata) una serie di informazioni sulla partita in corso; nel caso la connessione vada in timeout, restituisce una stringa vuota.
Ora so verificare se su un determinato sistema stia girando Q3A in modalità multiplayer, il passo successivo è ripetere questa procedura per tutti quelli presenti sulla mia rete locale.
Per farlo questo mi sono affidato a una soluzione 'sporca' ma funzionale: utilizzo il comando 'net view' di windows, ne analizzo il risultato ottenendo un elenco di indirizzi:
[sourcecode language='vb']
Public Function GetIpAddresses()
Dim addresses As New ArrayList
Dim MyAdd As String = Dns.GetHostByName(Dns.GetHostName()).AddressList(0).ToString
Dim psi As System.Diagnostics.ProcessStartInfo = New System.Diagnostics.ProcessStartInfo()
psi.FileName = ("C:\WINDOWS\System32\cmd.exe")
psi.Arguments = "/c net view > lista.txt"
psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
Application.DoEvents()
System.Diagnostics.Process.Start(psi)
Dim sr As System.IO.StreamReader = Nothing
Dim run As Boolean = False
While run = False
Application.DoEvents()
Try
System.Threading.Thread.Sleep(1000)
sr = New System.IO.StreamReader(Application.StartupPath & "\lista.txt")
run = True
Catch ex As Exception
run = False
End Try
End While
While sr.ReadLine().StartsWith("--") True
Application.DoEvents()
End While
Dim str As String = ""
Dim comp(64) As String
Dim i As Integer = 0
While str.StartsWith("The") True
Application.DoEvents()
str = sr.ReadLine
comp(i) = str.Split(Char.Parse(" "))(0)
comp(i) = comp(i).Substring(2, comp(i).Length - 2)
If comp(i) = "e" Then
Application.DoEvents()
comp(i) = Nothing
End If
i = i + 1
End While
sr.Close()
sr = Nothing
For Each s As String In comp
If s Nothing Then
If s.ToUpper Dns.GetHostName.ToUpper Then
addresses.Add(Dns.GetHostByName(s).AddressList(0).ToString)
End If
End If
Next
Return addresses
End Function
[/sourcecode]
a questo punto non mi resta che sottoporre l'array di indirizzi restituito dalla funzione GetIpAddresses a CheckServer e, se viene rilevato un server, leggere i dati restituiti e formattarli adeguatamente:
[sourcecode language='vb']
Dim lista As ArrayList = GetIpAddresses()
For Each riga As String In lista.ToArray
Dim server As String = CheckServer(riga)
If server "" Then
Dim ServerName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "sv_hostname") + 1)
Dim MapName As String = server.Split("\")(Array.IndexOf(server.Split("\"), "mapname") + 1)
Dim FragLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "fraglimit") + 1)
Dim TimeLimit As String = server.Split("\")(Array.IndexOf(server.Split("\"), "timelimit") + 1)
Dim Version As String = server.Split("\")(Array.IndexOf(server.Split("\"), "version") + 1)
risultato = risultato & "--- Trovato Server ---" & vbCrLf
risultato = risultato & "Ip Address:" & riga & vbCrLf
risultato = risultato & "Server Name: " & ServerName & vbCrLf
risultato = risultato & "Mappa: " & MapName & vbCrLf
risultato = risultato & "FragLimit: " & FragLimit & vbCrLf
risultato = risultato & "TimeLimit: " & TimeLimit & FragLimit & vbCrLf
risultato = risultato & "Versione: " & Version & FragLimit & vbCrLf
TextBox1.Text = TextBox1.Text & risultato
End If
Application.DoEvents()
Next
End Sub
[/sourcecode]
Ho completato il tutto con il codice necessario a ridurre l'applicazione nella system tray (facendo ripetere la procedura di scan ogni 5 secondi) e a far riapparire un form qualora la ricerca fornisca esito positivo.
Il sorgente completo è scaricabile da QUI.
(e sperate che non finisca mai tra le mani del vostro capoufficio!) :-D