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.
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