Enrera
Mòdul 7
Iniciació a la programació en Java
  Pràctica
1
2
3
4
4
   
Exercicis
Exercicis
 
 
  Comunicació TCP/IP de baix nivell entre hosts: els sockets
   

IPs, ports i sockets:

Una màquina està connectada a la xarxa per alguna mena de dispositiu físic (una targeta Ethernet, per exemple) que sol ser únic. Totes les dades que entren i surten de la màquina ho fan a través d'aquest dispositiu. Però algunes de les dades són per a una aplicació determinada, altres per una altra... Com es distingeixen i s'encaminen cadascuna cap a la seva destinació?

Cada conjunt de dades que circula per la xarxa Internet, o per la xarxa d'una Intranet, porta annexa informació respecte al nom de la màquina que les ha de rebre, l'adreça IP, que és un nombre enter de 32 bits, és a dir, un nombre de quatre xifres escrit en base 256. Això aconsegueix que les dades siguin "lliurades" només a la màquina destinatària i siguin ignorades per les altres màquines de la xarxa. Típicament, una adreça IP té aquest aspecte:

193.145.88.16 (màquina a la XTEC)

192.168.0.120 (màquines a intranets petites)

Ara, la màquina que rep les dades ha de saber a quina de les aplicacions que hi corren li van destinades. Això s'aconsegueix perquè les dades porten annex també un nombre (enter de 16 bits) que és el port. Cadascuna de les aplicacions que esperen dades haurà establert un socket (endoll) i l'haurà associat a un port. Llavors el socket funciona com un endoll virtual pel qual només hi circulen les dades dirigides al port que li està associat.

Parlant des del punt de vista del programador: si volem que la nostra aplicació mogui dades per la xarxa (amb el protocol TCP/IP!) hem de construir sockets i associar-los a ports. Les dades han de portar, annexa, informació respecte a adreces IP i ports.

   
Alguns números de port el costum els ha associat a aplicacions estàndard: el port 21 correspon a servidors FTP, el port 80 a servidors HTTP, el port 23 a connexions Telnet, encara que això és simplement un conveni avalat per l'ús corrent.
   

Fem un experiment bàsic de connexió TCP/IP amb sockets i ports. Intenteu fer aquesta connexió via telnet a "The National Institute for Standards and Technology" des de la consola de comandes del windows:

C:\WINDOWS>telnet india.colorado.edu 13

Quan doneu aquesta instrucció, esteu intentant una connexió a un dels servidors del NIST. Aquesta màquina té en marxa un programa que té accés a l'hora d'un rellotge atòmic. Quan nosaltres enviem un paquet amb la sol·licitud de connexió al servidor pel port 13, el programa interpreta que ens interessa saber l'hora, genera un paquet amb un cadena que descriu l'hora actual (amb molta precisió!) ens l'envia i després tanca la nostra connexió.

Programar aquest model de comunicació amb Java és bastant senzill. Estudiem ara com Java tracta elsl sockets.

   
Sockets per connectar
   
  • Un socket és, doncs, un endoll amb una etiqueta identificativa, el número de port, amb el qual una aplicació surt a la xarxa. A Java, un socket és una instància de la classe java.net.Socket. El mètode constructor més elemental (el que farem servir aquí) és:

    public Socket(String host, int port)

    String host és una cadena (string) que representa, o bé, la IP de la màquina amb la qual volem connectar (193.145.88.16, 192.168.0.207, etc.), o bé el seu nom (pie.xtec.net, ftp.rediris.es, etc.), i int port és el número de port pel qual aquesta màquina remota ens espera. Amb aquesta construcció, la nostra màquina porta la iniciativa de la connexió. La postura passiva, és a dir, la d'esperar connexions d'altres màquines, requereix una mica més de sofisticació i la tractem més avall.

  • Una vegada establert el socket, tant poden sortir dades cap a la màquina remota com poden entrar-ne les que ens envia. La gestió és aquesta:

    1. Construir els objectes de flux d'entrada (classe java.io.InputStream) i sortida (classe java.io.OutputStream) amb els mètodes de la classe java.net.Socket:

      public InputStream getInputStream()
      public OutputStream getOutputStream()

      (Consulteu-ne la documentació per veure la mena d'excepcions que tiren)

    2. Gestionar els objectes de flux de la manera que convingui. Als tres exemples següents, la gestió es fa així:

      • Per convertir els bytes de l'inputstream a caràcters, construïm un reader (classe java.io.InputStreamReader) per a aquest inputstream. Després, construïm un altre reader (classe java.io.BufferedReader) per a aquest inputstreamreader, que ens permetrà llegir les dades línia per línia. Consulteu la documentació d'aquestes dues classes). El mètode de la classe java.io.BufferedReader,

        public String readLine()

        ens permet llegir les dades d'entrada (suposat que són caràcters!) línia a línia.

      • Per poder escriure sobre l'outputstream, construïm un writer (classe java.io.PrintWriter) per a aquest outputstream. El mètode per escriure és:

        public void println(String x)

        i és de la
        classe java.io.PrintWrite
Posem tot això en funcionament:
   
Pràctica

Es tracta de connectar-se a una màquina de la qual sabem que ens respondrà. Pot servir-nos qualsevol servidor FTP, cosa que implica que el port ha de ser el 21. La següent aplicació (projecte Enviar, "Empty Project") estableix un socket a algun dels servidors i ports posats com a variables de classe:

        String host="siscu05.infovia.xtec.net";
        int port=21;
        
//String host="ftp.rediris.es";
        //int port=21;
        //String host="pie.xtec.net";
        //int port=21;

(comenteu i descomenteu al vostre gust).

Cada vegada que escriviu alguna cosa al teclat (stdIn) i premeu "Retorn", s'envia aquest text a la màquina remota i es llegeix una línia d'allò que la màquina remota ens està enviant.

   
 
   
  El codi és aquest:
   
 
import java.io.*;
import java.net.*;

public class Enviar {

    public static void main(String[] args) throws IOException {

        
//String host="siscu05.infovia.xtec.net";
        
//int port=21;
        String host="ftp.rediris.es";
        int port=21;
        
//String host="pie.xtec.net";
        
//int port=21;
        Socket socket= null;
        PrintWriter out=null;
        BufferedReader in=null;
            try {
                socket=new Socket(host,port);
                OutputStream os=socket.getOutputStream();
                out=new PrintWriter(os,true);
                InputStream is=socket.getInputStream();
                InputStreamReader isr=new InputStreamReader(is);
                in=new BufferedReader(isr);
            } catch (UnknownHostException e) {
                System.err.println("No conec l'host: "+host);
                System.exit(1);
            } catch (IOException e) {
                System.err.println("No puc establir "+
                                   "connexió I/O a "+host);
                System.exit(1);
            }
        InputStreamReader isr=new InputStreamReader(System.in);
        BufferedReader stdIn=new BufferedReader(isr);
        String userInput="Escriu comanaments per a "+host;
            while (userInput!=null) {
                userInput=stdIn.readLine();
                out.println(userInput);
                System.out.println("R>"+in.readLine());
                    if (userInput.equals("quit")) {
                        System.out.println("Programa acabat...");
                        out.close();
                        in.close();
                        stdIn.close();
                        socket.close();
                        System.exit(0);
                    }
            }
    }

}

   
  Si connecteu amb un servidor FTP (port 21) o proveu de fer una connexió Telnet (port 23) heu d'enviar textos que siguin comanaments, per tal que la màquina remota, en rebrel's, els entengui. Comandes adequades són "user el_vostre_nom_d'usuari" ("user anonymous" si no teniu cap compte a la màquina remota), "pass el_vostre_password" ("pass la_meva_adreça_de_correu" si entreu com a usuari anònim), "pwd", etc.
   
Sockets i threads:
   
A l'exemple anterior, l'esquema és "rebo-envio-rebo-envio-rebo-...". Però ja es veu que per a una bona comunicació, els processos de rebre i d'enviar han de ser independents. No ha de caldre que nosaltres enviem res per tal que veiem allò que rebem. La solució immediata és separar el procés de rebre en un thread apart. Això és el que es fa en el següent exemple (projecte EnviarAmbFils, "Empty Project"):
   
 
   
Pràctica
import java.io.*;
import java.net.*;

public class EnviarAmbFils {

    //static String host="siscu05.infovia.xtec.net";
    //static int port=21;

    static String host="ftp.rediris.es";
    static int port=21;
    //static String host="pie.xtec.net";
    //static int port=21;

    RebreThread rebreThread;
    BufferedReader in=null;

    public static void main(String[] args) throws IOException {
        EnviarAmbFils mainAp=new EnviarAmbFils();
        Socket socket=null;
        PrintWriter out=null;
            try {
                socket=new Socket(host,port);
                OutputStream os=socket.getOutputStream();
                out=new PrintWriter(os,true);
                InputStream is=socket.getInputStream();
                InputStreamReader isr=new InputStreamReader(is);
                mainAp.in=new BufferedReader(isr);
            } catch (UnknownHostException e) {
                System.err.println("No conec l'host: "+host);
                System.exit(1);
            } catch (IOException e) {
                System.err.println("No puc establir "+
                                   "connexió I/O a "+host);
                System.exit(1);
            }
        mainAp.rebreThread=new RebreThread(mainAp);
        mainAp.rebreThread.start();
        InputStreamReader isr=new InputStreamReader(System.in);
        BufferedReader stdIn=new BufferedReader(isr);
        String userInput=" ";
            while (userInput!=null) {
                userInput=stdIn.readLine();
                out.println(userInput);
                    if (userInput.equals("quit")) {
                        System.out.println("Programa acabat...");
                        out.close();
                        mainAp.in.close();
                        stdIn.close();
                        socket.close();
                        System.exit(0);
                    }
            }
    }

}

class RebreThread extends Thread {

    EnviarAmbFils mainAp;

    public RebreThread (EnviarAmbFils ap) {
        super();
        mainAp=ap;
    }

    public void run () {
        String missatge;
            while (true) {
                    try {
                        missatge=mainAp.in.readLine();
                        System.out.println("R>"+missatge);
                    } catch (IOException eIO) {
                        System.err.println("No rebo res de "+
                                           EnviarAmbFils.host);
                        System.out.println("Programa acabat...");
                        System.exit(1);
                    }
            }
    }

}
   
  Observeu que a l'haver d'instanciar la classe EnviarAmbFils en un objecte mainAp, les variables de classe, o bé es referencien com a mainAp.variable (variables de l'objecte), o bé han de ser static! (variables de la classe).
   
  Sockets per escoltar. Convertint la nostra màquina en un servidor:
   

Si del que es tracta és que la nostra màquina estigui en espera d'eventuals connexions que puguin requerir altres màquines de la xarxa, llavors direm que la nostra màquina actua com a servidor (server) i el procediment a seguir és aquest:

  • Construir un serversocket, que és una instància de la classe java.net.ServerSocket. El mètode constructor més elemental (el que farem servir aquí) és:

    public ServerSocket(int port)

    El paràmetre int port és el número de port al qual s'han de dirigir les màquines remotes per connectar-se a la nostra. Un serversocket és, doncs, un endoll virtual que una aplicació exposa a l'exterior.

  • Per tal que la connexió s'obri efectivament, cal crear un socket pel qual hi circularan els streams corresponents (i que ja hem après a gestionar abans). Cal cridar al mètode de la classe java.net.ServerSocket:

    public Socket accept()

    Quan alguna màquina de la xarxa requereix la connexió, s'executa aquest mètode i es crea el socket i ja pot circular la informació entre ambdues màquines!
Pràctica
L'exemple següent és una variació de l'anterior per tal que la nostra màquina actui com a servidor. El resultat és una aplicació de xat en pantalla negra, mitjançant la qual podem enviar i rebre en temps real missatges de text a una altra màquina de la xarxa:
   
 
import java.io.*;
import java.net.*;

public class Xat {
    static String host="192.168.0.150";
    static int portRemot=10;
    static int portLocal=10;
    RebreThread rebreThread;
    Socket xerraSocket=null,escoltaSocket=null;
    ServerSocket serverSocket=null;

    public static void main(String[] args) throws IOException {
            if (args.length>0) {
                host=args[0];
            }
            if (args.length>1) {
                portRemot=Integer.parseInt(args[1]);
            }
            if (args.length>2) {
                portLocal=Integer.parseInt(args[2]);
            }
        Xat mainAp=new Xat();
        PrintWriter out=null;
            try {
                mainAp.serverSocket=new ServerSocket(portLocal);
            } catch (IOException e) {
                System.out.println("No puc obrir el meu port "+
                                    portLocal);
                System.exit(1);
            }
        mainAp.rebreThread=new RebreThread(mainAp);
        mainAp.rebreThread.start();
        InputStreamReader isr=new InputStreamReader(System.in);
        BufferedReader stdIn=new BufferedReader(isr);
        String userInput=" ";
            while (userInput!=null) {
                userInput=stdIn.readLine();
                    try {
                        mainAp.xerraSocket=new Socket(host,
                                                      portRemot);
                        OutputStream os=
                                mainAp.xerraSocket.getOutputStream();
                        out=new PrintWriter(os,true);
                    } catch (UnknownHostException e) {
                        System.out.println("No conec l'host: "+
                                           mainAp.host);
                        System.exit(1);
                    } catch (IOException e) {
                        System.out.println("No puc connectar a "+
                                           host);
                        System.exit(1);
                    }
                out.println(userInput);
                out.close();
                mainAp.xerraSocket.close();
                mainAp.xerraSocket=null;
                    if (userInput.equals("quit")) {
                        System.out.println("Programa acabat...");
                        stdIn.close();
                        System.exit(0);
                    }
            }
    }

}

class RebreThread extends Thread {

    Xat mainAp;

    public RebreThread (Xat ap) {
        super();
        mainAp=ap;
    }

    public void run () {
        String missatge;
            while (true) {
                    try {
                        mainAp.escoltaSocket=
                                        mainAp.serverSocket.accept();
                        InputStream is=
                               mainAp.escoltaSocket.getInputStream();
                        InputStreamReader isr=
                                           new InputStreamReader(is);
                        BufferedReader in=new BufferedReader(isr);
                        missatge=in.readLine();
                        System.out.println("R>"+missatge);
                        in.close();
                        mainAp.escoltaSocket.close();
                    } catch (IOException eIO) {
                        System.out.println("Lectura fallada: "+
                                                   mainAp.portLocal);
                    }
                mainAp.escoltaSocket=null;
            }
    }

}
   

Observeu:

  • Per defecte, la màquina es connectarà a la màquina 192.168.0.150 pel seu port 10. El port que ofereix per a connexions és el 10. Això es pot variar al cridar l'execució de l'aplicació si la demanem amb els paràmetres:

    <IP de la maquina remota> <port remot> <port local>

  • Necessiteu dues màquines connectades en xarxa per assajar l'aplicació!

  • Allò que escriviu vosaltres apareixerà a la pantalla sense més. Al prémer "Retorn", el text, llegit del reader isr (que llegeix de la entrada estàndard del sistema, System.in, és a dir, del teclat) s'escriu al writer out i marxa cap a l'altra màquina.

  • El text que ve de l'altra màquina, llegit del reader isr del thread (que llegeix de l'inputstream del socket, is) s'escriu a la sortida estàndard del sistema, és a dir, de la pantalla, precedit de "R>".
   
  Temps morts d'un socket:
   

Als programes que hem escrit, si el socket no pot establir la connexió amb el servidor, es queda esperant. El programa quedarà penjat tot el temps que el sistema operatiu permeti. És raonable decidir d'antuvi el temps mort que li permetrem, cridant el mètode de la classe java.net.Socket, public void setSoTimeout(int timeout).

Socket s = new Socket(...);
s.setSoTimeout(15000);
// temps en mil·lisegons d'espera

D'aquesta forma, quan un socket sobrepassi el temps mort admisible, llençarà una excepció del tipus InterruptedIOException que el nostre programa pot interceptar i, llavors, actuarà en consequència.

   

El SDK amb el que treballem en aquest curs, el 1.4 permet abreujar aquest procés:

Socket s = new Socket();
s.connect(new InetSocketAddress("servidor",port),tempsmort);

És clar que d'aquesta forma fem incompatible el nostre codi amb SQK més antics.

   
  Resoldre les IPs d'internet:
   
Com hem vist en els programes d'exemples anteriors, una de les limitacions que tenim és que per a fer la connexió necessitem la IP del servidor, mentres que sovint, estem força més acostumats a memoritzar o desar l'adreça de l'host pel seu nom. Una altra novetat que incorpora la versió 1.4 del SDK de Java és el de permetre la resolució de noms de manera fàcil. Si als vostres programes us resulta més fàcil utilitzar el nom del lloc que la seva IP, llavors, per a resoldre el nom, podeu fer servir la classe InetAddress del paquet java.net.
   
Pràctica Escriviu aquest petit programa per tal de verificar-ne el funcionament:
   
 
import java.net.*;

class AInternet {

    public static void main(String[] args) {
            try {
                String host = "www.xtec.net";
                InetAddress[] ads = InetAddress.getAllByName(host);
                    for (int i=0; i < ads.length; i++) {
                        System.out.println(ads[i]);
                    }
            } catch (Exception e) {
            }
    }

}

   
   
   
 
Amunt