Socket In Java

Ti sei mai chiesto come funziona lo streaming? Oppure cosa succede dopo che hai inviato un messaggio su WhatsApp? Tutto parte dai Socket!

Cos'è un Socket

Socket significa presa e prima di tutto è un concetto: Prendi due prese, collegale con un cavo e potranno comunicare tra loro. La prima implementazione di Socket nasce negli anni '80 come API (Application Programming Interface) per trasferire dati tra processi nei sistemi operativi Unix. Il concetto si è poi rivelato essere talmente buono e innovativo, che a queste API sono state aggiunte altre famiglie di socket:

  • AF_UNIX: usati da Unix per l'IPC (Inter process Comunication), solo per processi locali
  • AF_INET: usano il protocollo IP v4
  • AF_INET6: usano il protocollo IP v6
  • AF_BTH: comunicano con Bluetooth

In base alle caratteristiche del software da realizzare possiamo scegliere il tipo di socket:

  • SOCK_STREAM (Stream socket): quando vogliamo che i dati vengano ricevuti tutti e in ordine (chat, download di file, siti web)
  • SOCK_DGRAM (Datagram socket): quando ci interessa solo che la comunicazione sia veloce (streaming, multiplayer online)

Il Socket quindi è un oggetto software che permette la comunicazione tra processi locali o host remoti.

In questa guida ci concentreremo sui socket AF_INET.

Socket IPv4 in Java

Nei Socket AF_INET lo Stream Socket utilizza il protocollo TCP, mentre il Datagram Socket usa UDP.

Socket Address

Per identificare un Socket è necessario un indirizzo, chiamato Socket Address. Nel caso della famiglia AF_INET, il Socket Address è composto da due elementi:

  • Indirizzo IP: Un numero di 4 Byte, rappresentato in singoli byte in notazione decimale puntata (es. 127.0.0.1), che identifica l'host.
  • Numero di porta: Un numero di 2 Byte, da 0 a 65535, che identifica il processo associato al Socket.

Datagram Socket

La comunicazione con UDP dei Datagram Socket è molto semplice: un socket (client) invia una richiesta ad un altro socket (server) e questo può ritornargli una risposta come no.

Datagram Socket

Scriviamo un semplice programma per mostrare l'utilizzo della Classe DatagramSocket.

Abbiamo un server che ricevuta una connessione risponde con l'ora esatta.

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class OraEsattaServer {

    private int porta;
    private DatagramSocket socket;

    public OraEsattaServer(int porta) throws SocketException{
        
        //Apro il socket sulla porta specificata

        this.porta = porta;
        this.socket = new DatagramSocket(porta);

        System.out.println("Server avviato sulla porta " + porta);
    }

    public void receive(){

        try{

            // Ricevo una richiesta (non leggo i dati ricevuti, non mi servono)

            DatagramPacket richiesta = new DatagramPacket(new byte[256], 256);
            socket.receive(richiesta);

            // Mi prendo l'ora esatta, la trasformo in string e poi in array di byte per poterla inviare
            DateTimeFormatter formatoOra = DateTimeFormatter.ofPattern("HH:mm:ss");
            String ora = "L'ora esatta è: " + formatoOra.format(LocalDateTime.now());
            byte[] bufferOra = ora.getBytes();

            // Mi prendo ip e porta del client dal pacchetto che ho ricevuto
            InetAddress ipClient = richiesta.getAddress();
            int portaClient = richiesta.getPort();

            // Invio la risposta al client
            DatagramPacket risposta = new DatagramPacket(bufferOra, bufferOra.length, ipClient, portaClient);
            socket.send(risposta);

        }
        catch(IOException e){
            System.out.println("Errore nella risposta: "+ e);
        }

    }

    public static void main(String[] args) {
        
        int serverPort = 8000;

        OraEsattaServer server;

        try{
            server = new OraEsattaServer(serverPort); 
        }
        catch(SocketException e){
            System.out.println("Errore all'avvio: "+ e);
            return;
        }

        System.out.println("Premere Ctrl + C per fermare il server");

        while(true){
            server.receive();
        }
    }
}

Abbiamo poi un client che gli invia una richiesta:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;

public class OraEsattaClient {

    public static void main(String[] args){

        int serverPort = 8000;
        String serverIP = "127.0.0.1";

        try{
            // Mi prendo l'IP dalla string serverIP e apro un socket
            InetAddress serverAddress = InetAddress.getByName(serverIP);
            DatagramSocket socket = new DatagramSocket();

            // Creo e invio una richiesta (vuota, non devo inviare nessun dato)
            DatagramPacket richiesta = new DatagramPacket(new byte[256], 256, serverAddress, serverPort);
            socket.send(richiesta);

            // Ricevo la risposta dal server
            byte[] bufferRisposta = new byte[256];
            DatagramPacket risposta = new DatagramPacket(bufferRisposta, bufferRisposta.length);
            socket.receive(risposta);

            // Trasformo in string ala risposta
            String ora = new String(bufferRisposta, 0, risposta.getLength());

            // Stampo l'ora esatta e chiudo il client
            System.out.println(ora);
            socket.close();
        }
        catch(SocketTimeoutException e){
            System.out.println("Timeout: "+ e);
        }
        catch(IOException e){
            System.out.println("Errore nell' elaborazione della risposta: "+ e);
        }

    }
}

Avviamo prima il server, poi il client; Da quest'ultimo otterremo il seguente output:

L'ora esatta è: 21:18:28

Stream Socket

La comunicazione con gli Stream Socket è un po' più complessa: siccome il protocollo TCP è orientato e confermato dal lato server avremo un socket per ogni connessione stabilita con un client.

Per stabilire la connessione il client farà una richiesta alla porta nota del server, il socket in ascolto delegherà la richiesta ad un nuovo socket, il quale effettuerà un three way handshake con il client e risponderà alle sue richieste. Il socket che si è liberato potrà tornare in ascolto di nuovi client. In questo modo il server può gestire più richieste contemporaneamente.

Stream Socket

Scriviamo un programma con le classi ServerSocket e Socket. In base al comando che invieremo sarà possibile ottenere la data o l'ora.

Abbiamo un server con un ServerSocket in ascolto. Quando un client tenta di connettersi il ServerSocket istanzierà un Socket che continuerà la comunicazione su un thread separato.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DataOraServer {

    private int porta;
    private ServerSocket serverSocket;

    public DataOraServer(int porta) throws IOException{
        
        //Apro il socket sulla porta specificata

        this.porta = porta;
        this.serverSocket = new ServerSocket(porta);

        System.out.println("Server avviato sulla porta " + porta);
    }

    public void listen(){

        try{

            // Ascolto in attesa di una richiesta di connessione

            Socket delegato = serverSocket.accept();

            // Avvio la comunicazione con il client su un nuovo thread, per poter accettare altre richieste

            Thread t = new Thread(() -> gestisciClient(delegato));
            t.start();

        }
        catch(IOException e){
            System.out.println("Errore nella connessione con il client: "+ e);
        }

    }

    private void gestisciClient(Socket socket){

        try{
            // Mi prendo gli stream per ricevere ed inviare dati al client
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            DateTimeFormatter formatoOra = DateTimeFormatter.ofPattern("HH:mm:ss");
            DateTimeFormatter formatoData = DateTimeFormatter.ofPattern("dd/MM/yyyy");

            //rimango in ascolto del client finchè è connesso
            while(socket.isConnected()){

                String comando = in.readLine();

                switch(comando){

                    case "data":
                        out.println("Data: " + formatoData.format(LocalDateTime.now()));
                        break;
                        
                    case "ora":
                        out.println("Ora: " + formatoOra.format(LocalDateTime.now()));
                        break;

                    default:
                        out.println("Comando non riconosciuto");
                        break;
                }
            }
        }
        catch(IOException e){
            System.out.println("Errore nella comunicazione con il client: "+ e);
        }
    }

    public static void main(String[] args) {
        
        int serverPort = 8000;

        DataOraServer server;

        try{
            server = new DataOraServer(serverPort); 
        }
        catch(IOException e){
            System.out.println("Errore all'avvio: "+ e);
            return;
        }

        System.out.println("Premere Ctrl + C per fermare il server");

        while(true){
            server.listen();
        }
    }
}

Abbiamo un client che chiede la data e successivamente l'ora:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;


public class DataOraClient {

    public static void main(String[] args){

        int serverPort = 8000;
        String serverIP = "127.0.0.1";

        try{
            // Apro un socket e mi connetto al server
            Socket socket = new Socket(serverIP, serverPort);

            // Mi prendo gli stream per ricevere ed inviare dati al server
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //chiedo la data
            out.println("data");

            // Ricevo la risposta dal server e la stampo
            String risposta = in.readLine();
            System.out.println(risposta);

            //chiedo l'ora
            out.println("ora");

            // Ricevo la risposta dal server e la stampo
            risposta = in.readLine();
            System.out.println(risposta);

            //Chiudo la connessione con il server
            socket.close();
        }
        catch(SocketTimeoutException e){
            System.out.println("Timeout: "+ e);
        }
        catch(IOException e){
            System.out.println("Errore nell' elaborazione della risposta: "+ e);
        }
    }
}

Avviamo prima il server, poi il client; Da quest'ultimo otterremo il seguente output:

Data: 30/09/2020
Ora: 21:35:11
© Nicola Bovolato 2020