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 (526 download )

Sommario

Introduzione

Nello sviluppo di applicazioni di rete, un server deve essere in grado di gestire più client contemporaneamente, spesso in numero variabile e non prevedibile.
Un’implementazione basata su un singolo flusso di esecuzione risulterebbe rapidamente inefficiente, poiché ogni richiesta bloccherebbe le successive.

In questo articolo viene analizzato il concetto di server multithread in Java, affrontando sia gli aspetti teorici sia l’implementazione pratica tramite socket TCP.

L’obiettivo è comprendere:

  • il funzionamento dei thread in Java;
  • il ruolo dell’interfaccia Runnable e della classe Thread;
  • la realizzazione di un server capace di gestire più connessioni simultanee.

Prerequisiti

Prima di proseguire è consigliato predisporre correttamente l’ambiente di sviluppo.
La procedura completa è disponibile nella guida dedicata:

Server Multithread e Thread in Java

Quando più client si collegano allo stesso server, l’uso di un singolo flusso di esecuzione comporterebbe il blocco del servizio durante la gestione di ogni richiesta.
Il multithreading consente di risolvere questo problema assegnando a ciascun client un thread dedicato.

Concetti fondamentali

Un processo è composto da:

  • Codice: la logica eseguibile
  • Risorse: memoria, file, variabili

Il thread rappresenta l’unità di esecuzione che utilizza le risorse del processo.
Più thread possono convivere nello stesso processo condividendone lo spazio di memoria.


Thread Safety

Non tutte le applicazioni sono adatte all’esecuzione concorrente.
Un programma è thread-safe se garantisce che l’accesso simultaneo ai dati non produca risultati incoerenti.

Java fornisce diversi strumenti per la gestione delle risorse condivise, tra cui:

  • synchronized
  • lock
  • volatile

Creazione dei Thread in Java

Java offre due modalità principali:

  1. Implementazione dell’interfaccia Runnable
  2. Estensione della classe Thread

In questo articolo viene privilegiata la prima soluzione, più flessibile e modulare.


Le interfacce in Java e il loro ruolo nel Multithreading

In Java, un’interfaccia rappresenta un contratto che definisce un insieme di metodi (astratti, dei default e statici) che una classe si impegna a implementare. A differenza delle classi, un’interfaccia non fornisce una concreta implementazione del comportamento, ma stabilisce cosa una classe deve fare, non come.

L’implementazione di un’interfaccia avviene tramite la parola chiave implements, e l’annotazione @Override consente di indicare esplicitamente che un metodo sta sovrascrivendo un metodo dichiarato nell’interfaccia.

Questo meccanismo è particolarmente importante nella progettazione di applicazioni modulari e riusabili, ed è alla base di molte soluzioni adottate da Java, inclusa la gestione dei thread tramite l’interfaccia Runnable.


Perché Runnable è un’interfaccia

Runnable impone l’implementazione di un solo metodo:

void run();

Separando il comportamento del thread dal meccanismo di esecuzione, Java consente:

  • maggiore modularità;
  • riusabilità del codice;
  • possibilità di estendere altre classi.

Per questi motivi, Runnable rappresenta la soluzione preferibile nelle applicazioni multithread moderne.


Implementazione dell’interfaccia Runnable

L’interfaccia Runnable definisce un unico metodo:

void run();

Quando un oggetto Runnable viene passato a un’istanza di Thread e si invoca start(), il metodo run() viene eseguito in modo concorrente.

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

Server TCP Multithread con Runnable

Obiettivo

Realizzare un server TCP che:

  • accetta più client contemporaneamente,
  • riceve stringhe di testo,
  • restituisce la versione in maiuscolo.

Ogni client viene gestito tramite un thread dedicato.


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 del Server

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

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 Client

Il client stabilisce una connessione con il server, invia stringhe di testo e riceve le risposte elaborate.


Passaggi chiave per l’implementazione del Client

  1. Creare la classe Client con:
    • Un costruttore
    • Un metodo crea_canali() che permette di creare i canali di comunicazione con il Server
    • Il metodo comunica() per comunicare con il Server
    • Il metodo chiudi_connessione() per chiudere la connessione con il Server
    • il metodo main()

Classe Client (Client1.java)

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();
}

Considerazioni finali

Il client, insieme al server multithread, realizza un semplice ma completo sistema client–server TCP in Java.
Ogni client comunica con il server attraverso una connessione dedicata, mentre il server è in grado di gestire più client simultaneamente grazie all’uso dei thread.

Questo esempio rappresenta una base solida per comprendere la comunicazione di rete in Java e per sviluppare applicazioni distribuite più avanzate.

Runnable vs Thread

Oltre alla soluzione con Runnable, è possibile implementare un server multithread estendendo direttamente la classe Thread. Questo approccio permette di associare il comportamento del thread alla gestione del singolo client senza dover creare un oggetto Thread separato.

L’esempio proposto replica lo stesso esercizio: il server accetta più connessioni, riceve stringhe dai client e restituisce le stesse stringhe in maiuscolo. L’unica differenza è che la classe che gestisce il client estende 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();
            }
        }
    }
}

Spiegazione del codice:

  • La classe ServerThread estende Thread. Questo permette di chiamare direttamente start() sul thread appena creato senza dover passare un oggetto Runnable.
  • Il flusso di comunicazione con il client è identico all’esempio precedente: messaggio di benvenuto, lettura del nome e ciclo di scambio stringhe.
  • Il server può gestire più client contemporaneamente grazie alla creazione di un nuovo thread per ogni connessione.

Client1.java

Il client rimane invariato rispetto all’esempio con Runnable, perché non dipende dalla modalità con cui il server gestisce i thread

Test dell’applicazione

Per verificare il funzionamento:

  1. Aprire Visual Studio Code o un altro IDE.
  2. Eseguire il Server (Server.java).
  3. Avviare uno o più Client (Client1.java).
  4. Digitare i messaggi nel client e osservare la risposta in maiuscolo dal server.
  5. Inserire "0" per terminare la sessione del client.

Considerazioni didattiche

  • Questo esempio mostra che Java permette due approcci principali per creare server multithread:
    1. Implementare Runnable e passarlo a un Thread.
    2. Estendere direttamente la classe Thread.
  • Il comportamento finale è identico, ma l’estensione di Thread è più immediata per chi vuole associare direttamente il thread alla gestione del client.
  • La scelta tra Runnable ed estensione di Thread dipende spesso da esigenze di progettazione e dalla necessità di ereditare da altre classi.

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 *