Creare Videogiochi - Game Developer

Versione completa: [C#-SOURCE] TCP Socket MultiClient/Server
Al momento stai visualizzando i contenuti in una versione ridotta. Visualizza la versione completa e formattata.
Salve,
nello sviluppo di un mio videogioco è sorta la necessità di avvalersi di un sistema dove un numero non definito di client potessero interfacciarsi con un server per la comunicazione e la trasmissione di informazioni utili.
Tale sistema è utile per la creazione di tutti quei servizi che prevedono una comunicazione di più client verso un server (un esempio abbastanza ovvio è una chat-room).
Mi sembrava, quindi, un interessante argomento da trattare con voi e, perché no, aprire una discussione costruttiva al riguardo.

Prima di ogni cosa, vi riporto un po' di documentazione utile alla causa. Verrà fatto utilizzo dei namespace System.Net.Sockets e System.Threading, vi invito a dargli un occhio nella documentazione di Microsoft.
Mi sento, inoltre, di consigliarvi SharpDevelop, che ritengo essere un buon IDE oltre al classico Visual Studio di Microsoft.

Piccolo appunto 1: questo rappresenta la base di un sistema di questo tipo. Va assolutamente affinato e vanno eliminate possibili falle nella sicurezza (che vi invito a segnalare, in modo da discuterne e in modo da risolverle subito). Ripeto: è una BASE di partenza.

Piccolo appunto 2: è volutamente scritto in inglese (salvo errori, in tal caso: pardon!).

Iniziamo, allora, con la stesura di questo progetto.

- Create una nuova soluzione nel vostro IDE ed usate questi namespace:
Codice:
using System;
using System.Net.Sockets;
using System.Threading; using System.IO;

- Creiamo una classe ausiliare, che ci servirà per identificare coloro che si connettono (per il momento sarà una classe banale, composta da IP e dal nome di chi si collega; quest'ultimo, però, non sarà utilizzato ancora):
Codice:
classConnectedClient
{
        public System.Net.EndPoint IP;
         //public string name;

        public ConnectedClient()
        {
            this.IP = null;
             //this.name = string.Empty;
        }

        public ConnectedClient(System.Net.EndPoint ip, string name)
        {
            this.IP = ip;
             //this.name = name;
        }    
}

- Creiamo una seconda classe ausiliaria, che ci verrà in aiuto successivamente:
Codice:
classSockAndThread
{
        public Socket socket;
        public int threadHash; }

- Iniziamo con la stesura della nostra classe principale:
Codice:
class MyServer //La nostra classe, che rappresenta il nostro server
{
        static TcpListener tcpListener = new TcpListener(28960); /*Dichiariamo un listener TCP sulla porta 28960 (vi ricordo che le prime 1024 sono riservate)*/
        private const int maxNumberOfClients = 100; /*Rappresenta il massimo numero di thread avviati. E' approssimabile al massimo numero di utenti potenzialmente connessi.*/
        static int currentNumberOfClients = 0; /*Intero che rappresenta il numero corrente di utenti connessi.*/

        private static List<ConnectedClient> playerList = new List<ConnectedClient>(); /*Lista degli utenti connessi (ConnectedClient)*/

        static void Listeners(object sockThread){ //Ecco il metodo vero e proprio, che si occuperà di ricevere i singoli pacchetti di un utente e di gestirli adeguatamente.
            SockAndThread sockAndThread = (SockAndThread)sockThread;
            Socket socketForClient = sockAndThread.socket;

            if(socketForClient.Connected){
                currentNumberOfClients++;
                Console.WriteLine("*** Client: " + socketForClient.RemoteEndPoint + " now connected on the server!");
                
                Console.WriteLine("*** Connected clients: " + currentNumberOfClients);

                NetworkStream networkStream = new NetworkStream(socketForClient);
                StreamWriter streamWriter = new StreamWriter(networkStream);
                StreamReader streamReader = new StreamReader(networkStream);

                try
                {
                    while (true) //Tale ciclo rimarrà verificato finché il client che ha generato il socket sarà connesso. Verrà interrotto quando tale client si disconnetterà.
                    {
                        if (!socketForClient.Connected) //Verrà interrotto quando tale client si disconnetterà.
                        {
                            break;
                        }
                        string theString = streamReader.ReadLine();
                        Console.WriteLine(socketForClient.RemoteEndPoint + " : " + theString);
                        if (theString == "exit") //Terminerà anche se il client manderà la stringa "exit".
                        {
                            break;
                        }
                    }
                }
                catch //Gestiamo la terminazione forzata del client, senza messaggi di disconnessione indirizzati al server. Infatti, se provate a chiudere forzatamente il client senza avvisare il server, esso non riuscirà più a leggere lo stream e solleverà un'eccezione.
                {
                    currentNumberOfClients--;
                    Console.WriteLine(socketForClient.RemoteEndPoint + " disconnected");
                }
                streamReader.Close();
                streamWriter.Close();
                networkStream.Close();
            }
            socketForClient.Close();
            numberOfStartedThread--;
            playerList.Remove(cnctdClient);
        }

        static int numberOfStartedThread = 0; //Rappresenta il numero di thread avviati.

        static void startGameServer()
        {
            while (true)
            {
                Socket socketForClient = tcpListener.AcceptSocket(); //Accettiamo il socket in attesa.
                if (socketForClient.Connected)
                {
                    SockAndThread sockAndThread = new SockAndThread();
                    sockAndThread.socket = socketForClient;
                    
                    ConnectedClient cnctdClient = new ConnectedClient();
                    cnctdClient.IP = socketForClient.RemoteEndPoint;
                    playerList.Add(cnctdClient);
                    
                    Thread newThread = new Thread(new ParameterizedThreadStart(Listeners));
                    sockAndThread.threadHash = newThread.GetHashCode();
                    newThread.Start(sockAndThread); //Avviamo il nostro gestore di client, passandogli l'informazione sul socket da gestire e sul thread (quest'ultimo non è utilizzato in questa versione).
  
                    numberOfStartedThread++;
                    Console.WriteLine("*** Number of Started Thread: " + numberOfStartedThread);
                }
            }
        }

        static void GameShell() //Ecco la shell del nostro server. Un volta avviato questo metodo, potremo inviare comandi all'applicazione.
        {
            Console.WriteLine("*** Game Shell runned!");
            while (true)
            {
                Console.Write("> ");
                string cmd = Console.ReadLine(); //Leggiamo ciò che viene scritto in console.

                switch (cmd) //In questo punto verranno implementati tutti i comandi che l'applicativo prevede. Ne ho implementati già tre, per mostrarvene la strutturazione.
                {
                    case "stop":
                        Console.WriteLine("*** Game Server is being stopped!");
                        
                        //Spazio riservato ad operazioni supplementari da implementare da voi. Non ho incluso in questa versione pubblica la terminazione dei thread e la loro gestione.
                        
                        Console.WriteLine("*** Game Server stopped!");
                        Console.WriteLine();
                        break;
                    case "start":
                        Console.WriteLine("*** Game Server is being started!");
                        Thread newThread = new Thread(startGameServer);
                        newThread.Start(); //Avviamo il game server.
                        Console.WriteLine("*** Game Server is started!");
                        break;
                    case "exit":
                        if (numberOfStartedThread > 0)
                        {
                            Console.WriteLine("*** Game Server cannot be closed!");
                            Console.WriteLine("*** Make sure to write \"stop\" command before!");
                            break;
                        }
                        else
                        {
                            Console.WriteLine("*** Game Server is being closed!");
                            Console.WriteLine("Press any key to continue...");
                            Console.ReadKey();
                            return;
                        }
                    default:
                        break;
                }
            }
        }
        
        public static void Main(string[] args) //Ed ecco il nostro Main, che conterrà l'avvio del listener TCP e l'avvio della shell di gioco.
        {
            Console.WriteLine("________________________________________");
            Console.WriteLine("---------- BoxWar Game Server ----------");
            Console.WriteLine("¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯");
            try       {
                tcpListener.Start(); //Avvio del listener sulla porta 28960.
                Console.WriteLine("*** TCP Listener runned!");
                Thread gameShell = new Thread(new ThreadStart(GameShell)); //Creazione di un nuovo thread, che rappresenterà la shell del nostro server.
                gameShell.Start(); //Avvio del thread.
       }
       catch
       {
                Console.WriteLine("*** TCP Listener not runned! Please, try again.");
       }

        }
    }

Abbiamo così terminato il nostro server. Una volta avviato, esso rimarrà in attesa di un nostro comando. Nella versione rilasciata sono disponibili solo tre comandi:
start (avvia il game server)
stop (ferma il game server)
exit (esce dall'applicazione)
Altri comandi dovrete implementarli da voi, come esercizio. Tongue
Inoltre, nella versione rilasciata non è presente la gestione avanzata dei thread. Potete notare come non ci sono operazioni sotto il comando "stop". Il mio consiglio è quello di implementarvi un ThreadPool, sempre come esercizietto e confrontarlo con gli altri utenti in questo post.

Abbiamo scritto un applicativo server, benissimo. Ma ora vogliamo provarlo, no? Altrimenti può sembrare tutta fatica sprecata!
Per provare quanto abbiamo realizzato, scriveremo un piccolo programmino client che ci permetterà di testare il nostro server.

- Creiamo un altro progetto e utilizziamo questi namespace:
Codice:
using System;
using System.Net.Sockets;
using System.Threading; using System.IO;

- Creiamo la nostra classe client:
Codice:
classProgram
{
        public static void Main(string[] args)
        {
            TcpClient socketForServer;
            try{
                socketForServer = new TcpClient("localhost", 28960); //Il nostro client TCP si collegherà all'indirizzo localhost:28960, ovvero 127.0.0.1:28960, cioè al nostro PC alla porta dove è in ascolto il nostro server.
            }
            catch{
                Console.WriteLine("Failed to connect to server at {0}:28960", "localhost");
                return;
            }
            
            NetworkStream networkStream = socketForServer.GetStream();
            StreamReader streamReader = new StreamReader(networkStream);
            StreamWriter streamWriter = new StreamWriter(networkStream);
            Console.WriteLine("********** This is client who is connected to localhost **********");
            
            try{
                string outputString;
                
                Console.WriteLine("type: ");
                string str = Console.ReadLine();
                while(str != "exit" ){ //Il ciclo non terminerà finché non scriveremo "exit"
                    streamWriter.WriteLine(str);
                    streamWriter.Flush();
                    Console.WriteLine("type: ");
                    str = Console.ReadLine();
                }
                if(str == "exit"){
                    streamWriter.WriteLine(str);
                    streamWriter.Flush();
                }
            }
            catch{
                Console.WriteLine("Exception throwed!");
            }
            
            networkStream.Close();
            Console.WriteLine("Press any key to exit from client");
            Console.ReadKey();
        } }

Provate ad avviare più client e vedete come il server si comporta. Il client, ovviamente, è banale, ma con questo metodo è possibile implementare qualsiasi comando.



Beh, direi che è tutto per il momento. Magari più in là vi aggiungo anche la gestione dei thread e qualche altra funzioncina carina.
Il mio invito è quello di migliorare da voi quanto vi ho mostrato e condividerlo con chiunque seguirà questo thread e abbia voglia di imparare.

P.S: una prima miglioria da realizzare, in quanto a sicurezza, è quella di implementare una ApplicationGUID, in modo che chi non conosce questa GUID non può inviare comandi al nostro server.

Saluti!
Primo aggiornamento di sicurezza (molto banale).

Changelog:
implementazione di un AppGUID
implementazione del sistema di gestione pacchetti
assieme ai pacchetti possono essere passati N parametri

- Create una nuova classe ausiliare, essa si occuperà del comportamento da usare per ogni tipo di pacchetto ricevuto (comandi, azioni, ecc):
Codice:
using System;

namespace PoyPoy2RemakeServer
{
    class Comportamento
    {
        private static const string appGUID = "Vostra AppGUID";

        public static bool IsApplicationAllowed(string GUID)
        {
            return appGUID == GUID;
        }
    } }

- Modificate il metodo Listeners come segue:
Codice:
staticvoid Listeners(object sockThread){
            SockAndThread sockAndThread = (SockAndThread)sockThread;
            Socket socketForClient = sockAndThread.socket;

            ConnectedClient cnctdClient = playerList.Find( x => x.IP == socketForClient.RemoteEndPoint);

            if(socketForClient.Connected){
                currentNumberOfClients++;
                Console.WriteLine("*** Client: " + socketForClient.RemoteEndPoint + " now connected on the server!");
                
                Console.WriteLine("*** Connected clients: " + currentNumberOfClients);
                NetworkStream networkStream = new NetworkStream(socketForClient);
                StreamWriter streamWriter = new StreamWriter(networkStream);
                StreamReader streamReader = new StreamReader(networkStream);

                bool isClientEnabled = false;

                try
                {
                    while (true)
                    {
                        if (!socketForClient.Connected)
                        {
                            break;
                        }
                        string stringa = streamReader.ReadLine();
                        Console.WriteLine(socketForClient.RemoteEndPoint + " " + cnctdClient.nome + " : " + stringa);

                        string[] theString = stringa.Split(' ');

                        if (theString[0] == "IS_APPLICATION_ALLOWED")
                        {
                            isClientEnabled = Comportamento.IsApplicationAllowed(theString[1]);
                        }
                        if (isClientEnabled)
                        {
                            //String d'uscita
                            if (theString[0] == "EXIT_SUCCESS")
                            {
                                break;
                            }
                            //Elenco dei pacchetti di comandi inviati dai client e gestione
                            switch (theString[0])
                            {
                                case "":
                                    break;
                            }
                        }
                    }
                }
                catch
                {
                    currentNumberOfClients--;
                    Console.WriteLine(socketForClient.RemoteEndPoint + " disconnected");
                }
                streamReader.Close();
                streamWriter.Close();
                networkStream.Close();
            }
            socketForClient.Close();
            numberOfStartedThread--;
            playerList.Remove(cnctdClient);  }


Al prossimo aggiornamento!
Saluti! Big Grin
EDIT: Piccola aggiustatina! Big Grin