JavaRush /Java Blog /Random-IT /Le classi Socket e ServerSocket, o “Hello, server? Riesci...
Sergey Simonov
Livello 36
Санкт-Петербург

Le classi Socket e ServerSocket, o “Hello, server? Riesci a sentirmi?"

Pubblicato nel gruppo Random-IT
Introduzione: "Sul tavolo c'era un computer, dietro c'era un codificatore..." Le classi Socket e ServerSocket, o “Hello, server?  Riesci a sentirmi?"  -1 Una volta, uno dei miei compagni di classe pubblicò un altro risultato del suo studio su Java, sotto forma di uno screenshot di un nuovo programma. Questo programma era una chat multiutente. A quel tempo stavo appena iniziando il mio viaggio nel padroneggiare la programmazione in questo linguaggio, ma ho sicuramente notato a me stesso che "lo voglio!" Il tempo passò e, avendo finito di lavorare al progetto successivo come parte dell'approfondimento delle mie conoscenze di programmazione, mi ricordai di quell'incidente e decisi che era giunto il momento. In qualche modo ho già iniziato ad approfondire questo argomento per pura curiosità, ma nel mio libro di testo Java principale (era il manuale completo di Schildt) erano fornite solo 20 pagine per il pacchetto java.net. Questo è comprensibile: il libro è già molto grande. C’erano le tabelle dei metodi e dei costruttori delle classi principali, ma questo è tutto. Il passo successivo è, ovviamente, l'onnipotente Google: miriadi di articoli diversi in cui viene presentata la stessa cosa: due o tre parole sui socket e un esempio già pronto. L'approccio classico (almeno nel mio stile di studio) è quello di capire prima di quali strumenti ho bisogno per lavorare, cosa sono, perché servono, e solo poi, se la soluzione al problema non è ovvia, armeggiare con quelli già pronti -realizzate inserzioni, svitando dadi e bulloni. Ma ho capito cosa era cosa e alla fine ho scritto una chat multiutente. Esternamente, si è scoperto qualcosa del genere: Le classi Socket e ServerSocket, o “Hello, server?  Riesci a sentirmi?"  - 2qui cercherò di darti una comprensione delle basi delle applicazioni client-server basate su socket Java usando l'esempio della progettazione della chat. Nel corso Javarash farai chat. Sarà su un livello completamente diverso, bello, grande, multifunzionale. Ma prima di tutto, devi sempre gettare le basi, quindi qui dobbiamo capire cosa sta alla base di una sezione del genere. (Se trovate mancanze o errori scrivete in PM o in un commento sotto l'articolo). Cominciamo. Testa prima: “La casa che...” Per spiegare come avviene una connessione di rete tra un server e un client, prendiamo l'esempio ormai classico di un condominio. Supponiamo che un client debba in qualche modo stabilire una connessione con un server specifico. Cosa deve sapere il ricercatore sull'oggetto della ricerca? Sì, l'indirizzo. Il server non è un'entità magica sul cloud e quindi deve trovarsi su una macchina specifica. Per analogia con una casa, dove dovrebbe aver luogo l'incontro di due parti concordate. E per ritrovarsi in un condominio non basta un indirizzo del condominio, bisogna indicare il numero dell'appartamento in cui avrà luogo l'incontro. Allo stesso modo, su un computer possono esserci più server contemporaneamente e, affinché il client possa contattarne uno specifico, deve anche specificare il numero di porta attraverso il quale avverrà la connessione. Quindi, l'indirizzo e il numero di porta. Un indirizzo indica un identificatore della macchina nello spazio Internet. Può essere un nome di dominio, ad esempio "javarush.ru" o un IP normale. Porta- un numero univoco a cui è associata una determinata presa (di questo termine parleremo più avanti), ovvero è occupata da un determinato servizio in modo da poter essere utilizzata per contattarlo. Quindi, affinché almeno due oggetti si incontrino sul territorio di uno (server), il proprietario dell'area (server) deve occupare un appartamento specifico (porto) su di essa (auto), e il secondo deve trovare il luogo dell'incontro conoscendo l'indirizzo della casa (dominio o ip) e il numero dell'appartamento (porta). Testa due: Meet Socket Tra i concetti e termini legati al lavoro in rete, uno molto importante è Socket. Indica il punto attraverso il quale avviene la connessione. In poche parole, una presa collega due programmi su una rete. La classe Socketimplementa l'idea di un socket. Il client e il server comunicheranno attraverso i suoi canali di input/output: Le classi Socket e ServerSocket, o “Hello, server?  Riesci a sentirmi?"  - 3 questa classe viene dichiarata lato client e il server la ricrea, ricevendo un segnale di connessione. Ecco come funziona la comunicazione online. Per cominciare, ecco i possibili costruttori di classi Socket:
Socket(String Name_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-address, int порт) throws UnknownHostException
"nome_host" - implica un nodo di rete specifico, un indirizzo IP. Se la classe socket non riesce a convertirlo in un indirizzo reale ed esistente, verrà generata un'eccezione UnknownHostException. Il porto è un porto. Se viene specificato 0 come numero di porta, il sistema stesso assegnerà una porta libera. Un'eccezione può verificarsi anche se la connessione viene persa IOException. Va notato che il tipo di indirizzo nel secondo costruttore è InetAddress. Viene in soccorso, ad esempio, quando è necessario specificare un nome di dominio come indirizzo. Inoltre, quando un dominio significa diversi indirizzi IP, InetAddresspuoi usarli per ottenerne un array. Tuttavia, funziona anche con IP. Puoi anche ottenere il nome host, l'array di byte che costituisce l'indirizzo IP, ecc. Ne parleremo più approfonditamente, ma per i dettagli completi dovrai consultare la documentazione ufficiale. Quando un oggetto di tipo viene inizializzato Socket, il client a cui appartiene annuncia sulla rete che desidera connettersi al server a un indirizzo e un numero di porta specifici. Di seguito sono riportati i metodi utilizzati più frequentemente della classe Socket: InetAddress getInetAddress()– restituisce un oggetto contenente dati sul socket. Se il socket non è connesso - null int getPort()- restituisce la porta su cui avviene la connessione al server int getLocalPort()- restituisce la porta a cui è associato il socket. Il fatto è che il client e il server possono “comunicare” su una porta, ma le porte a cui sono legati possono essere completamente diverse boolean isConnected()- restituisce true se la connessione è stabilita void connect(SocketAddress address)- indica una nuova connessione boolean isClosed()- restituisce true se il socket è chiuso boolean isBound()- restituisce true, se il socket è effettivamente associato a un indirizzo, la classe Socketimplementa l'interfaccia AutoCloseable, quindi può essere utilizzata nel try-with-resources. Tuttavia è possibile chiudere un socket anche nel modo classico, utilizzando close(). Capo tre: e questo è un ServerSocket Diciamo che abbiamo dichiarato, sotto forma di classe Socket, una richiesta di connessione lato client. Come farà il server a indovinare il nostro desiderio? Per questo, il server ha una classe come ServerSockete il metodo accetta() al suo interno. I suoi costruttori sono presentati di seguito:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
In fase di dichiarazione ServerSocket non è necessario specificare l'indirizzo di connessione, perché la comunicazione avviene sulla macchina server. Solo con un host multicanale è necessario specificare a quale IP è legato il socket del server. Capo Tre.Uno: Il Server che dice No Poiché fornire ad un programma più risorse di quante ne abbia bisogno è costoso ed irragionevole, pertanto nel costruttore ServerSocketviene chiesto di dichiarare le massime connessioni accettate dal server durante il funzionamento. Se non è specificato, per impostazione predefinita questo numero verrà considerato uguale a 50. Sì, in teoria possiamo supporre che ServerSocketsi tratti dello stesso socket, solo per il server. Ma gioca un ruolo completamente diverso rispetto alla classe Socket. È necessario solo nella fase di creazione della connessione. Dopo aver creato un oggetto tipo, ServerSocketdevi scoprire che qualcuno vuole connettersi al server. Il metodo accetta() è collegato qui. Il target attende finché qualcuno non vuole connettersi ad esso e quando ciò accade restituisce un oggetto di tipo Socket, ovvero un socket client ricreato. E ora che il socket client è stato creato sul lato server, può iniziare la comunicazione bidirezionale. Creare un oggetto tipo Socketsul lato client e ricrearlo utilizzando ServerSocketil lato server è il minimo richiesto per la connessione. Capo quattro: Lettera a Babbo Natale Вопрос: Come comunicano esattamente il client e il server? Ответ:Attraverso flussi di I/O. Cosa abbiamo già? Un socket con l’indirizzo del server e il numero di porta del client, e la stessa cosa, grazie ad Accept(), lato server. Quindi è ragionevole supporre che comunicheranno tramite un socket. Per fare ciò, esistono due metodi che danno accesso a flussi InputStreame OutputStreamoggetti di tipo Socket. Eccoli:
InputStream getInputStream()
OutputStream getOutputStream()
Poiché leggere e scrivere byte semplici non è altrettanto efficiente, i flussi possono essere racchiusi in classi adattatore, bufferizzate o meno. Per esempio:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Affinché la comunicazione sia bidirezionale, tali operazioni devono essere eseguite su entrambi i lati. Ora puoi inviare qualcosa utilizzando e ricevere qualcosa utilizzando e viceversa. In realtà, questa è praticamente l'unica funzione della classe Socket. E sì, non dimenticare il metodo flush() BufferedWriter: scarica il contenuto del buffer. In caso contrario le informazioni non verranno trasmesse e, pertanto, non verranno ricevute. Il thread ricevente attende anche l'indicatore di fine riga – “\n”, altrimenti il ​​messaggio non verrà accettato, poiché in effetti il ​​messaggio non è completato e non è completo. Se questo ti sembra scomodo, non preoccuparti, puoi sempre utilizzare la classe PrintWriter, che deve terminare, specificare true come secondo argomento, quindi l'estrazione dal buffer avverrà automaticamente:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
Inoltre, non è necessario indicare la fine della riga; questa classe lo fa per te. Ma l'I/O delle stringhe è il limite di ciò che un socket può fare? No, vuoi inviare oggetti tramite flussi socket? Per l'amor di Dio. Serializzali e sei a posto:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Capo cinque: Comunicazione reale tramite Internet Poiché per connettersi tramite una rete reale con un indirizzo IP reale è necessario disporre di un server completo, e poiché:
  1. La nostra futura chat, come utilità, non ha tali capacità. Può solo stabilire una connessione e ricevere/inviare un messaggio. Cioè, non ha funzionalità di server reale.
  2. Il nostro server, contenente solo socket dati e flussi I/O, non può funzionare come un vero server WEB o FTP, quindi solo con questo non potremo collegarci ad Internet.
Inoltre, stiamo appena iniziando a sviluppare il programma, il che significa che non è abbastanza stabile per funzionare immediatamente con una rete reale, quindi utilizzeremo l'host locale come indirizzo di connessione. Cioè, in teoria, il client e il server non saranno comunque collegati in alcun modo se non tramite un socket, ma per il debug del programma si troveranno sulla stessa macchina, senza un vero contatto attraverso la rete. Per indicare nel costruttore Socketche l'indirizzo è locale, ci sono 2 modi:
  1. Scrivi "localhost" come argomento dell'indirizzo, indicando uno stub locale. Anche "127.0.0.1" è adatto a questo: è solo una forma digitale di uno stub.
  2. Utilizzando IndirizzoInet:
    1. InetAddress.getByName(null)- null punta a localhost
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
Per semplicità utilizzeremo "localhost" di tipo String. Ma anche tutte le altre opzioni sono praticabili. Capo Sei: E' il momento di una conversazione Allora, abbiamo già tutto il necessario per implementare una sessione di conversazione con il server. Tutto ciò che resta da fare è metterlo insieme: il seguente elenco mostra come il client si connette al server, gli invia un messaggio e il server, a sua volta, conferma di aver ricevuto il messaggio utilizzandolo come argomento nel suo: "Server. Giava"
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    private static Socket clientSocket; // socket for communication
    private static ServerSocket server; // server socket
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try  {
                server = new ServerSocket(4004); // server socket listening on port 4004
                System.out.println("Server is running!"); // server would be nice
                // announce your launch
                clientSocket = server.accept(); // accept() will wait until
                //someone won't want to connect
                try { // after establishing a connection and recreating the socket for communication with the client, you can go
                    // to create I/O streams.
                    // now we can receive messages
                    in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    // and send
                    out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                    String word = in.readLine(); // wait for the client to write something to us
                    System.out.println(word);
                    // without hesitation responds to the client
                    out.write("Hi, this is the Server! I confirm you wrote: " + word + "\n");
                    out.flush(); // push everything out of the buffer

                } finally { // in any case, the socket will be closed
                    clientSocket.close();
                    // streams would also be nice to close
                    in.close();
                    out.close();
                }
            } finally {
                System.out.println("Server closed!");
                    server.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
"Cliente.java"
import java.io.*;
import java.net.Socket;

public class Client {

    private static Socket clientSocket; // socket for communication
    private static BufferedReader reader; // we need a reader that reads from the console, otherwise
    // do we know what the client wants to say?
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try {
                // address - local host, port - 4004, same as the server
                clientSocket = new Socket("localhost", 4004); // with this line we request
                // the server has access to the connection
                reader = new BufferedReader(new InputStreamReader(System.in));
                // read messages from the server
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // write there
                out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                System.out.println("Did you have something to say? Enter it here:");
                // if the connection happened and the threads were successfully created - we can
                // work further and offer the client something to enter
                // if not, an exception will be thrown
                String word = reader.readLine(); // wait for the client to do something
                // will not write to the console
                out.write(word + "\n"); // send a message to the server
                out.flush();
                String serverWord = in.readLine(); // wait for the server to say
                System.out.println(serverWord); // received - display
            } finally { // in any case, you need to close the socket and streams
                System.out.println("The client has been closed...");
                clientSocket.close();
                in.close();
                out.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }

    }
}
Naturalmente, dovresti prima avviare il server, perché a cosa si connetterà il client all'avvio se non c'è qualcosa che lo colleghi? :) L'output sarà così: /* Vuoi dire qualcosa? Inseriscilo qui: Ciao, server? Riesci a sentirmi? Ciao, questo è il Server! Confermo, hai scritto: Ciao server? Riesci a sentirmi? Il client è stato chiuso... */ Evviva! Abbiamo insegnato al server a comunicare con il client! Affinché la comunicazione non avvenga in due repliche, ma quante ne vuoi, è sufficiente avvolgere la lettura e la scrittura dei thread in un ciclo while (vero) e indicare per l'uscita che, secondo un determinato messaggio, ad esempio, "esci" , il ciclo veniva interrotto e il programma terminava. Capo Sette: Multiutente è meglio, il fatto che il server possa sentirci è positivo, ma sarebbe molto meglio se potessimo comunicare con qualcuno della nostra specie. Allegherò tutti i sorgenti alla fine dell'articolo, quindi qui mostrerò pezzi di codice non sempre grandi, ma importanti che permetteranno, se usati correttamente, di inventare una chat multiutente. Quindi, vogliamo essere in grado di comunicare con qualche altro client attraverso il server. Come farlo? Ovviamente, poiché il programma client ha un proprio metodo main, significa che può essere lanciato separatamente dal server e in parallelo con altri client. Cosa ci dà questo? In qualche modo è necessario che con ogni nuova connessione il server non entri immediatamente in comunicazione, ma scriva questa connessione in una sorta di elenco e proceda ad attendere una nuova connessione, e una sorta di servizio ausiliario sia impegnato nella comunicazione con uno specifico cliente. E i client devono scrivere sul server e attendere una risposta indipendentemente l'uno dall'altro. I thread vengono in soccorso. Diciamo che abbiamo una classe responsabile di ricordare le nuove connessioni: dovrebbe avere quanto segue specificato:
  1. Numero di porta.
  2. L'elenco in cui scrive la nuova connessione.
  3. E ServerSocket, in un'unica (!) copia.
public class Server {

    public static final int PORT = 8080;
    public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // list of all threads

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(PORT);
            try {
            while (true) {
                // Blocks until a new connection is made:
                Socket socket = server.accept();
                try {
                    serverList.add(new ServerSomthing(socket)); // add a new connection to the list
                } catch (IOException e) {
                    // If it fails, the socket is closed,
                    // otherwise, the thread will close it when it exits:
                    socket.close();
                }
            }
        } finally {
            server.close();
        }
    }
}
Ok, ora ogni socket ricreato non andrà perso, ma verrà archiviato sul server. Ulteriore. Ogni cliente ha bisogno di qualcuno che lo ascolti. Creiamo un thread con le funzioni del server dell'ultimo capitolo.
class ServerSomthing extends Thread {

    private Socket socket; // socket through which the server communicates with the client,
    // except for it - the client and server are not connected in any way
    private BufferedReader in; // socket read stream
    private BufferedWriter out; // socket write stream

    public ServerSomthing(Socket socket) throws IOException {
        this.socket = socket;
        // если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        start(); // call run()
    }
    @Override
    public void run() {
        String word;
        try {

            while (true) {
                word = in.readLine();
                if(word.equals("stop")) {
                    break;                }
                for (ServerSomthing vr : Server.serverList) {
                    vr.send(word); // send the received message with
                   // bound client to everyone else including him
                }
            }

        } catch (IOException e) {
        }
    }

    private void send(String msg) {
        try {
            out.write(msg + "\n");
            out.flush();
        } catch (IOException ignored) {}
    }
}
Quindi, nel costruttore del thread del server, deve essere inizializzato un socket attraverso il quale il thread comunicherà con un client specifico. Anche thread di I/O e tutto il necessario per avviare un thread direttamente dal costruttore. Ok, ma cosa succede quando il thread del server legge un messaggio dal client? Restituiscilo solo al tuo cliente? Non molto efficace. Stiamo creando una chat multiutente, quindi abbiamo bisogno che ogni client connesso riceva ciò che ha scritto una persona. È necessario utilizzare l'elenco di tutti i thread del server associati ai propri client e inviare ogni messaggio inviato a un thread specifico in modo che lo invii al proprio client:
for (ServerSomthing vr : Server.serverList) {
    vr.send(word); // send the received message
    // from the linked client to everyone else, including him
}
private void send(String msg) {
    try {
        out.write(msg + "\n");
        out.flush();
    } catch (IOException ignored) {}
}
Ora tutti i clienti sapranno cosa ha detto uno di loro! Se non vuoi che il messaggio venga inviato alla persona che lo ha inviato (sa già cosa ha scritto!), semplicemente durante l'iterazione dei thread, specifica che durante l'elaborazione di un oggetto, il thisciclo si sposterà all'elemento successivo senza eseguire eventuali azioni su di esso. Oppure, se preferisci, invia un messaggio al client affermando che il messaggio è stato ricevuto e inviato con successo. Ora è tutto chiaro con il server. Passiamo al cliente, o meglio ai clienti! Lì è tutto uguale, per analogia con il client dell'ultimo capitolo, solo quando si crea un'istanza è necessario, come mostrato in questo capitolo con il server, creare tutto il necessario nel costruttore. Ma cosa succede se, durante la creazione di un cliente, non ha ancora avuto il tempo di inserire nulla, ma gli è già stato inviato qualcosa? (Ad esempio lo storico della corrispondenza di chi si è già collegato alla chat prima di lui). Quindi i cicli in cui verranno elaborati i messaggi inviati devono essere separati da quelli in cui i messaggi vengono letti dalla console e inviati al server per l'inoltro ad altri. I thread vengono di nuovo in soccorso. Non ha senso creare un client come thread. È più conveniente creare un thread con un loop nel metodo run che legga i messaggi e, per analogia, scriva anche:
// thread reading messages from the server
private class ReadMsg extends Thread {
    @Override
    public void run() {

        String str;
        try {
            while (true) {
                str = in.readLine(); // waiting for a message from the server
                if (str.equals("stop")) {

                    break; // exit the loop if it's "stop"
                }
                            }
        } catch (IOException e) {

        }
    }
}
// thread sending messages coming from the console to the server
public class WriteMsg extends Thread {

    @Override
    public void run() {
        while (true) {
            String userWord;
            try {
               userWord = inputUser.readLine(); // messages from the console
                if (userWord.equals("stop")) {
                    out.write("stop" + "\n");
                    break; // exit the loop if it's "stop"
                } else {
                    out.write(userWord + "\n"); // send to the server
                }
                out.flush(); // clean up
            } catch (IOException e) {

            }

        }
    }
}
Nel costruttore del client devi solo avviare questi thread. Come chiudere correttamente le risorse di un cliente se vuole andarsene? È necessario chiudere le risorse del thread del server? Per fare ciò, molto probabilmente dovrai creare un metodo separato che verrà chiamato all'uscita dal ciclo di messaggi. Lì dovrai chiudere il socket e i flussi I/O. Lo stesso segnale di fine sessione per un particolare client deve essere inviato al suo thread del server, che deve fare lo stesso con il suo socket e rimuoversi dall'elenco dei thread nella classe del server principale. Capo Otto: Non c'è limite alla perfezione Puoi inventare infinite nuove funzionalità per migliorare il tuo progetto. Ma cosa dovrebbe essere trasferito esattamente a un client appena connesso? Penso che gli ultimi dieci eventi siano accaduti prima del suo arrivo. Per fare ciò, è necessario creare una classe in cui l'ultima azione con qualsiasi thread del server verrà inserita nell'elenco dichiarato e, se l'elenco è già pieno (ovvero ce ne sono già 10), eliminare la prima e aggiungere l'ultimo che è arrivato. Affinché il contenuto di questo elenco venga ricevuto da una nuova connessione, quando si crea un thread del server, nel flusso di output, è necessario inviarlo al client. Come farlo? Ad esempio, in questo modo:
public void printStory(BufferedWriter writer) {
// ...
}
Il thread del server ha già creato flussi e può passare il flusso di output come argomento. Successivamente, devi solo passare tutto ciò che deve essere trasferito al nuovo cliente in un ciclo di ricerca. Conclusione: queste sono solo le basi e molto probabilmente questa architettura di chat non funzionerà durante la creazione di un'applicazione reale. Questo programma è stato creato per scopi didattici e sulla base di esso ho mostrato come si può far comunicare il client con il server (e viceversa), come farlo per più connessioni e, ovviamente, come questo è organizzato sui socket. I sorgenti sono riordinati di seguito ed è allegato anche il codice sorgente del programma in analisi. Questa è la mia prima esperienza di scrittura di un articolo) Grazie per l'attenzione :)
  1. Pensare in Java Enterprise, di Bruce Eckel et. Al. 2003
  2. Java 8, La guida completa, Herbert Schildt, 9a edizione, 2017 (Capitolo 22)
  3. Articolo sulla programmazione dei socket in Java sui socket
  4. Presa nella documentazione ufficiale
  5. ServerSocket nella documentazione ufficiale
  6. fonti su GitHub
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION