Protocolos de comunicación en red

Repaso de la arquitectura TCP/IP

La arquitectura TCP/IP está compuesta por una serie de capas o niveles en los que se encuentran los protocolos que implementan las funciones necesarias para la comunicación entre dos dispositivos en red. Esta arquitectura es independiente del modelo teórico OSI, aunque tiene muchas similitudes (ambos modelo se basan en capas o niveles). Se puede afirmar que el modelo OSI es el empleado en el estudio de las redes de datos mientras que el modelo o arquitectura TCP/IP es un modelo real empleado es las redes actuales.

En la siguiente figura se aprecian los niveles o capas de los modelos OSI y TCP/IP.

Representación de capas o niveles OSI y TCP/IP.

A continuación, se describe cada una de las capas y los protocolos incluidos en ellas dentro del modelo TCP/IP.

Capa de acceso a red

Ofrece la capacidad de acceder a cualquier red física, es decir, brinda los recursos que se deben implementar para transmitir datos a través de la red local. Por tanto, la capa de acceso a la red contiene especificaciones relacionadas con la transmisión de datos por una red física (red local) Ethernet, en anillo, FDDI, etc. En este nivel, dependiendo del hardware de acceso, se define la estructura de datos conocida como trama que tendrá una vida útil unicamente en la red local.

Representación de una trama Ethernet.

En este nivel se definen direcciones físicas o direcciones MAC de los dispositivos. Las tramas emplearán estas direcciones para especificar origen y destino de los datos que transportan. Las direcciones MAC están compuestas por 6 bytes (48 bits) donde los tres primeros bytes identifican al fabricante de la tarjeta de red. Los otros tres bytes identifican a la tarjeta.

Representación de una dirección MAC en formato hexadecimal.

Capa de Internet

La capa de Internet, también conocida como capa de red o capa IP, acepta y transfiere paquetes para la red. Esta capa incluye el famoso protocolo de Internet (IP), el protocolo de resolución de direcciones (ARP) y el protocolo de mensajes de control de Internet (ICMP).

Protocolo IP

El protocolo IP y sus protocolos de enrutamiento asociados son posiblemente la parte más significativa del conjunto TCP/IP. El protocolo IP se encarga de:

  • Direcciones IP: Las convenciones de direcciones IP forman parte del protocolo IP. Se corresponde con la identificación de equipos en la red. Las direcciones IP cambian en función de la red en la que está presente el dispositivo. Las direcciones IP identifican equipos en la conexión extremo a extremo. Actualmente se emplean direcciones de versión 4 (32 bits) y versión 6 (128 bits).

  • Encaminamiento: El protocolo IP determina la ruta que debe utilizar un paquete, basándose en la dirección IP del destinatario.

  • Formato de paquetes: el protocolo IP agrupa paquetes en unidades conocidas como datagramas. Los datagramas viajaran entre el origen y destino IP dentro de las tramas de datos.

  • Fragmentación: Si un paquete es demasiado grande para su transmisión a través del medio de red, el protocolo IP del sistema de envío divide el paquete en fragmentos de menor tamaño. Cuando los fragmentos llegan al receptor, el protocolo IP del sistema receptor reconstruye los fragmentos y crea el paquete original.

El que una aplicación conozca la IP de la máquina en la que está siendo ejecutada es fundamental para aplicaciones que requieren concexión de datos. En el capítulo anterior se presentó un método para obtener esta identificación si se empleaba una red Wifi. Independientemente del tipo de red al que nos conectemos con el móvil tenemos más opciones para informar de la IP del equipo. En este caso utilizamos la clase InetAddress y el método getHostAddress().

El siguiente código hade uso del método anterior para obtener la IP del smartphone:

    private String getIpAddress()
    {
        String ip = "";
        try
        {
            Enumeration<NetworkInterface> enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (enumNetworkInterfaces.hasMoreElements())
            {
                NetworkInterface networkInterface = enumNetworkInterfaces.nextElement();
                Enumeration<InetAddress> enumInetAddress = networkInterface.getInetAddresses();
                while (enumInetAddress.hasMoreElements())
                {
                    InetAddress inetAddress = enumInetAddress.nextElement();

                    if (inetAddress.isSiteLocalAddress())
                    {
                        ip += "IP: " + inetAddress.getHostAddress() + "\n";
                    }

                }

            }

        } catch (SocketException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
            ip += "¡Algo fue mal! " + e.toString() + "\n";
        }

        return ip;
    }

Protocolo ARP

El protocolo de resolución de direcciones (ARP) se encuentra conceptualmente entre el vínculo de datos (acceso a red) y las capas de Internet. ARP ayuda al protocolo IP a dirigir los datagramas al sistema receptor adecuado realizando la correspondencia entre direcciones MAC (48 bits de longitud) y las direcciones IP conocidas (32 bits de longitud).

Protocolo ICMP

El protocolo de mensajes de control de Internet (ICMP) detecta y registra las condiciones de información y error de la red. Situaciones como la falta de conectividad, problemas en la fragmentación o la redirección de datagrams son detectadas e informadas al origen con mensajes ICMP.

Capa de transporte

La capa de transporte TCP/IP se encarga de identificar las aplicaciones que desean conectarse en red. Recibe y envía datos de las aplicaciones y, en función de la seguridad y/o fiabilidad necesaria, así como la rapidez, puede prestar dos tipos de servicio a las aplicaciones: seguro y fiable (TCP), frente a rápido y no fiable (UDP).

En la capa de transporte se definen los números de puerto (16 bits). En Internet se suele asociar a cada aplicación un número de puerto concreto. En la máquina que hace de "servidor" estos números de puerto se asocian a aplicaciones genéricas: 80 para la web, 25 para el correo electrónico, 7 para ECHO o 4661 para eDonkey. El cliente suele emplear puertos con una numeración más elevada, también de 16 bits, que se incrementará cada vez que se abre una nueva aplicación que desea comunicación en red. Estos números podrán ser, por ejemplo: 1242, 2045, 3021, etc.

Protocolo TCP

TCP permite a las aplicaciones comunicarse entre sí como si estuvieran conectadas físicamente. TCP envía los datos en un formato que se transmite carácter por carácter, en lugar de transmitirse por paquetes discretos. Esta transmisión consiste en establecer una conexión y un control de la llegada de los datos. Cuando todo ha sido enviado exitosamente se procede al cierre de la conexión. El protocolo de transporte denomina a su estructura de datos como segmento.

La descarga Web o la transferencia de ficheros precisan de una conexión con TCP.

TCP, por tanto, confirma que un paquete ha alcanzado su destino dentro de una conexión establecida entre los hosts de envío y recepción. El protocolo TCP se considera un protocolo fiable orientado a la conexión.

Protocolo UDP

UDP proporciona un servicio de entrega de datagramas. UDP no verifica las conexiones entre los hosts transmisores y receptores. Dado que el protocolo UDP elimina los procesos de establecimiento y verificación de las conexiones, resulta ideal para las aplicaciones que envían pequeñas cantidades de datos o cuando el volumen de datos es elevado y éste tiene que llegar en tiempo real (aquí se prefiere la rapidez a la fiabilidad).

VoIP es un ejemplo de aplicación que utilizará UDP.

Capa de aplicación

La capa de aplicación define las aplicaciones de red y los servicios de Internet estándar que puede utilizar un usuario. Estos servicios utilizan la capa de transporte para enviar y recibir datos. Existen varios protocolos estandarizados de aplicación:

  • HTTP (páginas web)

  • FTP (tranferencia de archivos)

  • telnet (conexión a servidor)

  • SSH (conexión segura a servidor)

  • SMTP (correo electrónico)

  • POP (correo electrónico)

  • DNS (resolución de nombres de dominio)

Estructura de datos transporte en TCP/IP

Siguiendo el esquema de la encapsulación de protocolos de TCP/IP, los datos de las aplicaciones se introducirán en segmentos TCP o en datagramas UDP en función del requerimiento en cuanto a seguridad, fibilidad o rapidez comentado anteriormente.

La estructura de datos de TCP se denomina segmento y posee una cabecera de control considerablemente mayor que la cabecera del datagrama UDP que es la denominación de la estructura de datos de UDP. En este último caso, UDP añade a los datos de aplicación los números de puerto origen y destino. TCP necesita más campos en su cabecera para controlar la información enviada y, por tanto, ademas de los números de puerto tendremos números de secuencia, número de ACK, señalización, etc.

Representación de la cabecera de TCP.

Sockets

Los sockets son un sistema de comunicación entre procesos de diferentes máquinas en red (ordenadores, smartphones,...). Más exactamente, un socket es un punto de comunicación por el cual un proceso puede emitir o recibir información.

Cuando utilizamos Sockets para comunicar procesos nos basamos en la arquitectura cliente y servidor. Así pues, estableceremos dos Sockets: uno será la parte servidor y recibirá la transmisión del cliente y otro será la parte cliente que recibirá la respuesta del servidor.

Dependiendo el protocolo con el que vamos a realizar la conexión, tendremos dos tipos de Socket, los que utilizan el protocolo TCP, y los que utilizan el protocolo UDP. Ambos sockets utilizan la dirección IP (32 bits) así como el número de puerto (16 bits) de las máquinas cliente y servidor para poder establecer los puntos de comunicación de los procesos (una conexión estará formada por un par de sockets, que son los extremos de la conexión).

Sockets stream (TCP)

Los sockets stream ofrecen un servicio orientado a conexión, donde los datos se transfieren como un flujo continuo, sin encuadrarlos en registros o bloques. Este tipo de socket se basa en el protocolo TCP que, tal y como se ha comentado antes, es un protocolo orientado a conexión. Esto implica que antes de transmitir información hay que establecer una conexión entre los dos sockets. Mientras uno de los sockets atiende peticiones de conexión (servidor), el otro solicita la conexión (cliente). Una vez que los dos sockets están conectados, ya se puede transmitir datos en ambas direcciones. El protocolo incorpora de forma transparente al programador la corrección de errores. Es decir, si detecta que parte de la información no llegó a su destino correctamente, esta volverá a ser trasmitida. Además, no limita el tamaño máximo de información a transmitir.

Sockets datagram (UDP)

Los sockets datagram se basan en el protocolo UDP y ofrecen un servicio de transporte sin conexión. Es decir, podemos mandar información a un destino sin necesidad de realizar una conexión previa. El protocolo UDP es más eficiente que TCP, pero tiene el inconveniente que no se garantiza la fiabilidad. Además, los datos se envían y reciben en datagramas (paquetes de información) de tamaño no limitado a un valor concreto. La entrega de un datagrama no está garantizada: estos pueden ser duplicados, perdidos o llegar en un orden diferente al que se envió.

La gran ventaja de este tipo de sockets es que apenas introduce sobrecarga sobre la información transmitida. Además, los retrasos introducidos son mínimos, lo que los hace especialmente interesantes para aplicaciones en tiempo real, como la transmisión de audio y vídeo sobre Internet. Sin embargo, presenta muchos inconvenientes al programador: cuando transmitimos un datagrama no tenemos la certeza de que este llegue a su destino, por lo que, si fuera necesario, tendríamos que implementar nuestro propio mecanismo de control de errores.

Programación de Sockets

El modelo comunicación de los sockets es el siguiente:

  • El servidor establece un puerto y espera durante un cierto tiempo (timeout) a que el cliente establezca la conexión. Cuando el cliente solicite una conexión, el servidor abrirá esta conexión socket con el método accept().

  • El cliente establece una conexión con la máquina host a través del puerto que se designe el parámetro respectivo.

  • El cliente y el servidor se comunican con manejadores InputStream y OutputStream.

  • Se procede al cierre de la conexión.

En la siguiente figura se representa este esquema de comunicación de sockets entre cliente y servidor.

Conexión de sockets y comunicación entre cliente y servidor.

Los permisos en el manifest para las conexiones cliente / servidor pueden ser únicamente:

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Como recomendación del alumno Rafael Espí Botella, es importante tener en cuenta la targetApi que se declara en el manifest, pues en los móviles con Android 6.0 se deben gestionar los permisos de la app de forma explícita. Los que poseen la api 22 o anterior deberían funcionar con normalidad. Por lo tanto, lo más práctico es dejar la targetApi 22 y compilar con 22 ó 23. Más información en: http://developer.android.com/intl/es/training/permissions/declaring.html

Creación y apertura de sockets

A la hora de programar aplicaciones cliente / servidor, es importante tener en cuenta el rol de cada máquina, algo que se tendrá en cuenta en la programación a desarrollar. En la parte de cliente:

Socket miCliente;
miCliente = new Socket(IPservidor, numeroPuerto);

Donde IPservidor será la IP del equipo con el que intentamos una conexión y numeroPuerto es el puerto (un número) sobre el cual nos queremos conectar. Cuando se selecciona un número de puerto, se debe tener en cuenta que los puertos en el rango 0-1023 están reservados para usuarios con muchos privilegios (superusuarios o root). Estos puertos son los que utilizan los servicios estándar del sistema como email, ftp o http. Para las aplicaciones que se desarrollen, es importante asegurarse la selección de un puerto por encima del 1023.

En el ejemplo anterior no se usan excepciones; sin embargo, es una gran idea la captura de excepciones cuando se está trabajando con sockets. El mismo ejemplo quedaría como:

Socket miCliente;
try {
     miCliente = new Socket(IPservidor, numeroPuerto);
    }
catch(IOException e)
    {
     System.out.println(e);
    }

Si estamos programando un Servidor, la forma de apertura del socket es la que muestra elsiguiente ejemplo:

Socket miServicio;
try {
     miServicio = new ServerSocket(numeroPuerto);
    }
catch(IOException e)
     {
      System.out.println(e);
     }

A la hora de la implementación de un servidor también necesitamos crear un objeto socket desde el ServerSocket para que esté atento a las conexiones que le puedan realizar clientes potencialesy poder aceptar esas conexiones:

Socket socketServicio = null;
try {
     socketServicio = miServicio.accept();
     }
catch(IOException e)
    {
     System.out.println(e);
    }

Creación de streams

Antes de continuar, aclarar que todo cliente ha de conocer la dirección del socket del servidor; en este caso los valores se indican en el par de variables ip y puerto. Nunca tenemos la certeza de que el servidor admita la conexión, por lo que es obligatorio utilizar una sección try / catch. La conexión propiamente dicha se realiza con el constructor de la clase Socket. Siempre hay que tener previsto que ocurra algún problema, en tal caso, se creará la excepción y se pasará a la sección catch. En caso contrario, continuaremos obteniendo el InputStream y el OutputStream asociado al socket. Lo cual nos permitirá obtener las variables, entrada y salida mediante las que podremos recibir y transmitir información, que es el siguiente paso que a continuación se detalla.

Streams de entrada

En la parte Cliente de la aplicación, se puede utilizar la clase DataInputStream para crear unstream de entrada que esté listo a recibir todas las respuestas que el servidor le envíe.

DataInputStream entrada;
try {
    entrada = new DataInputStream(miCliente.getInputStream());
    }
catch(IOException e)
    {
    System.out.println(e);
    }

La clase DataInputStream permite la lectura de líneas de texto y tipos de datos primitivos de Java de un modo altamente portable; dispone de métodos para leer todos esos tipos como: read(), readChar(), readInt(), readDouble() y readLine(). Deberemos utilizar la función que creamos necesaria dependiendo del tipo de dato que esperemos recibir del servidor.

En el lado del Servidor, también usaremos DataInputStream, pero en este caso para recibir las entradas que se produzcan de los clientes que se hayan conectado:

DataInputStream entrada;
try {
     entrada = new DataInputStream(socketServicio.getInputStream());
    }
catch(IOException e)
    {
     System.out.println(e);
    }

Streams de Salida

En la parte del Cliente podemos crear un stream de salida para enviar información al socket del servidor utilizando las clases PrintStream, PrintWriter o DataOutputStream:

PrintStream salida; 
try {
     salida = new PrintStream(miCliente.getOutputStream());
    }
catch(IOException e)
    {
     System.out.println(e);
    }
try {
    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),
                    true);

    out.println(mensaje);


    catch (UnknownHostException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
DataOutputStream salida;
try {
     salida = new DataOutputStream(miCliente.getOutputStream());
    }
catch(IOException e)
    {
     System.out.println(e);
    }

La clase DataOutputStream permite escribir cualquiera de los tipos primitivos de Java, muchosde sus métodos escriben un tipo de dato primitivo en el stream de salida. De todos esos métodos, el más útil quizás sea writeBytes().

En el lado del Servidor, podemos utilizar la clase PrintStream para enviar información al cliente:

PrintStream salida;
try {
     salida = new PrintStream(socketServicio.getOutputStream());
    }
catch(IOException e)
    {
     System.out.println(e);
    }

Pero también podemos utilizar la clase DataOutputStream como en el caso de envío de información desde el cliente.

Cierre de sockets

Siempre deberemos cerrar los canales de entrada y salida que se hayan abierto durante la ejecución de la aplicación. En la parte del cliente:

try 
   {
     salida.close();
     entrada.close();
     miCliente.close();
   }
catch (IOException e)
   {
     System.out.println(e);
   }

Y en la parte del servidor:

try 
   {
     salida.close();
     entrada.close();
     socketServicio.close();
     miServicio.close();
   }
catch (IOException e)
   {
     System.out.println(e);
   }

Programación en Android de aplicación básica cliente / servidor

Conexión entre cliente y servidor

A continuación se detalla parte del código para lograr una conexión exitosa entre dos terminales smartphone. Uno de ellos será el cliente y el otro el servidor.

Client.java

Dentro de la programación del cliente podemos destacar el siguiente código:

//cuando el usuario haga clic en conectar, se lanza un asynctask que intenta conectar con el servidor
    View.OnClickListener buttonConnectOnClickListener = new View.OnClickListener()
    {

                @Override
                public void onClick(View arg0)
                {
                    MyClientTask myClientTask = new MyClientTask(editTextAddress.getText().toString(), Integer.parseInt(editTextPort.getText().toString()));
                    myClientTask.execute();
                }
    };

    public class MyClientTask extends AsyncTask<Void, Void, Void>
    {

        String dstAddress; //IP de servidor
        int dstPort; //Puerto del servidor
        String response = "";

        MyClientTask(String addr, int port)
        {
            dstAddress = addr;
            dstPort = port;
        }

        @Override
        protected Void doInBackground(Void... arg0)
        {

            Socket socket = null;

            try
            {
                //Creamos un socket indicando la ip y el puerto de destino
                socket = new Socket(dstAddress, dstPort);

                //Creamos un OutputStream de tipo Array de Bytes
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024);
                byte[] buffer = new byte[1024];

                int bytesRead;
                //Abrimos un inputStream para el socket
                InputStream inputStream = socket.getInputStream();

                //Vamos leyendo del inputStream y nos vamos guardando lo que leemos en el outputstream
                while ((bytesRead = inputStream.read(buffer)) != -1)
                {
                    byteArrayOutputStream.write(buffer, 0, bytesRead);
                    response += byteArrayOutputStream.toString("UTF-8");
                }

            }
            catch (UnknownHostException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
                response = "Excepción desconocida del servidor: " + e.toString();
            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
                response = "IOException: " + e.toString();
            }
            finally
            {
                //Al terminar, cerramos el socket si no es null
                if(socket != null)
                {
                    try
                    {
                        socket.close();
                    }
                    catch (IOException e)
                    {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result)
        {
            //Actualizamos TextView indicando si se ha podido o no conectar con el servidor
            textResponse.setText(response);
            super.onPostExecute(result);
        }

    }

Server.java

Dentro de Server.java, destacamos lo siguiente:

//Thread para gestionar el servidor y su socket
    private class SocketServerThread extends Thread
    {
        //establecemos el puerto del servidor
        static final int SocketServerPORT = 8888;
        int count = 0;

        @Override
        public void run()
        {
            try
            {
                //Abrimos el puerto
                serverSocket = new ServerSocket(SocketServerPORT);
                //Actualizamos el TextView que indica el puerto
                Server.this.runOnUiThread(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        info.setText("Puerto: " + serverSocket.getLocalPort());
                    }
                });

                //Bucle para dejar el servidor a la escucha de clientes
                while (true)
                {
                    //Creamos un socket que esta a la espera de una conexion de cliente
                    Socket socket = serverSocket.accept();
                    //Cuando recibimos un cliente, actualizamos el textview de mensajes
                    count++;
                    message += "#" + count + " desde " + socket.getInetAddress() + ":" + socket.getPort() + "\n";
                    Server.this.runOnUiThread(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            msg.setText(message);
                        }
                    });

                    //Se lanza un thread para indicar al cliente que se ha conectado al servidor
                    SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(socket, count);
                    socketServerReplyThread.run();
                }
            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    //Thread para indicar al cliente que se haya conectado que lo ha conseguido
    private class SocketServerReplyThread extends Thread
    {

        private Socket hostThreadSocket;
        int cnt;

        SocketServerReplyThread(Socket socket, int c)
        {
            hostThreadSocket = socket;
            cnt = c;
        }

        @Override
        public void run()
        {
            //Creamos un objeto OutputStream para poder comunicarnos con el cliente
            OutputStream outputStream;
            String msgReply = "Hola desde Android, tú eres #" + cnt;

            try
            {
                //Inicializamos el outputstream con el host del servidor
                outputStream = hostThreadSocket.getOutputStream();
                //Creamos PrintStream para poder añadir mensajes al outputStream de forma más asequible
                PrintStream printStream = new PrintStream(outputStream);
                printStream.print(msgReply);
                printStream.close();

                message += "Mensaje para el cliente: " + msgReply + "\n";

                Server.this.runOnUiThread(new Runnable()
                {

                    @Override
                    public void run()
                    {
                        msg.setText(message);
                    }
                });

            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
                message += "¡Algo fue mal! " + e.toString() + "\n";
            }

            //Actualizamos TextView de historial de conexiones de clientes al servidor
            Server.this.runOnUiThread(new Runnable()
            {
                @Override
                public void run()
                {
                    msg.setText(message);
                }
            });
        }

    }

Envío de texto y ficheros mediante sockets

Una de las principales utilidades cuando hemos abierto un punto de comunicación entre dos dispositivos es el envío de texto y de ficheros.

En Android, los ficheros estarán almacenados en la tarjeta de memoria asociada al dispositivo, por lo que tendremos que tener acceso de lectura y escritura a este hardware. En AndroidManifest.xml no debemos olvidar añadir los siguientes dos permisos a los ya existentes para el envío de texto:

android.permission.WRITE_EXTERNAL_STORAGE
android.permission.READ_EXTERNAL_STORAGE

A continuación se muestra un ejemplo de código para enviar un fichero por parte de un cliente a un servidor. En este caso se hace uso de un nuevo socket para el envío del fichero. Una vez enviado el fichero este socket (que también tendrá un número de puerto diferenciado) se cerrará.

El código ha sido implementado por los alumnos de la segunda promoción del Máster Jesús Escribano García y Ramón Torregrosa López.

private class ClienteFichero extends Thread {
        String dstAddress;
        int dstPort;
        File file;
        Socket socket = null;
        ClienteFichero(String address, int port) {
            dstAddress = address;
            dstPort = port;
        }

        @Override
        public void run() {

            try {
                socket = new Socket(dstAddress, dstPort);

                file = new File(Environment.getExternalStorageDirectory(),  fichName);
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                byte[] bytes;
                FileOutputStream fos = null;
                try {
                    bytes = (byte[]) ois.readObject();
                    fos = new FileOutputStream(file);
                    fos.write(bytes);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } finally {
                    if (fos != null) {
                        fos.close();
                    }

                }

                socket.close();

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        String path = Environment.getExternalStorageDirectory() + "/"+fichName; //nombre del fichero de salida
                        File imgFile = new File(path);
                        if (imgFile.exists()) {
                            linearLayout.addView(ViewHelper.newImageView(ClienteActivity.this, Uri.fromFile(file)));
                            Toast.makeText(ClienteActivity.this, "Fichero enviado", Toast.LENGTH_LONG).show();
                        }
                        else{
                            Toast.makeText(ClienteActivity.this, "No se encuentra el fichero "+path, Toast.LENGTH_LONG).show();
                        }


                    }
                });


            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }


    }

Para el servidor, el código que espera y recibe un fichero tipo imagen sería el siguiente:

private class ServerImagen extends Thread {

        @Override
        public void run() {
            Socket socket = null;

            try {
                serverSocket = new ServerSocket(SocketServerPORTImg);

                while (true) {//INFINITOS CLIENTES
                    socket = serverSocket.accept();
                    FicheroImagen mFicheroImagen = new FicheroImagen(socket);
                    mFicheroImagen.start();
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }

    }


    private class FicheroImagen extends Thread {
        Socket socket;
        String imagenRecibida="recibido.png";
        FicheroImagen(Socket socket){
            this.socket= socket;
        }

        @Override
        public void run() {
            File file = new File(Environment.getExternalStorageDirectory(), fichName);

            byte[] bytes = new byte[(int) file.length()];
            BufferedInputStream bis;
            try {
                bis = new BufferedInputStream(new FileInputStream(file));
                bis.read(bytes, 0, bytes.length);

                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                oos.writeObject(bytes);
                oos.flush();

                socket.close();

                ServidorActiviy.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        String path = Environment.getExternalStorageDirectory() + "/"+fichName;
                        File imgFile = new File(path);
                        if (imgFile.exists()) {
                            linearLayout.addView(ViewHelper.newImageView(ServidorActiviy.this, Uri.fromFile(imgFile)));
                            Toast.makeText(ServidorActiviy.this, "Fichero recibido", Toast.LENGTH_LONG).show();
                        }
                        else{
                            Toast.makeText(ServidorActiviy.this, "Error al recibir fichero ", Toast.LENGTH_LONG).show();
                        }

                    }});

            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

        }
    }

Programación para Voz IP

La programación de Voz IP posee mayor dificultad que el envío de texto o ficheros entre dispositivos. Esta dificultad no se debe a tener que manejar tanto sockets TCP como UDP en todo momento, sino más bien al proceso de codificación de la voz para convertila en una unidad de datos válida para ser eviada entre un equipo y otro.

Para la codificación de la voz existen varias alternativas. Entre todas ellas, destacan los codificadores híbridos o de análisis por síntexis, donde el emisor lleva a cabo un análisis que obtiene los parámetros de la señal para luego sintetizarla y conseguir el mayor parecido a la original.

El objetivo de estos codificadores es obtener voz de alta calidad a tasas de bit bajas (inferiores a 8kHz). Su funcionamiento se basa en analizar un conjunto de muestras como si se tratase de una sola para obtener los parámetros de la señal. Al decodificar la trama, se sintetizan los parámetros para conseguir que se parezca al original Entre estas técnicas podemos destacar CELP y RPE-LTP. Al comparar ambos algortimos llegamos a la conclusión que CELP (codificación predictiva lineal excitada por código) reduce el ancho de banda necesario para el envío de los streams de voz, per a cambio exige cálculos costosos para el procesamiento de la voz, algo inviable en lenguajes de alto nivel como Java. Por esta razón se elige RPE-LTP.

Codificación RPE-LTP (Regular Pulse Excitation - Long Term Prediction)

Es la codificación empleada en GSM. Su ventaja frente a CELP es que reduce la complejidad de las operaciones y, por tanto, la computación necesaria para la codificación. Por contra, requiere un ancho de banda mayor que el anterior.

Tendremos dos formas diferentes para procesar la información. Por un lado y de forma resumida, cuando el interlocutor habla por el dispositivo, habrá que recoger la voz del micrófono (AudioRecord), codificarla para convertirla en el stream adecuado y enviarla mediante UPD (datagrampacket, método send()). En el otro dispositivo se realizará la acción inversa, es decir, primero se recibirá el datagrama con el stream de datos codificados (datagrampacket, metodo receive()), decodificarlo para convertirlo en voz sintética y reproducirlo en el altavoz (AudioTrack).

Reprentación de la codificación RPE-LTP.

Representación de la decodificación RPE-LTP.

Redes de nueva generación, nuevos servicios

Las redes de nueva generaciónn (NGN en inglés) es un amplio término que se refiere a la evolución de la actual infraestructura de redes de telecomunicación y acceso telefónico con el objetivo de lograr la convergencia tecnológica de los nuevos servicios multimedia (voz, datos, video...) en los próximos años. La idea principal que se esconde en este tipo de redes es el transporte de paquetes encapsulados de información a través de Internet. Estas nuevas redes están siendo creadas a partir del protocolo Internet Protocol (IP), siendo el término "all-IP" comúnmente utilizado para describir dicha evolución.

Las redes de nueva generación implican cambios fundamentales en la arquitectura de red tradicional con la migración del servicio de voz desde la tradicional arquitectura conmutada (PSTN) a la nueva VoIP además de la sustitución de las redes tradicionales (legacy-service) como la X.25 o la Frame Relay. Esto supone incluso una migración para el usuario tradicional hacia un nuevo servicio como es el VoIP que significa el tratamiento de la voz como datos, aunque con requerimientos de calidad de servicio superiores (QoS).

Arquitectura de las redes de nueva generación

Las redes NGN dividen la red en cuatro planos o capas. En la siguiente figura se aprecian estos cuatro planos.

Representación de la arquitectura de NGN.

La primera de ellas es la capa de acceso. Se preveen tres diferentes tipos de acceso red:

  • Acceso móvil

  • Banda ancha

  • Banda estrecha

Las velocidades de acceso a la red se están incrementado en los últimos años gracias a las redes híbridas de fibra coaxial o las redes de fibra hasta el hogar. Los accesos inferiores a 30 Mbps no tendrían consideración de NGN, incluso hay tendencia actual a elevar a 100Mbps la velocidad mínima requerida para que una red de acceso sea catalogada como una red de nueva generación.

En el plano de transmporte, las redes de nueva generación están basadas en tecnologías de Internet incluyendo el protocolo IP y el MPLS. MPLS (Multiprotocol Label Switching) es un nuevo protocolo que opera entre la capa de enlace de datos y la capa de red del modelo OSI. Fue diseñado para unificar el servicio de transporte de datos para las redes basadas en circuitos y las basadas en paquetes. Está reemplanzado actualmente a la tecnología Frame Relay y ATM para transportar datos a alta velocidad en las redes actuales.

Representación de las bondades de MPLS para la transmisión de datos.

En el plano de control tenemos el subsistema multimedia IP (IMS). IMS es una manera completamente nueva de distribuir multimedia (voz, video, datos, etc.) independiente del dispositivo (teléfono, móvil, o fijo, IPTV, notebook, etc.) o de medio de acceso (3G / EDGE / GPRS, Wi-Fi, banda ancha, línea telefónica, etc.). En este plano también destaca el Softswitch que provee control de llamadas y servicios inteligentes para todo tipo de tráfico multimedia generado.

Equipo Softswitch T7000 de Taqua Systems.

En el nivel de aplicación, es el protocolo SIP (Session Initiation Protocol) el que destaca como la nueva estandarización para servicios audiovisuales, sustituyendo a H.323. El Protocolo H.323 de la ITU-T (International Telecommunication Union), ha sido hasta día de hoy el protocolo común en sesiones de comunicación audiovisual sobre paquetes de red, especialmente Voz IP y videoconferencia. Es importante indicar que H.323 no garantiza una calidad de servicio, y en el transporte de datos puede, o no, ser fiable; en el caso de voz o vídeo, nunca es fiable. A esto se añade su pésima gestión de NAT y firewals.

SIP, o Session Initiation Protocol es cada vez más usado en los sistemas de Telefonía IP. Está basado en HTTP (HyperText Transfer Protocol) adoptando las características más importantes de este estándar como son la sencillez de su sintaxis y una estructura cliente/servidor basada en un modelo petición/respuesta. Otra de las ventajas de SIP es su sistema de direccionamiento. Las direcciones SIP tienen una estructura parecida a la un correo electrónico dotando a sus clientes de una alta movilidad facilitando una posible integración en comunicaciones móviles.

Representación de la encapsulación del protocolo SIP en TCP o UPD.

Android proporciona una API que soporta el protocolo de inicio de sesión (SIP). Esto permite añadir funciones de telefonía de Internet basadas en SIP a las aplicaciones. Android incluye una pila de protocolos SIP completa y servicios integrados de gestión de llamadas que permiten a las aplicaciones configurar fácilmente las llamadas de voz entrantes y salientes, sin tener que administrar sesiones, la comunicación a nivel de transporte, grabar audio o reproducir directamente.

Tipos de aplicaciones que puedan utilizar la API de SIP:

  • Videoconferencia.

  • La mensajería instantánea.

Estos son los requisitos para el desarrollo de una aplicación SIP:

  • Dispositivo móvil que ejecute Android 2.3 o superior.

  • SIP se ejecuta a través de una conexión de datos inalámbrica, por lo que el dispositivo debe tener una conexión de datos (con un servicio móvil de datos o Wi-Fi).

  • Cada participante en la sesión de comunicación de la solicitud tiene que tener una cuenta SIP Existen varios proveedores u operadores de red que ofrecen cuentas SIP. Por ejemplo, en la siguiente Web se puede obtener una cuenta de SIP: https://mdns.sipthor.net/register_sip_account.phtml

A la hora de programar aplicaciones con SIP, hay que intriducir el permiso correspondiente en el AndroidManifest.xml:

android.permission.USE_SIP
android.permission.INTERNET

Para asegurar que la aplicación sólo se puede instalar en dispositivos que son capaces de soportar SIP, se añadirá el siguiente código al Manifest:

     <Android usa-sdk: minSdkVersion = "9" />.

Esto indica que su aplicación requiere Android 2.3 o superior.

Para utilizar la API SIP, la aplicación a desarrollar debe crear un objeto de la clase SipManager. El SipManager se encargará de lo siguiente:

  • Inicio de las sesiones SIP.

  • Iniciar y recibir llamadas.

  • Registrarse con un proveedor de SIP.

  • Verificar la conectividad de la sesión.

A continuación se muestra la creación de un objeto de esta clase.

public SipManager mSipManager = null;
...
if(mSipManager == null) {
    mSipManager = SipManager.newInstance(this);
}

A continuación se muestra el código válido para iniciar el establecimiento de una llamada de audio mediante protocolo SIP:

SipAudioCall.Listener listener = new SipAudioCall.Listener() {

   @Override
   public void onCallEstablished(SipAudioCall call) {
      call.startAudio();
      call.setSpeakerMode(true);
      call.toggleMute();
         ...
   }

   @Override
   public void onCallEnded(SipAudioCall call) {
      // Do something.
   }
}

Proyecto a desarrollar por el alumno

El alumno tendrá que desarrollar una aplicación cliente servidor para comunicar dos dispositivos smarphone para el envío de datos o voz IP.

Debido a la características del proyecto a desarrollar (requiere dos dispositivos móviles) el proyecto se realizará preferentemente en pareja.

Se podrá elegir entre:

  • Desarollar una aplicación cliente / servidor que envía y recibe datos (incluyendo mensajes e imágenes).

  • Desarrollar una aplicación para comunicación con Voz IP.

Requerimientos mínimos para las aplicaciones

App de envío de mensajes e imágenes:

  • El servidor únicamente aceptará a un cliente.

  • Debes ser capaz de poder comunicar un dispositivo con otro y enviar secuencias de texto y al menos un archivo tipo imagen almacenado en tu dispositivo.

App de Voz IP

  • El servidor únicamente aceptará a un cliente.

  • La conexión entre los dispositivos se debe realizar con TCP, mientras que el envío de audio se realizará con UDP.

  • Para la codificación de la voz se recomienda emplear el codifcador RPE-LTP.

Enriquecimientos

Se recomienda a los alumnos internar enriquecer la aplicación para que ésta mejore en funcionalidad y manejabilidad para el usuario. Asimismo, se valorará el tratamiento adecuado de las situaciones de error en red más comunes.

Ejemplos de enriquecimientos:

  • Poder aceptar comunicaciones de más de un cliente y ser correctamente atendidas.

  • Posibilidad de rechazar conexiones de determinados clientes.

  • Los dos dispositivos pueden actuar tanto de cliente como de servidor. El dispositivo que pide conexión con otro sería el cliente y el otro actuaría de servidor.

  • Descubrimiento de los equipos de forma automática (propagación de IP, nombre, etc.)

  • Confirmación de recepción de datos.

  • Mostrar la configuración IP del dispositivo.

El alumno tendrá libertad para proponer otros enriquecimientos para mejorar su aplicación.

Last updated