Produzione Schede con Impianti Client e Magazzino Server TCP in Java

Un esercizio piuttosto articolato in Java che prevede una connessione ad Server e multi thread/client. Simuliamo un po’ quanto già visto con la dinamica multithread in locale (leggi qui), ma lo rivisitiamo anche in funzione di connessioni client/server con socket. Argomento spesso visto nelle lezioni di Tecnologia e Progettazione degli Istituti Tecnici ramo Informatica.

L’idea è quella di creare una sorta di server remoto che funga un po’ da magazzino centrale a cui vengono spedite le schede prodotto dagli Impianti satellite che altro non sono che client TCP. La difficoltà sta nel dover condividere alcune informazioni tra gli impianti e il server e c’è quindi bisogno di una sorta di variabile, che indichiamo come Buffer, che consente lo scambio di queste informazioni tra le varie componenti di rete in modo sincronizzato.

Server

La classe Magazzino, al di la delle necessarie strutture dati per la connessione, non presenta grandi anomalie. Gli stessi costruttori aprono un socket e inizializzano il buffer collettivo. Il metodo standard Run inizializza due vector, uno per salvare lo storico delle connessioni Socket, l’altro per salvare tutti i thread che vengono creati a fronte di una richiesta da netcat. Quindi i client e i loro thread non sono essenzialmente indipendenti qui in questo ma sono creati dal server. Da qui la necessità di creare il buffer condiviso dal server e passarlo in fase di costruzione dei singoli client. Al while infinito il compito di rimanere in attesa di una richiesta client da soddisfare. Viene quindi creata una uova classe impianto con annesso thread con socket, buffer condiviso e una lettera casuale per simulare il nome/idenificativo dell’impianto che si collega al server Magazzino. Il socket e l’impianto vengono immessi nei rispettivi vector per poterli riutilizzare eventualmente dopo.

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Random;
import java.util.Vector;

public class Magazzino implements Runnable
{
    public final static int SERVER_PORT = 1300;
    private ServerSocket server;
    private Buffer buffer;

    public Magazzino() throws IOException
    {
        server = new ServerSocket(SERVER_PORT);
        server.setSoTimeout(1000);
        buffer = new Buffer();
    }
    public Magazzino(int port) throws IOException
    {
        server = new ServerSocket(port);
        server.setSoTimeout(1000);//timeout di un secondo
        buffer = new Buffer();
    }

    public Magazzino(int port, Buffer b) throws IOException
    {
        this(port);
        buffer = new Buffer();
    }

    public void run()
    {
        Socket connection;

        Vector<Socket> elencoConnection = new Vector<Socket>();
        Vector<Impianto> elencoImpianti = new Vector<Impianto>();
        System.out.println("MAGAZZINO PRONTO");
        while (!Thread.interrupted())
        {
            try
            {
                connection = server.accept();
                System.out.println("Connessione richiesta da: " + connection.getInetAddress().toString() + ": " + connection.getPort());

                elencoConnection.add(connection);

                /* Stacco una lettera a caso per identificare il client/impianto che viene creato
                *  Non prevedo controllo duplicati
                **/
                Random random = new Random();
                char carattereCasuale = (char) (random.nextInt(26) + 'a');
                carattereCasuale = Character.valueOf(Character.toUpperCase(carattereCasuale));

                Impianto ct = new Impianto(connection, buffer,carattereCasuale);
                elencoImpianti.add(ct); //lista al momento inutilizzata se non per storico
                Thread client_thread = new Thread(ct);
                client_thread.start();
            }
            catch (SocketTimeoutException exception)
            {

                System.out.print("."); //stampa i punti fino a qunado non lo fermo
                //STAMPA INFORMAZIONI ALL'USCITA?
            }
            catch (IOException exception)
            {
            }

        }
        System.out.println(buffer.stampaSchede());
        for (Socket s : elencoConnection)
        {
            try
            {
                s.close();
            }
            catch (IOException e) { }
        }

        try
        {
            server.close();
        }
        catch (IOException exception) { }
    }
}

Buffer condiviso

Ma in realtà i lbuffer condiviso dovrebbe essere la classe più complessa, eppure il lettore più atento si accorgerà come la modalità di questa classe sia molto simile alla classe Model degli esercizi JavaFX o le classi gestione della porzione di OOP Java. Anchenqui ci troviamo di fronte ad una classe che semplicemente è una gestione di tutte le schede con un vector. Il vector ha il vantaggio di essere una struttura dati con accesso condiviso già regolato in modo automatico da opportuni semafori che ne consentono la sincronizazione necessaria.

I metodi sono intuitivi: produco una scheda a la inserisco nel vettore annotando l’impianto/client da cui deriva la richiesta, stampo l’elelenco delle schede inserite, l’ultima scheda inserita e la quantità di schede inserite.

import java.util.Vector;

public class Buffer
{
    private Vector<Scheda> listaSchede;

    public Buffer()
    {
        this.listaSchede = new Vector<>();
    }


    public synchronized String produci(char _impianto)
    {
        Scheda s = new Scheda(_impianto);
        listaSchede.add(s);  //creo una nuova scheda con orario e seriale casuale
        return s.toString();
    }


    /**
     * @return
     */
    public synchronized String getUltimaSchedaProdotta()
    {
        return this.listaSchede.lastElement().toString();
    }

    public synchronized int totaleProdotto()
    {
        return this.listaSchede.size();
    }

    public synchronized String stampaSchede()
    {
        String testo = null;

        for(Scheda scheda: this.listaSchede)
        {
            testo += "ORA: "+ scheda.getTempoProduzione() + " - Impianto/Seriale: " + scheda.toString() + "\n";
        }

        return testo;
    }
}

La risorsa prodotta

Samo finalmente alla classe risorsa che deve essere inviata dal thread client al server attraverso il buffer. Qui abbiamo simulto di produrre delle fantomatiche schede dotate di un seriale, una datatempo di produzione e una informazine su quale client/impianto lo abbia prodotto.

Non presenta particolari elementi da sottolneare: soliti metodi di acecsso, costruttori e un toString. Nel costruttore abbiamo simulato la creazione di un seriale di 4 cifre random. Il lettore, senza troppa fatica, potrebbe modificare l’esercizio per far si che il buffer condiviso crei un seriale standard progressivo magari da distribuire ai client.

import java.time.LocalDateTime;
import java.util.Random;

/**
 * Simuliamo un bene da produrre, schede elettroniche ad esempio
 * Ogni scheda è identificata da un seriale random di 5 cifre e
 * il DataTime di produzione (ovvero di creazione dell'istanza di scheda)
 * un carattere A, B, C... che identifica lo stabilimento (o meglio il thread) che lo ha prodotto
 */
class Scheda
{
    private String          seriale;
    private LocalDateTime   tempoProduzione;
    private char            stabilimentoProd;


    public Scheda()
    {
        this.tempoProduzione = LocalDateTime.now();

        Random r = new Random();
        this.seriale  = Integer.toString(r.nextInt(10));
        this.seriale  += Integer.toString(r.nextInt(10));
        this.seriale  += Integer.toString(r.nextInt(10));
        this.seriale  += Integer.toString(r.nextInt(10));
        this.seriale  += Integer.toString(r.nextInt(10));

    }

    public Scheda(char stabilimento)
    {
        this();
        this.stabilimentoProd = stabilimento;
    }
    public Scheda(String _seriale, char _s)
    {
        this.tempoProduzione = LocalDateTime.now();
        this.seriale = _seriale;
        this.stabilimentoProd = _s;
    }

    public Scheda(String _seriale, char _stabilimento, LocalDateTime _tempo)
    {
        this(_seriale, _stabilimento);
        this.tempoProduzione = _tempo;
    }

    public void setSeriale(String _s)
    {
        this.seriale = _s;
    }

    public String getSeriale()
    {
        return this.seriale;
    }

    public LocalDateTime getTempoProduzione()
    {
        return this.tempoProduzione;
    }

    public void setTempoProduzione(LocalDateTime tempoProduzione) {
        this.tempoProduzione = tempoProduzione;
    }

    public char getStabilimentoProd() {
        return stabilimentoProd;
    }

    public void setStabilimentoProd(char stabilimentoProd) {
        this.stabilimentoProd = stabilimentoProd;
    }

    @Override
    public String toString() {
        return this.stabilimentoProd+ "-" + this.seriale;
    }
}

Il Client

import java.io.*;
import java.net.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Classe per gestire e creare dei client thread che si collegano al TCPServer
 * Per connettere da Linux usa il comando netcat ip porta
 * (ad es. netcat 127.0.0.1 1300)
 * Il comando netcat in realtà "stuzziaca" il server. Sarà la classe ServerTCP
 * a creare una istanza di questa classe
 */
public class Impianto implements Runnable
{
    private Socket connection;
    private InputStream input;
    private OutputStream output;
    private Buffer buffer; //elemento condiviso
    private char id;
    private int qtaProdotta;

    public Impianto(Socket connection, Buffer buffer, char id) throws IOException
    {
        this.connection = connection;
        this.input = this.connection.getInputStream();
        this.output = this.connection.getOutputStream();
        this.buffer = buffer;
        this.id = id;
        this.qtaProdotta = 0;
    }

    @Override
    public void run()
    {
        byte[] buffer = new byte[64 * 1024];
        int data;
        String risposta = "";
        String cmd = "";



        try
        {
            risposta = "Benvenuto "+"\n"+
                       "DYNAMIC PORT = "+ connection.getPort()+"\n"+
                       "CONNECTION PORT = "+connection.getLocalPort()+ "\n" +
                       "IP = "+connection.getInetAddress().toString()+ "\n" +
                       "ID = " + this.id + "\n" +
                       "COMANDI DISPONIBILI: datetime, whois, prodotto, totaleprodotto, produci, bye, help, ultimo" + "\n";
            output.write(risposta.getBytes());
            output.flush();

            while ((data = input.read(buffer)) != -1 && !Thread.interrupted())
            {
                cmd = new String(buffer, 0, data, "ISO-8859-1");
                cmd = cmd.trim();

                String[] parts = cmd.split(" ");//splitta il comando in ingresso usando lo spazio vuoto come separatore

                switch (parts[0])
                {
                    case "datetime":
                        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
                        risposta = dtf.format(LocalDateTime.now());
                        break;

                    case "whois":
                        risposta = String.valueOf(Character.toUpperCase(this.id));
                        break;

                    case "prodotto":
                        risposta = String.valueOf(this.qtaProdotta);
                        break;

                    case "totaleprodotto":
                        risposta = String.valueOf(this.buffer.totaleProdotto());
                        break;

                    case "bye":
                        Thread.currentThread().interrupt();
                        Thread.currentThread().join();
                        risposta = "OK, alla prossima!";
                        break;

                    case "produci":
                        String s = this.buffer.produci(this.id);
                        this.qtaProdotta++;
                        risposta = "--- Scheda " + s + " prodotta ---";
                        break;

                    case "ultima":
                        risposta = this.buffer.getUltimaSchedaProdotta();
                        break;

                    case "help":
                        risposta = "COMANDI DISPONIBILI: datetime, whois, prodotto, produci, totaleprodotto, ultimo, bye, help";
                        break;

                    default:
                        risposta = "ERRORE";
                        break;
                }
                risposta += "\n";
                output.write(risposta.getBytes());
                output.flush();
            }


        }
        catch (IOException exception) {  }
        catch (InterruptedException e) {  }

        try
        {
            System.out.println("Connessione chiusa!");
            input.close();
            output.close();
            connection.shutdownInput();
            connection.shutdownOutput();
            connection.close();
        }
        catch (IOException _exception) {  }

    }
}

Il Main

La classe main non presenta particolari sorprese. Inizializziamo un Server/Magazzino col suo thread annesso e ne attendiamo l’interruzione eventuale.

import java.io.IOException;

public class Main
{
    public static void main(String[] args)
    {
        try
        {
            Magazzino server = new Magazzino(1300);
            Thread tserver = new Thread(server);
            tserver.start();

            System.in.read();
            tserver.interrupt();
            tserver.join();

        } catch (IOException exception) {
            System.err.println("Errore!");
        } catch (InterruptedException exception) {
            System.err.println("FINE·");
        }
    }
}

Ultima modifica 21 Febbraio 2023