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
Sommario
- Introduzione
- Server Multithread in Java
- Capitolo 1: Generalità sui Thread
- Capitolo 2: Le Interfacce in Java
- Capitolo 3: Implementazione di Runnable
- Capitolo 4: Primo Esempio – Implementazione di un Server Multithread con Runnable
- Capitolo 5: Primo Esempio – Implementazione di un Server Multithread come estensione di Thread
- Test dell’applicazione
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:
- Codice – La logica eseguibile che condivide la CPU con altri processi.
- 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:
-
Implementare l’interfaccia
Runnable
(metodo più flessibile). -
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
-
Creare una classe che implementa l’interfaccia
Runnable
. -
Definire il metodo
run()
, contenente il codice che deve essere eseguito nel thread. -
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()
.
- Creare un’istanza della classe che implementa
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
- La classe
MyTask
implementaRunnable
, definendo il metodorun()
, che stampa un messaggio a ogni iterazione. -
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 metodorun()
in parallelo.
- Vengono create due istanze della classe
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:
-
Classe
Server
(gestisce le connessioni e avvia i thread per ogni client). -
Classe
ServerThread
(implementa l’interfacciaRunnable
e gestisce la comunicazione con il client).
Passaggi chiave per l’implementazione
- 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.
- 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).
-
Un costruttore che riceve il
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:
-
Server
→ Gestisce le connessioni dei client e avvia un nuovo thread per ognuno. -
ServerThread
→ ImplementaRunnable
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?
-
Crea i canali di comunicazione con il client (
in
eout
). - Invia un messaggio di benvenuto e chiede il nome del client.
- Legge i messaggi inviati dal client.
- Se il client invia
"0"
, termina la connessione. - Risponde con la stringa ricevuta, trasformandola in maiuscolo.
- 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:
- Si collega al server sulla porta 12345.
- Invia il proprio nome.
- Scambia messaggi con il server (invia stringhe e riceve la versione in maiuscolo).
- Se l’utente invia
"0"
, il client si disconnette.
Struttura del Codice
La classe Client1
ha tre parti principali:
-
Connessione al Server → Si connette all’indirizzo
localhost
e alla porta12345
. - Comunicazione → Scambia messaggi con il server.
- 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. -
in
→ Legge i dati ricevuti dal server. -
out
→ Invia dati al server. -
tastiera
→ Legge l’input dell’utente. -
SERVER_ADDRESS
ePORT
→ 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?
- Legge il messaggio di benvenuto dal server e lo stampa.
- 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?
- Crea un’istanza del client e si connette al server.
- Inizializza i canali di comunicazione.
- 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