In questa guida, JAVA – Server Multithread, realizziamo delle applicazioni di rete a livello di Socket con protocollo TCP in JAVA. L’idea di base è di consentire una connessione multipla di diversi client ad un server. Qui in basso e nella sezione download si possono scaricare i sorgenti

In questa guida, JAVA – Server Multithread, realizziamo delle applicazioni di rete a livello di Socket con protocollo TCP in JAVA. L’idea di base è di consentire una connessione multipla di diversi client ad un server. Qui in basso e nella sezione download si possono scaricare i sorgenti


main.zip (101 download )

Sommario

Introduzione

Prima di iniziare è utile preparare l’ambiente di lavoro. Tutte le indicazioni sono disponibili alla pagina: https://profgiagnotti.it/guida-alla-creazione-dellambiente-di-lavoro-per-applicazioni-con-socket-in-java/

Server Multithread in Java

Capitolo 1: Generalità sui Thread

Quando si sviluppa un server, è comune che più client debbano connettersi contemporaneamente, spesso in un numero non predeterminato. Un esempio tipico è una chat server-client, in cui il numero di client attivi può variare dinamicamente. Per gestire in modo efficiente la ricezione e l’invio dei messaggi, è essenziale adottare un approccio multithreading, che consente di eseguire più operazioni in parallelo.

Concetti Teorici sui Thread

Un processo può essere considerato come composto da due componenti principali:

  1. Codice – La logica eseguibile che condivide la CPU con altri processi.
  2. Risorse – L’insieme delle variabili, file, memoria e altre risorse associate al processo.

Il sistema operativo gestisce queste due componenti in modo indipendente:

  • La parte di un processo che viene eseguita sulla CPU è chiamata thread o processo leggero (LWP – Light Weight Process).
  • La parte che possiede le risorse è chiamata processo o task.

Un thread è quindi un segmento di codice eseguito in modo sequenziale all’interno di un processo. Tutti i thread di un processo condividono le stesse risorse e risiedono nello stesso spazio di indirizzamento. Grazie al multithreading, è possibile eseguire più thread in parallelo, migliorando l’efficienza del programma.

Thread Safety

Non tutti i codici possono essere eseguiti in modalità multithreading. Affinché un programma funzioni correttamente con più thread, deve essere thread-safe, ovvero garantire che nessun thread possa accedere a dati in fase di modifica da parte di un altro thread. Java mette a disposizione strumenti per garantire la sicurezza dei dati, come synchronized, locks, e volatile.

Creazione di Thread in Java

Java offre due principali metodi per creare un thread:

  1. Implementare l’interfaccia Runnable (metodo più flessibile).
  2. Estendere la classe Thread.

Ci concentreremo sulla prima modalità, in quanto consente una maggiore modularità e facilita l’estensione delle classi in futuro.

Capitolo 2: Le Interfacce in Java

Un’interfaccia in Java è un tipo di riferimento che definisce un contratto per le classi che la implementano. Essa può contenere:

  • Metodi astratti (solo dichiarazione, senza implementazione).
  • Metodi di default (con implementazione, a partire da Java 8).
  • Metodi statici.
  • Costanti (variabili public static final).

Caratteristiche delle Interfacce

  • Definizione dei metodi: Le classi che implementano un’interfaccia devono fornire una concreta implementazione dei metodi dichiarati nell’interfaccia.
  • Ereditarietà multipla: Una classe può implementare più interfacce, superando il limite dell’ereditarietà singola tra classi.
  • Costanti: Tutte le variabili dichiarate in un’interfaccia sono implicitamente public static final e devono essere inizializzate immediatamente.
  • Polimorfismo: Un’interfaccia consente di scrivere codice più generico e flessibile.

Le classi che implementano un’interfaccia utilizzano la parola chiave implements. Inoltre, si può usare l’annotazione @Override per indicare che un metodo sta sovrascrivendo un altro definito nell’interfaccia.

In basso un esempio di implementazione dell’ interfaccia “Veicolo”


public interface Veicolo {
  public String targa = "";
  public float velocitàMax;
  public void accendi();//metodo astratto
  public void spegni();//metodo astratto
}

In basso un esempio di implementazione della classe “Auto” che implementa l’interfaccia “Veicolo”


public class Auto implements Veicolo {
  @Override
  public void accendi() {
      System.out.println("avvio del motore...");
  }
  @Override
  public void spegni() {
      System.out.println("spegnendo il motore...");
  }
}

In basso un esempio di implementazione di una istanza di “Veicolo”


Veicolo tesla = new Auto();

tesla.accendi(); 
tesla.spegni();
            

Capitolo 3: Implementazione di Runnable

La documentazione ufficiale di Java specifica che l’interfaccia Runnable è progettata per essere implementata da qualsiasi classe le cui istanze devono essere eseguite in un thread separato.

Caratteristiche dell’interfaccia Runnable

  • La classe che implementa Runnable deve definire il metodo:
void run();

Quando un oggetto Runnable viene utilizzato per creare un thread, l’invocazione del metodo start() su un’istanza di Thread farà partire l’esecuzione del metodo run().

Passaggi per creare un Thread con Runnable

  1. Creare una classe che implementa l’interfaccia Runnable.
  2. Definire il metodo run(), contenente il codice che deve essere eseguito nel thread.
  3. Nel metodo main() della classe principale:
    • Creare un’istanza della classe che implementa Runnable.
    • Passare questa istanza a un oggetto della classe Thread.
    • Avviare il thread con il metodo start().

Esempio di Implementazione

// Implementazione dell'interfaccia Runnable
class MyTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - Iterazione: " + i);
            try {
                Thread.sleep(1000); // Simula un'attività con una pausa di 1 secondo
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // Creazione delle attivazioni (istanze di MyTask)
        Runnable task1 = new MyTask();
        Runnable task2 = new MyTask();
        
        // Creazione degli esecutori (oggetti Thread)
        Thread thread1 = new Thread(task1, "Thread 1");
        Thread thread2 = new Thread(task2, "Thread 2");

        // Avvio dei thread
        thread1.start();
        thread2.start();
    }
}

Spiegazione del Codice

  1. La classe MyTask implementa Runnable, definendo il metodo run(), che stampa un messaggio a ogni iterazione.
  2. Nel main():
    • Vengono create due istanze della classe MyTask.
    • Le istanze vengono passate a due oggetti Thread.
    • I thread vengono avviati con start(), eseguendo il metodo run() in parallelo.

Output Atteso:

Thread 1 - Iterazione: 0
Thread 2 - Iterazione: 0
Thread 1 - Iterazione: 1
Thread 2 - Iterazione: 1
Thread 1 - Iterazione: 2
Thread 2 - Iterazione: 2
...

Capitolo 4: Primo Esempio – Implementazione di un Server Multithread con Runnable

Obiettivo

L’obiettivo di questo capitolo è creare un server multithread capace di accettare connessioni da un numero indefinito di client. Ogni client invierà il proprio nome e delle stringhe di testo. Il server risponderà restituendo le stringhe ricevute in maiuscolo.

Per implementare questo sistema utilizzeremo il multithreading, creando un thread separato per ogni client connesso. In questo modo, il server potrà gestire più richieste contemporaneamente senza bloccare il flusso di esecuzione.


Struttura del Server

Per realizzare il server, dobbiamo creare due classi principali:

  1. Classe Server (gestisce le connessioni e avvia i thread per ogni client).
  2. Classe ServerThread (implementa l’interfaccia Runnable e gestisce la comunicazione con il client).

Passaggi chiave per l’implementazione

  1. Creare la classe Server con:
    • Un costruttore per inizializzare il server.
    • Un metodo connetti() che permette di accettare le connessioni dai client.
    • Il metodo main() che crea un’istanza del server e avvia la gestione delle connessioni.
  2. Creare la classe ServerThread con:
    • Un costruttore che riceve il Socket del client.
    • Il metodo run() che gestisce la comunicazione con il client (lettura, elaborazione e risposta).

Implementazione del Server Multithread con interfaccia runnable

Realizziamo Un server che accetta connessioni multiple, chiede il nome del client e una stringa e la restituisce con caratteri maiuscoli

1. Classe Server (Server.java)



import java.io.*;
import java.net.*;

// Classe principale del server
public class Server {
    private static final int PORT = 12345; // Porta su cui il server ascolta
    private ServerSocket serverSocket;

     // Costruttore
     public Server(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void Connetti() {
        System.out.println("Server in ascolto sulla porta " + PORT);
        try {
            while (true) {
                // Attende una connessione da un client
                Socket clientSocket = serverSocket.accept();
                System.out.println("Client connesso: " + clientSocket.getInetAddress());

                
                // Crea un nuovo thread per gestire la comunicazione con il client
                ServerThread attivaThread = new ServerThread(clientSocket); //attivo il thread
                Thread thread = new Thread(attivaThread);//attivo l'esecutore
                thread.start(); // Avvio il thread
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close(); // Chiude il ServerSocket alla fine
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            Server server = new Server(PORT);
            server.Connetti();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// Classe per gestire la comunicazione con un client
class ServerThread implements Runnable {
    private Socket clientSocket; // Socket del client
    private BufferedReader in;
    private PrintWriter out;

    public ServerThread(Socket socket) {
        this.clientSocket = socket;
    }

    @Override
    public void run() {
        try {
            // Crea i canali di comunicazione
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            out = new PrintWriter(clientSocket.getOutputStream(), true);

            // Benvenuto e richiesta nome al client
            String benvenuto = "Ciao, dimmi il tuo nome: ";
            out.println(benvenuto);
            // Lettura del nome del client
            String nome = in.readLine();
            System.out.println(nome + " si è connesso! IP: " + clientSocket.getInetAddress() + " Porta: " + clientSocket.getPort());

            // Ciclo per leggere le stringhe inviate dal client
            String inputLine;
            while (true) {
                out.println("Scrivi una stringa (0=Esci): ");
                inputLine = in.readLine();
                if (inputLine.equals("0")) {
                    System.out.println(nome + " si è disconnesso");
                    break; // Termina la connessione se il client invia "0"
                }
                // Risponde con la stringa in maiuscolo
                out.println(inputLine.toUpperCase());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // Chiude le risorse
                in.close();
                out.close();
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Struttura del Codice

Il codice è diviso in due classi:

  1. Server → Gestisce le connessioni dei client e avvia un nuovo thread per ognuno.
  2. ServerThread → Implementa Runnable e gestisce la comunicazione con un singolo client.

Classe Server (Gestione delle Connessioni)
Attributi:
private static final int PORT = 12345; // Porta su cui il server ascolta
private ServerSocket serverSocket;
  • Il server ascolta sulla porta 12345.
  • ServerSocket è il socket che aspetta le connessioni in arrivo.

Costruttore:
public Server(int port) throws IOException {
    serverSocket = new ServerSocket(port);
}
  • Inizializza il ServerSocket sulla porta specificata.

Metodo Connetti()

Cosa fa questo metodo?

  • Mette il server in ascolto sulla porta 12345.
  • Attende connessioni dai client (serverSocket.accept()).
  • Per ogni client connesso, crea un nuovo oggetto ServerThread e avvia un thread separato (thread.start()).
  • Se il server viene chiuso, chiude il ServerSocket.
Metodo main()
  • Avvia un’istanza del server e chiama il metodo Connetti(), iniziando l’ascolto delle connessioni.

Classe ServerThread (Gestione di un singolo Client)

  • Questa classe implementa l’interfaccia Runnable, quindi può essere eseguita come thread separato.
  • Gestisce la comunicazione con un singolo client.

Attributi:
private Socket clientSocket;
private BufferedReader in;
private PrintWriter out;
  • clientSocket → Socket del client.
  • BufferedReader (in) → Per leggere i dati inviati dal client.
  • PrintWriter (out) → Per inviare dati al client.

Costruttore:
public ServerThread(Socket socket) {
    this.clientSocket = socket;
}
  • Salva il socket del client passato come parametro.

Metodo run()

Cosa fa questo metodo?

  1. Crea i canali di comunicazione con il client (in e out).
  2. Invia un messaggio di benvenuto e chiede il nome del client.
  3. Legge i messaggi inviati dal client.
  4. Se il client invia "0", termina la connessione.
  5. Risponde con la stringa ricevuta, trasformandola in maiuscolo.
  6. Alla chiusura, libera le risorse (in, out, clientSocket).

2. Classe Client (Client1.java)

Dopo aver implementato il Server si procede ad implementare i Client (n Client, occorre solo cambiare il nome) come segue:

package cs_tcp_06_Multithread_toUpperCase_runnable;


import java.io.*;
import java.net.*;

public class Client1 {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;
    private BufferedReader tastiera;
    private static final String SERVER_ADDRESS = "localhost"; // Indirizzo del server
    private static final int PORT = 12345; // Porta del server

    // Costruttore
    public Client1(String host, int port) throws IOException {
        socket = new Socket(SERVER_ADDRESS, PORT);
        tastiera = new BufferedReader(new InputStreamReader(System.in));
    }

    // Metodo per creare i canali di comunicazione
    private void creaCanali() throws IOException {
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new PrintWriter(socket.getOutputStream(), true);
    }

    // Metodo per la comunicazione con il server
    private void comunica() throws IOException {
        String messaggioDalServer;
        String userInput;

        // Ricezione messaggio di benvenuto
        messaggioDalServer = in.readLine();
        System.out.println(messaggioDalServer);

        // Invia il nome al server        
        userInput = tastiera.readLine();
        out.println(userInput); // Invio del nome al server

        // Ciclo per inviare messaggi al server    
        while (true) {
            // Ricevi il messaggio dal server (invito a scrivere una stringa)
            messaggioDalServer = in.readLine();
            System.out.println(messaggioDalServer);

            // Leggi input dell'utente da tastiera
            userInput = tastiera.readLine();
            out.println(userInput); // Invia la stringa al server

            // Se l'input è "0", termina la connessione
            if (userInput.equals("0")) {
                System.out.println("Disconnessione dal server...");
                break;
            }

            // Ricevi e stampa la risposta del server
            messaggioDalServer = in.readLine();
            System.out.println("Risposta dal server: " + messaggioDalServer);
        }

        // Chiudi le risorse alla fine
        chiudiConnessione();
    } 

    // Metodo per chiudere le risorse
    private void chiudiConnessione() throws IOException {
        in.close();
        out.close();
        socket.close();
        tastiera.close();
        System.out.println("Connessione chiusa.");
    }

    public static void main(String[] args) throws IOException {
        Client1 client1 = new Client1(SERVER_ADDRESS, PORT);
        client1.creaCanali();
        client1.comunica();
    }
}

Il codice implementa un client TCP in Java, che si connette al server multithread sviluppato in precedenza. Il client:

  1. Si collega al server sulla porta 12345.
  2. Invia il proprio nome.
  3. Scambia messaggi con il server (invia stringhe e riceve la versione in maiuscolo).
  4. Se l’utente invia "0", il client si disconnette.

Struttura del Codice

La classe Client1 ha tre parti principali:

  1. Connessione al Server → Si connette all’indirizzo localhost e alla porta 12345.
  2. Comunicazione → Scambia messaggi con il server.
  3. Chiusura della Connessione → Libera le risorse quando la sessione termina.

Classe Client1

Attributi
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private BufferedReader tastiera;
private static final String SERVER_ADDRESS = "localhost"; // Indirizzo del server
private static final int PORT = 12345; // Porta del server
  • socket → Permette di connettersi al server.
  • inLegge i dati ricevuti dal server.
  • outInvia dati al server.
  • tastieraLegge l’input dell’utente.
  • SERVER_ADDRESS e PORT → Definiscono l’indirizzo e la porta del server.

Costruttore
public Client1(String host, int port) throws IOException {
    socket = new Socket(SERVER_ADDRESS, PORT);
    tastiera = new BufferedReader(new InputStreamReader(System.in));
}

Cosa fa?

  • Crea un socket TCP e si connette al server localhost:12345.
  • Inizializza tastiera per leggere i dati dall’utente.

Metodo creaCanali()

Cosa fa?

  • Inizializza i canali di comunicazione tra client e server.
  • in → Riceve messaggi dal server.
  • out → Invia messaggi al server.

Metodo comunica()

Cosa fa?

  1. Legge il messaggio di benvenuto dal server e lo stampa.
  2. L’utente inserisce il suo nome, che viene inviato al server.

Ciclo di Comunicazione

Cosa fa?

  • Il server chiede un input all’utente.
  • Il client legge e invia la stringa al server.
  • Il server risponde con la stringa in maiuscolo.
  • Se l’utente inserisce "0", il client si disconnette.

Metodo chiudiConnessione()

osa fa?

  • Chiude i canali di comunicazione (in, out, socket, tastiera).

Metodo main()

Cosa fa?

  1. Crea un’istanza del client e si connette al server.
  2. Inizializza i canali di comunicazione.
  3. Avvia la comunicazione con il server.

Capitolo 5: Primo Esempio – Implementazione di un Server Multithread come estensione di Thread

A titolo di esempio si propone la soluzione dello stesso esercizio utilizzando la classe Thread:

Server.java

package cs_tcp_07_Multithread_toUpperCase_extendsThread;


import java.io.*;
import java.net.*;

// Classe principale del server
public class Server {
    private static final int PORT = 12345; // Porta su cui il server ascolta
    private ServerSocket serverSocket;

    // Costruttore
    public Server(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    // Metodo per iniziare ad ascoltare le connessioni
    public void Connetti() {
        System.out.println("Server in ascolto sulla porta " + PORT);
        try {
            while (true) {
                // Attende una connessione da un client
                Socket clientSocket = serverSocket.accept();
                System.out.println("Client connesso: " + clientSocket.getInetAddress());

                // Assegna un nuovo thread per gestire la comunicazione con il client
                ServerThread thread = new ServerThread(clientSocket);
                thread.start(); // Avvia il thread
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close(); // Chiude il ServerSocket alla fine
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            Server server = new Server(PORT);
            server.Connetti();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// Sottoclasse di Thread per gestire la comunicazione con un client
class ServerThread extends Thread {
    private Socket clientSocket;
    private BufferedReader in;
    private PrintWriter out;

    //costruttore
    public ServerThread(Socket socket) {
        this.clientSocket = socket;
    }

    @Override
    public void run() {
        try {
            // Crea i canali di comunicazione
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            out = new PrintWriter(clientSocket.getOutputStream(), true);

            // Benvenuto e richiesta nome al client
            String benvenuto = "Ciao, dimmi il tuo nome: ";
            out.println(benvenuto);
            // Lettura del nome del client
            String nome = in.readLine();
            System.out.println(nome + " si è connesso! IP: " + clientSocket.getInetAddress() + " Porta: " + clientSocket.getPort());

            // Ciclo per leggere le stringhe inviate dal client
            String inputLine;
            while (true) {
                out.println("Scrivi una stringa (0=Esci): ");
                inputLine = in.readLine();
                if (inputLine.equals("0")) {
                    System.out.println(nome + " si è disconnesso");
                    break; // Termina la connessione se il client invia "0"
                }
                // Risponde con la stringa in maiuscolo
                out.println(inputLine.toUpperCase());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // Chiude le risorse
                in.close();
                out.close();
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Client1.java

package cs_tcp_07_Multithread_toUpperCase_extendsThread;

import java.io.*;
import java.net.*;

public class Client1 {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;
    private BufferedReader tastiera;
    private static final String SERVER_ADDRESS = "localhost"; // Indirizzo del server
    private static final int PORT = 12345; // Porta del server

    // Costruttore
    public Client1(String host, int port) throws IOException {
        socket = new Socket(SERVER_ADDRESS, PORT);
        tastiera = new BufferedReader(new InputStreamReader(System.in));
    }

    // Metodo per creare i canali di comunicazione
    private void creaCanali() throws IOException {
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new PrintWriter(socket.getOutputStream(), true);
    }

    // Metodo per la comunicazione con il server
    private void comunica() throws IOException {
        String messaggioDalServer;
        String userInput;

        // Ricezione messaggio di benvenuto
        messaggioDalServer = in.readLine();
        System.out.println(messaggioDalServer);

        // Invia il nome al server        
        userInput = tastiera.readLine();
        out.println(userInput); // Invio del nome al server

        // Ciclo per inviare messaggi al server    
        while (true) {
            // Ricevi il messaggio dal server (invito a scrivere una stringa)
            messaggioDalServer = in.readLine();
            System.out.println(messaggioDalServer);

            // Leggi input dell'utente da tastiera
            userInput = tastiera.readLine();
            out.println(userInput); // Invia la stringa al server

            // Se l'input è "0", termina la connessione
            if (userInput.equals("0")) {
                System.out.println("Disconnessione dal server...");
                break;
            }

            // Ricevi e stampa la risposta del server
            messaggioDalServer = in.readLine();
            System.out.println("Risposta dal server: " + messaggioDalServer);
        }

        // Chiudi le risorse alla fine
        chiudiConnessione();
    } 

    // Metodo per chiudere le risorse
    private void chiudiConnessione() throws IOException {
        in.close();
        out.close();
        socket.close();
        tastiera.close();
        System.out.println("Connessione chiusa.");
    }

    public static void main(String[] args) throws IOException {
        Client1 client1 = new Client1(SERVER_ADDRESS, PORT);
        client1.creaCanali();
        client1.comunica();
    }
}

Test dell’applicazione

  • Aprire Visual Studio Code
  • Aprire la cartella in cui è contenuto il codice
  • Mandare in run il Server
  • Mandare in run i due Client
  • Verificare il funzionamento a console

Nella sezione Download dedicata puoi scaricare i codici sorgente di applicazioni Server Multithread con utilizzo di Socket TCP

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *