Protocolos Otros de los aspectos más importantes de los Sistemas Distribuidos son los protocolos de comunicación que se detallan a continuación. Definición: Un protocolo de comunicación es un conjunto de reglas y formatos que se utilizan para la comunicación entre procesos que realizan una determinada tarea. Se requieren dos tipos de especificaciones: o Especificación de la secuencia de mensajes que se han de intercambiar. o Especificación del formato de los datos en los mensajes. La finalidad de los protocolos es permitir que componentes heterogéneos de sistemas distribuidos puedan desarrollarse independientemente, y por medio de las capas que componen el protocolo, exista una comunicación transparente entre ambos componentes. Es conveniente mencionar que estas capas del protocolo deben presentarse tanto en el receptor como en el emisor. Dentro de los protocolos más utilizados en los sistemas distribuidos se encuentran: o IP: Protocolo de Internet.- Protocolo de la capa de Red, que define la unidad básica de transferencia de datos y se encarga del direccionamiento de la información, para que llegue a su destino en la red. o TCP: Protocolo de Control de Transmisión.- Protocolo de la capa de Transporte, que divide y ordena la información a transportar en paquetes de menor tamaño para su envío y recepción. o HTTP: Protocolo de Transferencia de Hipertexto.- Protocolo de la capa de aplicación, que permite el servicio de transferencia de páginas de hipertexto entre el cliente Web y los servidores. o SMTP: Protocolo de Transferencia de Correo Simple.- Protocolo de la capa de aplicación, que procesa el envío de correo electrónico por la red. o POP3: Protocolo de Oficina de Correo.- Protocolo de la capa de aplicación, que gestiona los correos en Internet, es decir, permite a una estación de trabajo recuperar los correos que están almacenados en el servidor. Ventajas de un Sistema Distribuidos Las ventajas de los sistemas distribuidos con respecto de los centralizados son: - Economía. Los microprocesadores ofrecen mejor proporción precio/rendimiento que los mainframes, pues se pueden reunir un gran número de CPU’s baratos en un mismo sistema y dado el avance tecnológico de estos, se puede dar un mejor rendimiento que un sólo mainframe. - Velocidad. Un sistema distribuido puede tener mayor poder de cómputo que un mainframe. - Distribución inherente. Algunas aplicaciones utilizan computadoras que están separadas a cierta distancia. Por ejemplo, trabajo cooperativo apoyado por computadora, juegos cooperativos apoyados por computadora. - Confiabilidad. Si una computadora se descompone, el sistema puede sobrevivir como un todo. Crecimiento por incrementos. Se puede añadir poder de cómputo en pequeños incrementos. Ventajas de los sistemas distribuidos con respecto a las computadoras personales aisladas - Datos compartidos. Permiten que varios usuarios tengan acceso a una base de datos común. - Dispositivos compartidos. Permiten que varios usuarios compartan periféricos caros como scanners o impresoras a color. - Comunicación. Facilita la comunicación de persona a persona; por ejemplo, mediante correo electrónico, FAX, chats, foros, etc. - Flexibilidad. Difunde la carga de trabajo entre las computadoras disponibles en la forma más eficaz en cuanto a costos. Desventajas de un Sistema Distribuido - Software. Existe poco software para los sistemas distribuidos en la actualidad. Redes. La red se puede saturar o causar otros problemas. Seguridad. Un acceso sencillo también se aplica a datos secretos. La principal dificultad en el desarrollo de un sistema distribuido es el software, dado que el diseño, la implantación presenta numerosas interrogantes: Tipos de sistemas operativos y Lenguajes de Programación adecuados para estos sistemas Niveles de Transparencia Responsabilidades del sistema y de los usuarios Otro problema potencial es la configuración de las redes dado que es necesario considerar: perdidas de mensajes saturación en el trafico extensión de la red configuración de la topología Aplicaciones Distribuidas. - Una red de computadoras con una pila de procesadores Una aerolínea Fábrica de robots Un banco con sucursales Internet Multimedia y conferencias Capítulo 4. Comunicación en red La diferencia más importante entre un sistema distribuido y un sistema con un procesador es la comunicación entre procesos. En un sistema con un procesador se supone la existencia de una memoria compartida, lo cual no existe en un sistema distribuido. La comunicación entre procesos debe respetar reglas llamadas protocolos. 4.1 Protocolos con capas Para que dos procesos logren la comunicación se deben poner de acuerdo en varios aspectos como, cuántos voltios hay que utilizar para señalar un bit 0 y un bit 1; cómo detectar el fin de un mensaje, etc. Para facilitar este trabajo la organización internacional de estándares (internacional standards organization- ISO) ha desarrollado un modelo de referencia llamado el modelo de referencia para interconexión de sistemas abiertos, lo cual se abrevia como ISO OSI o el modelo OSI. El modelo OSI está diseñado para permitir la comunicación de los sistemas abiertos. Un sistema abierto es aquel preparado para comunicarse con cualquier otro sistema abierto mediante estándares que gobiernan el formato, contenido y significado de los mensajes enviados y recibidos. Un protocolo es un acuerdo entre las partes acerca de cómo debe desarrollarse la comunicación. El modelo OSI maneja dos tipos generales de protocolos (conexiones). 1. Protocolos orientados a la conexión. Antes de intercambiar datos, el emisor y el receptor deben establecer en forma explícita una conexión y tal vez el protocolo. 2. Protocolos con conexión. No es necesaria una negociación previa. 4.1.1 Modelo OSI En el modelo OSI, la comunicación se divide en 7 niveles o capas. Cada capa se maneja de forma independiente y se encarga de un aspecto específico. Cada capa proporciona una interfaz con la capa por encima de ella. La interfaz es un conjunto de operaciones que juntas definen el servicio que la capa está preparada para ofrecer a sus usuarios. La ventaja de un protocolo por capaz es su independencia, ya que una capa puede modificarse o mejorarse sin afectar a las demás. En el modelo OSI cuando se va a enviar un mensaje, este pasa por todas las capas comenzando en la de Aplicación. Cada capa le coloca un encabezado al frente o al final, al llegar el mensaje al otro lado, cada capa lo va desmenuzando, quitándole el encabezado que le corresponde; la capa que recibe el mensaje es la capa física. Capas Fisica, Enlace de Datos, Red, Transporte, Sesión, Presentación, Aplicación Capa física Su propósito es transportar el flujo de bits de una computadora a otra. El protocolo de la capa física se encarga de la estandarización de las interfaces eléctricas, mecánicas y de señalización. Maneja elementos como la intensidad de la señal de red, cables, enchufes, voltajes, distancias entre cables. Capa de enlace de datos La tarea principal de esta capa es agrupar a los bits en unidades llamadas marcos o tramas, y detectar y corregir errores. Uno de los mecanismos en la detección de errores es asignar números secuenciales a las tramas y a cada trama colocarle una suma de verificación, sino esta correcta la suma de verificación a la hora de recibir el marco, entonces el receptor le pide al transmisor que vuelva a transmitir el marco x. Capa de red La tarea principal de la capa de red es elegir la mejor ruta (a esta actividad se le llama ruteo), la ruta más corta no siempre es la mejor, lo importante es la cantidad de retraso, y esto complica los algoritmos de ruteo. La capa de red maneja dos protocolos: X.25 (orientado a la conexión) y el IP (sin conexión). Capa de transporte La tarea de la capa de transporte es proporcionar conexiones confiables y económicas. Las conexiones confiables (orientadas a la conexión) se pueden construir por arriba de X.25 o IP. En X.25 los paquetes llegan en orden, en IP no, entonces la capa de transporte es la encargada de ponerlos en orden. El protocolo de transporte oficial ISO tiene cinco variantes, TP0, TP1, … , TP4. Los protocolos más utilizados en esta capa son: TCP (transmisión control protocolprotocolo para el control de transmisiones), orientado a la conexión y UDP (universal datagrama protocol- protocolo datagrama universal) que es un protocolo sin conexión. Capa de sesión Esta es en esencia una versión mejorada de la capa de transporte. Proporciona el control del diálogo, facilidades en la sincronización, la posibilidad de establecer conexiones llamadas sesiones y la posibilidad de transferir datos sobre las sesiones en forma ordenada. En la práctica rara vez las aplicaciones soportan esta capa. Capa de presentación Esta capa trata los problemas relacionados con la representación y el significado de los datos. Capa de aplicación Es una colección de varios protocolos para actividades comunes, como el correo electrónico, la transferencia de archivos y la conexión entre terminales remotas a las computadoras en una red. Esta capa contiene los programas de usuario. Protocolos utilizados: X.400 para correo electrónico, X.500 para el servidor de directorios. 4.2 El modelo cliente servidor Un sistema distribuido basado en LAN no utiliza de modo alguno los protocolos con capas; y si lo hacen, solo utilizan un subconjunto de toda una pila de protocolos. El modelo cliente-servidor se presenta con la idea de estructurar el sistema operativo como un grupo de procesos en cooperación, llamados servidores, que ofrezcan servicios a los usuarios, llamados clientes. Las computadoras de los clientes y servidores ejecutan el mismo micronúcleo (generalmente) y ambos se ejecutan como procesos de usuario. Este modelo se basa usualmente en un protocolo solicitud/respuesta sencillo y sin conexión. El cliente es quien solicita al servidor algún servicio, y el servidor es quien atiende la solicitud y regresa los datos solicitados o un código de error si algo falló. Ventajas Sencillez. El cliente envía un mensaje y obtiene respuesta. Eficiencia. La pila del protocolo es más corta y por tanto más eficiente. No es necesario implantar capas como la de sesión o red y transporte, ya que este trabajo lo hace el hardware. Se reducen los servicios de comunicación. Solo necesitamos las llamadas send (enviar) y receive (recibir). 4.3 Direccionamiento Para que un proceso envíe un mensaje a otro proceso que esta en otra computadora, es necesario que el primer proceso sepa donde esta el otro proceso (su dirección). El primer acercamiento para resolver el problema de cómo identificar un proceso, es asociarle a cada computadora una dirección, aunque esto implicaría que cada computadora sólo puede ejecutar un proceso. Para resolver esta limitante, se podrían dar identificadores a los procesos y no a las computadoras, pero ahora surge el problema de cómo identificar dos procesos. Un esquema común consiste en utilizar nombre con dos partes, una para especificar la computadora y la otra para especificar el proceso. Sin embargo este tipo de direccionamiento no es bueno, ya que cada usuario debe conocer la dirección del servidor y esto ya no es transparente. Si el servidor falla y su dirección cambia, entonces se deben recompilar los programas que utilicen ese servidor. Una alternativa es que cada proceso elija su identificador de un gran espacio de direcciones dispersas, como el espacio de enteros de 64 bits. La posibilidad de que dos procesos elijan el mismo número es muy pequeña y el método puede utilizarse en sistemas extensos. ¿Cómo sabe el núcleo emisor a qué computadora enviar el mensaje? En una LAN que soporte transmisiones, el emisor puede transmitir un paquete especial de localización con la dirección del proceso destino. Todas las computadoras de la red lo recibirán y los núcleos verificarán si la dirección es la suya, en este caso, regresa un mensaje “aquí estoy” con su dirección en la red. El núcleo emisor utiliza entonces esa dirección y la captura, para evitar el envío de otra transmisión la próxima vez que necesite al servidor. Este último esquema tiene un problema: la carga adicional en el sistema. Esto se evita utilizando una computadora adicional para la asociación a alto nivel de los nombres de los servicios con las direcciones de las computadoras. Así se utilizan nombres de procesos y no números. Cada vez que se ejecute un cliente, primero va al servidor de nombres, y pide el número de la computadora donde se localiza algún servidor. Así las direcciones se ocultan. Este método tiene la desventaja de utilizar un componente centralizado. Primitivas con bloqueo vs. Sin bloqueo La forma de comunicación entre los procesos es el intercambio de mensajes, para lo cual son necesarias ciertas primitivas como send para enviar y receive para recibir. Existen dos tipos de primitivas: con bloqueo o sin bloqueo. Primitivas con bloqueo (síncronas). Al invocarse un send o un receive, el control del proceso no regresa hasta que se haya enviado el mensaje o hasta que se haya recibido un mensaje en el caso de receive, mientras tanto el proceso queda bloqueado. En este caso la desventaja esta en que el CPU esta inactivo durante la transmisión de los mensajes. Primitivas sin bloqueo (asíncronas). Tanto send como receive no tiene bloqueo. En el caso de send, esta primitiva coloca el mensaje en el buffer de mensaje y regresa el control inmediatamente sin que el mensaje haya sido enviado. El problema esta en saber cuando se puede volver a utilizar el buffer de mensajes. Existen dos soluciones a este problema. La primera es que el núcleo copie el mensaje a un buffer interno del núcleo y luego permita el proceso continuar. Así el proceso permanecerá bloqueado mientras se lleva a cabo la copia. La desventaja es que cada mensaje debe ser copiado del espacio del usuario al espacio del núcleo. Esto puede reducir el desempeño del sistema. La segunda solución es interrumpir al emisor cuando el mensaje ha sido enviado y hacerle saber que ya puede usar nuevamente el buffer de mensajes. Este método es eficiente, pero es muy difícil escribir los programas en forma correcta y es casi imposible depurarlos cuando están incorrectos. En el caso de un receive sin bloqueo, éste sólo avisa al núcleo de la localización del buffer y regresa el control inmediatamente. Para hacer que éste espere se utilizaría un wait. En las primitivas síncronas puede suceder que un send o receive se queden esperando por siempre, así que para evitarlo se utilizan los llamados tiempos de espera, si en este tiempo nada llega, la llamada send termina con un estado de error. Primitivas almacenadas en buffer vs. No almacenadas Las primitivas anteriores son primitivas no almacenadas, esto es, los mensajes son enviados a una dirección específica. El proceso receptor se bloquea mientras copia el mensaje a un buffer. Esto funciona bien mientras el receptor este siempre listo para escuchar un mensaje. Pero cuando primero se ejecuta send y luego receive hay problemas, puesto que la llamada a receive es el mecanismo que indica al núcleo del servidor la dirección que utiliza el servidor y la posición donde colocar el mensaje que está por llegar; entonces si primero llega un mensaje, el servidor no sabrá de quién es y dónde ponerlo. Soluciones: 1. Confiar que se va a ejecutar siempre un receive antes de un send. Mala idea. 2. Hacer que el transmisor realice varios intentos para enviar su mensaje, pero si el servidor esta muy ocupado, otros esperarán un tiempo y luego se rendirán. 3. Que el núcleo receptor mantenga pendientes los mensajes. Si no ocurre un receive en un determinado tiempo, el mensaje es descartado. 4. Utilizar un buzón donde almacenar los mensajes. Un proceso que va a recibir mensajes pide al núcleo que le sea creado un buzón, en donde le depositarán los mensajes y él los irá leyendo. A esta técnica se le conoce como primitiva con almacenamiento en buffers. El problema con los buzones es que son finitos y se pueden llenar. Soluciones: 1. Mantener pendientes los mensajes 2. Descartarlos 3. El cliente debe verificar primero si hay espacio, si existe entonces enviar mensaje sino bloquearse. Primitivas confiables vs. No confiables Cuando el cliente envía un mensaje, a pesar de utilizar primitivas con bloqueo, no existe garantía de que el mensaje haya sido entregado. El mensaje pudo haberse perdido. Existen tres enfoques de este problema: 1. Volver a definir la semántica de send para hacerla no confiable, es decir, la primitiva no se encarga de verificar que su mensaje haya sido entregado y toda la responsabilidad de comunicación confiable se deja en manos del usuario. 2. El núcleo de la computadora receptora envía un reconocimiento al núcleo de la computadora emisora. El reconocimiento va de núcleo a núcleo, así ni el cliente ni el servidor se dan cuenta de esto. El núcleo entonces liberará al proceso cuando reciba el reconocimiento. 3. Aprovechar el hecho de que en la comunicación cliente-servidor se realiza de la siguiente manera: el cliente le envía un mensaje de solicitud al servidor y el servidor le da una respuesta al cliente. Entonces no hay mensaje de reconocimiento por parte del núcleo del servidor, sino que la respuesta del servidor funciona como reconocimiento. Así, el emisor (cliente) permanece bloqueado hasta que regresa la respuesta. Si se tarda demasiado, el núcleo emisor puede enviar la solicitud nuevamente. En este método no existe reconocimiento para la respuesta, esta omisión puede ocasionar problemas si el servicio solicitado por el cliente requiere cómputo extenso, porque el servidor no se enteraría si el cliente recibió o no la respuesta. Para solucionar esto se envía reconocimiento de la respuesta en base a un cronómetro, si la respuesta llegó rápido, no hay reconocimiento, si tardó demasiado si hay reconocimiento. 4.4 Sockets Ambas formas de comunicación (UDP y TCP) utilizan la abstracción de sockets, que proporciona los puntos extremos de la comunicación entre procesos. Los sockets (conectores) están presentes en la mayoría de las versiones de UNIX, incluido Linux y también Windows NT y Macintosh OS. Los sockets permiten conectar dos programas en red para que se puedan intercambiar datos. Los sockets están basados en una arquitectura cliente/servidor. En esta arquitectura uno de los programas debe estar siempre arrancado y pendiente de que alguien establezca conexión con él. Este programa se denomina servidor. El otro programa lo arranca el usuario cuando lo necesita y es el programa que da el primer paso en el establecimiento de la comunicación. Este programa se llama cliente. El servidor, está continuamente a la escucha y a la espera de que alguien se quiera conectar a él. Si hacemos una comparación con un teléfono, un servidor es una empresa 24 horas, 365 días al año, pendiente de recibir llamadas de sus clientes. El cliente, en un momento dado decide conectarse a un servidor y hacerle alguna petición. Cuando el cliente no necesita al servidor, cierra la conexión. En la comparativa del teléfono, el cliente es el que llama por teléfono a la empresa cuando necesita algo de ella. Por ejemplo, un servidor de páginas web está siempre en marcha y a la escucha. El navegador es el cliente. Cuando arrancamos el navegador y ponemos la dirección del servidor web, el navegador establece la comunicación y le pide al servidor la página web que queremos ver. El servidor la envía y el navegador la muestra. La comunicación entre procesos consiste en la transmisión de un mensaje entre un conector de un proceso y un conector de otro proceso. Para los procesos receptores de mensajes, su conector debe estar asociado a un puerto local y a una de las direcciones Internet de la computadora donde se ejecuta. Los mensajes enviados a una dirección de Internet y a un número de puerto concretos, sólo pueden ser recibidos por el proceso cuyo conector esté asociado con esa dirección y con ese puerto. Los procesos pueden utilizar un mismo conector tanto para enviar como para recibir mensajes. Cada computadora permite un gran número (216) de puertos posibles, que pueden ser usados por los procesos locales para recibir mensajes. Cada proceso puede utilizar varios puertos para recibir mensajes, pero un proceso no puede compartir puertos con otros procesos de la misma computadora. Cualquier cantidad de procesos puede enviar mensajes a un mismo puerto. Cada conector se asocia con un protocolo concreto, que puede ser UDP o TCP. 4.4.1 Sockets flujos (TCP) Son un servicio orientado a la conexión, donde los datos se transfieren sin encuadrarlos en registros o bloques. Si se rompe la conexión entre los procesos, éstos serán informados de tal suceso para que tomen las medidas oportunas. El protocolo de comunicaciones con flujos es un protocolo orientado a la conexión, ya que para establecer una comunicación utilizando el protocolo TCP, hay que establecer en primer lugar una conexión entre un par de sockets. Mientras uno de los sockets atiende peticiones de conexión (servidor), el otro solicita una conexión (cliente). Una vez que los dos sockets estén conectados, se pueden utilizar para transmitir datos en ambas direcciones. El protocolo TCP (Transmission Control Protocol) funciona en el nivel de trasporte, basándose en el protocolo de red IP (Internet Protocol). IP proporciona comunicaciones no fiables y no basadas en conexión, muy dependientes de saturación en la red, caídas de notos, etc. Por el contrario, TCP está orientado a conexión y proporciona comunicaciones fiables basadas en mecanismos de red que gestionan el control de flujo de paquetes y de congestión en los nodos. En Java, las comunicaciones TCP se realizan utilizando la clásica abstracción de socket. Los sockets nos permiten establecer y programar comunicaciones sin tener que conocer los niveles inferiores sobre los que se asientan. Para identificar el destino de los paquetes de datos, los sockets utilizan los conceptos de dirección y puerto. Los valores númericos de puestos 1 – 1023 se reservan a servicios de interés general, montados a menudo sobre protocolos de uso extendido: Puerto 80 25 110 119 Servicio Para web con http Para correo saliente con SMTP Para correo entrante con POP3 Para el servicio de noticias con NNTP Los valores de puertos entre 1024 y 49151 se usan para servicios específicos de uso no general, el resto (a partir de 49152) se emplean para designar servicios de uso esporádico. Establecimiento de comunicaciones Java proporciona dos clases de abstracción de comunicaciones TCP: una para los procesos cliente (socket) y otra para los procesos servidor (ServerSocket). Antes de entrar en los detalles de programación se muestra la Figura 4.1 que es el esquema básico de establecimiento de comunicaciones TCP. 1. El programa que proporciona el servicio (programa servidor) crea una instancia de la clase ServerSocket, indicando el puerto asociado al servicio: ServerSocket SocketServidor = new ServerSocket(Puerto) 2 El programa que proporciona el servicio invoca al método accept sobre el socket de tipo ServerSocket. Este método bloquea el programa hasta que se produce una conexión por parte de un cliente: …SocketServidor.accept(); 2. El método accept devuelve un socket de tipo Socket, con el que se realiza la comunicación de datos del cliente al servidor: Socket ComunicaConCliente = SocketServidor.accept(); 3. El programa cliente crea una instancia de tipo Socket, a la que proporciona la dirección del nodo destino y el puerto del servicio: Socket SocketCliente = new Socket(Destino, Puerto); 4. Internamente, el socket del cliente trata de establecer comunicación con el socket de tipo ServerSocket existente en el servidor; cuando la comunicación se establece es cuando realmente (físicamente) se produce el paso 3 del diagrama. 5. Con los pasos anteriores completados se puede empezar a comunicar datos entre el cliente (o cliente) y el servidor. Programa Cliente Programa Servidor 2.accept 1. Instanciación ServerSocket 4. Instanciación Puerto destino 5. Conexión TCP (interno) Socket Socket Nodo destino Puerto destino 3. return del accept 6. Comunicación Figura 4.1 Comunicación TCP entre un cliente y un servidor. Puerto Transmisión de datos TCP es un protocolo especialmente útil cuando se desea transmitir un flujo de datos en lugar de pequeñas cantidades aisladas de información. Debido a está característica, los sockets de Java están diseñados para transmitir y recibir datos a través de los Streams definidos en el paquete java.io. La clase Socket contiene dos métodos importantes que se emplean en el proceso de transmisión de flujos de datos: InputStream getInputStream() OutputStream getOutputStream() Estas clases son abstractas, por lo que no podemos emplear directamente todos sus métodos. En general se usan otras clases más especializadas que nos permiten trabajar con flujos de datos como: DataOutputStream, DataInputStream, FileOutputStream, FileInputStream, etc. Ejemplo Hola Mundo Para esta versión necesitamos: Un programa que se ejecute en el equipo cliente y envíe el texto “Hola mundo”: TCPClienteHolaMundo Un programa que se ejecute en el equipo servidor y reciba e imprima el mensaje: TCPServidorHolaMundo Los programas TCPClienteHolaMundo y TCPServidorHolaMundo han sido implementados siguiendo los esquemas mostrados en los apartados anteriores, por lo que resultarán muy fáciles de entender. Comencemos con TCPClienteHolaMundo: TCPClienteHolaMundo.java import java.net.Socket; import java.io.*; import java.net.UnknownHostException; public class TCPClienteHolaMundo{ public static void main(String[] args){ OutputStream FlujoDeSalida; DataOutputStream Flujo; try{ Socket SocketCliente = new Socket("localhost",8000); FlujoDeSalida = SocketCliente.getOutputStream(); Flujo = new DataOutputStream(FlujoDeSalida); Flujo.writeBytes("Hola Mundo"); SocketCliente.close(); }catch (UnknownHostException e){ System.out.println("Referencia a host no resuelta"); }catch (IOException e){ System.out.println("Error en las comunicaciones"); }catch (SecurityException e){ System.out.println("Comunicacion seguridad"); } no permitida por razones de por razones de } } Ahora escribimos la clase TCPServidorHolaMundo: import java.net.ServerSocket; import java.net.Socket; import java.io.*; public class TCPServidorHolaMundo{ public static void main(String[] args){ byte[] Mensaje = new byte[80]; InputStream FlujoDeEntrada; DataInputStream Flujo; try{ ServerSocket SocketServidor = new ServerSocket(8000); Socket ComunicaConCliente = SocketServidor.accept(); System.out.println("Comunicacion establecida"); FlujoDeEntrada = ComunicaConCliente.getInputStream(); Flujo = new DataInputStream(FlujoDeEntrada); int BytesLeidos = Flujo.read(Mensaje); System.out.println(new String(Mensaje)); ComunicaConCliente.close(); SocketServidor.close(); }catch (IOException e){ System.out.println("Error en las comunicaciones"); System.exit(0); }catch (SecurityException e){ System.out.println("Comunicacion no permitida seguridad"); System.exit(0); } } } Ejemplos de lectura y escritura de un socket A continuación se presenta un simple ejemplo que ilustra como un programa puede establecer la conexión a un programa servidor usando la clase Socket y como el cliente puede enviar datos y recibir datos del servidor a través del socket. El ejemplo implementa un cliente, echoClient, que se conecta al echoServer. El echoServer simplemente recibe datos del cliente y hace echo hacia atrás. Programa: echoClient.java import java.io.*; import java.net.*; public class echoClient { public static void main(String[] args) throws IOException { Socket echoSocket = null; PrintWriter out = null; BufferedReader in = null; try { echoSocket = new Socket("taranis", 7); out = new PrintWriter(echoSocket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader( echoSocket.getInputStream())); } catch (UnknownHostException e) { System.err.println("Don't know about host: taranis."); System.exit(1); } catch (IOException e) { System.err.println("Couldn't get I/O for " + "the connection to: taranis."); System.exit(1); } BufferedReader stdIn = new BufferedReader( new InputStreamReader(System.in)); String userInput; while ((userInput = stdIn.readLine()) != null) { out.println(userInput); System.out.println("echo: " + in.readLine()); } out.close(); in.close(); stdIn.close(); echoSocket.close(); } } El programa cliente debe realizar los siguientes pasos: 1. 2. 3. 4. 5. Abrir un socket Abrir un flujo de entrada y salida para el socket. Leer desde y escribir al flujo de acuerdo al protocolo del servidor Cerrar los flujos Cerrar el socket Únicamente el paso 3 difiere de cliente a cliente, dependiendo del servidor. Los otros pasos son los mismos. Ejercicio: Construya el servidor. Otros ejemplos Revisar los programas: KnockKnockProtocol KnockKnockServer KnockKnockClient KnockKnockServer.java import java.net.*; import java.io.*; public class KnockKnockServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(4444); } catch (IOException e) { System.err.println("Could not listen on port: 4444."); System.exit(1); } Socket clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Accept failed."); System.exit(1); } PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader( new InputStreamReader( clientSocket.getInputStream())); String inputLine, outputLine; KnockKnockProtocol kkp = new KnockKnockProtocol(); outputLine = kkp.processInput(null); out.println(outputLine); while ((inputLine = in.readLine()) != null) { outputLine = kkp.processInput(inputLine); out.println(outputLine); if (outputLine.equals("Bye.")) break; } out.close(); in.close(); clientSocket.close(); serverSocket.close(); } } KnockKnockClient.java import java.io.*; import java.net.*; public class KnockKnockClient { public static void main(String[] args) throws IOException { Socket kkSocket = null; PrintWriter out = null; BufferedReader in = null; try { kkSocket = new Socket("taranis", 4444); out = new PrintWriter(kkSocket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader(kkSocket.getInputStream())); } catch (UnknownHostException e) { System.err.println("Don't know about host: taranis."); System.exit(1); } catch (IOException e) { System.err.println("Couldn't get I/O for the connection to: taranis."); System.exit(1); } BufferedReader stdIn InputStreamReader(System.in)); String fromServer; String fromUser; = new while ((fromServer = in.readLine()) != null) { System.out.println("Server: " + fromServer); if (fromServer.equals("Bye.")) break; fromUser = stdIn.readLine(); if (fromUser != null) { System.out.println("Client: " + fromUser); out.println(fromUser); } } out.close(); in.close(); stdIn.close(); kkSocket.close(); } } KnockKnockProtocol.java import java.net.*; import java.io.*; public class KnockKnockProtocol { private static final int WAITING = 0; private static final int SENTKNOCKKNOCK = 1; private static final int SENTCLUE = 2; private static final int ANOTHER = 3; private static final int NUMJOKES = 5; private int state = WAITING; private int currentJoke = 0; BufferedReader(new private String[] clues = { "Turnip", "Little Old Lady", "Atch", "Who", "Who" }; private String[] answers = { "Turnip the heat, it's cold in here!", "I didn't know you could yodel!", "Bless you!", "Is there an owl in here?", "Is there an echo in here?" }; public String processInput(String theInput) { String theOutput = null; if (state == WAITING) { theOutput = "Knock! Knock!"; state = SENTKNOCKKNOCK; } else if (state == SENTKNOCKKNOCK) { if (theInput.equalsIgnoreCase("Who's there?")) { theOutput = clues[currentJoke]; state = SENTCLUE; } else { theOutput = "You're supposed to say \"Who's there?\"! " + "Try again. Knock! Knock!"; } } else if (state == SENTCLUE) { if (theInput.equalsIgnoreCase(clues[currentJoke] + " who?")) { theOutput = answers[currentJoke] + " Want another? (y/n)"; state = ANOTHER; } else { theOutput = "You're supposed to say \"" + clues[currentJoke] + " who?\"" + "! Try again. Knock! Knock!"; state = SENTKNOCKKNOCK; } } else if (state == ANOTHER) { if (theInput.equalsIgnoreCase("y")) { theOutput = "Knock! Knock!"; if (currentJoke == (NUMJOKES - 1)) currentJoke = 0; else currentJoke++; state = SENTKNOCKKNOCK; } else { theOutput = "Bye."; state = WAITING; } } return theOutput; } } Configuración de las comunicaciones Clase ServerSocket Métodos principales socket accept() void bind(SocketAddress a) Acción Espera a que se realice una conexión y devuelve un socket para comunicarse con el cliente. Asigna la dirección establecida al socket creado con accept, si no se utiliza este método se asigna automáticamente una dirección temporal. void close() InetAddress getInetAddress() int getLocalPort() int getSoTimeout() void setSoTimeout(int ms) Cierra el socket Devuelve la dirección a la que está conectada el socket Devuelve el número de puerto asociado al socket Devuelve el valor en milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept Asigna el número de milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept Clase Socket Métodos principales void bind(SocketAddress a) Acción Asigna la dirección establecida al socket creado con accept, si no se utiliza este método se asigna automáticamente una dirección temporal void close() Cierra el socket. void connect(SocketAddress Conecta el socket a la dirección de servidor a) establecida void connect(SocketAddress Conecta el socket a la dirección de servidor a, int ms) establecida, esperando un máximo de ms milisegundos. InetAddress getInetAddress() Devuelve la dirección a la que está conectada el socket InputStream getInputStream Devuelve el stream de entrada asociada al socket Int gelLocalPort() Devuelve el número de puerto asociado al socket OutputStream Devuelve el stream de salida asociado al socket getOutputStream() int getPort() Devuelve el valor del puerto remoto al que está conectado int getSoLinger() Devuelve el número de milisegundos que se espera a los datos después de cerrar el socket. 4.4.2 Sockets Datagrama (UDP) Son un servicio de transporte sin conexión. Son más eficientes que TCP, pero en su utilización no está garantizada la fiabilidad. Los datos se envían y reciben en paquetes, cuya entrega no está garantizada. Los paquetes pueden ser duplicados, perdidos o llegar en un orden diferente al que se envió. El protocolo de comunicación con datagramas es un protocolo sin conexión, es decir, cada vez que se envíen datagramas es necesario enviar el descriptor del socket local y la dirección del socket que debe recibir el datagrama. Como se puede ver, hay que enviar datos adicionales cada vez que se realice una comunicación, aunque tiene la ventaja de que se pueden indicar direcciones globales y el mismo mensaje llegará a muchas computadoras a la vez. Un datagrama enviado por UDP se transmite desde un proceso emisor a un proceso receptor sin acuse de recibo ni reintentos. Si algo falla, el mensaje puede no llegar a su destino. Se transmite un datagrama, entre procesos, cuando uno lo envía, y el otro lo recibe. La comunicación de datagramas UDP utiliza operaciones de envío, no bloqueantes y recepciones, bloqueantes. La operación send devuelve el control cuando ha dirigido el mensaje a las capas inferiores UDP e IP, que son las responsables de su entrega en el destino. A la llegada, el mensaje será colocado en una cola del conector que está enlazado con el puerto de destino. El mensaje podrá obtenerse de la cola de recepción mediante una invocación pendiente o futura del método recibe sobre ese conector. Si no existe ningún proceso ligado al conector destino, los mensajes serán descartados. El método recibe produce un bloqueo hasta que se reciba un datagrama, a menos que se haya establecido un tiempo límite (time out) asociado al conector. Cualquier proceso que necesite enviar o recibir mensajes debe crear, primero, un conector asociado a una dirección Internet y a un puerto local. Un servidor enlazará su conector a un puerto de servidor (uno que resulte con los clientes de modo que puedan enviarle mensajes). Un cliente ligará su conector a cualquier puerto local libre. El método recibe devolverá, además del mensaje, la dirección Internet y el puerto del emisor, permitiendo al receptor enviar la correspondiente respuesta. Para algunas aplicaciones, resulta aceptable utilizar un servicio que sea susceptible de sufrir fallos de omisión ocasionales. Por ejemplo, el Servicio de Nombres de Dominio en Internet (Domain Name Service, DNS) está implementado sobre UDP. Los datagramas UDP son, en algunas ocasiones, una elección atractiva porque no padecen la sobrecarga asociadas a la entrega de mensajes garantizada. Existen tres fuentes principales para esa sobrecarga: 1. La necesidad de almacenar información de estado en el origen y en el destino. 2. La transmisión de mensajes extra. 3. La latencia para el emisor. Establecimiento de comunicaciones La Figura 4.2 muestra gráficamente el esquema básico de establecimiento de comunicaciones UDP. Programa servidor Programa cliente 1. new DatagramSocket 5. new DatagramPacket 2. new DatagramPacket 4. new DatagramSocket DatagramSocket DatagramSocket 6.send 3.receive Puerto destino Puerto destino Nodo destino Figura 4.2 Comunicación UDP Los pasos que sigue la Figura 4.2 se explican a continuación. 1 El programa que proporciona el servicio (servidor) crea una instancia de la clase DatagramSocket, hincando el puerto asociado al servicio: DatagramSocket MiSocket = new DatagramSocket(4000); 2 El programa servidor crea una instancia de la clase DatagramPacket, donde se guardarán los datos recibidos: DatagramPacket(buffer, buffer.length); 3 El programa servidor invoca el método receive sobre el socket de tipo DatagramSocket. Este método, por defecto, bloquea el programa hasta que llegan los datos: MiSocket.receive(Paquete); 4 El programa cliente crea una instancia de tipo DatagramSocket; DatagramSocket MiSocket = new DatagramSocket(); 5 El programa cliente crea una instancia de tipo DatagramPacket, proporcionándole los datos, además de la dirección y puerto destino. DatagramPacket Paquete = new DatagramPacket(buffer, Mensaje.length(), InetAddress.getByName(“localhost”),4000) 6 El programa que utiliza el servicio (programa cliente) invoca el método send sobre el socket de tipo DatagramSocket: MiSocket.send(Paquete); Ejemplo Hola Mundo UPDEnvia.java import java.net.*; public class UDPEnvia{ public static void main(String args[]){ try{ DatagramSocket MiSocket = new DatagramSocket(); byte[] buffer= new byte[15]; String Mensaje = "Hola Mundo"; buffer = Mensaje.getBytes(); DatagramPacket Paquete = new DatagramPacket(buffer, InetAddress.getByName("localhost"),1400); Mensaje.length(), MiSocket.send(Paquete); MiSocket.close(); }catch (Exception exc){ System.out.println("Error"); }//try } }//UDPEnvia UDPRecibe.java import java.net.*; public class UDPRecibe{ public static void main(String args[]){ try{ DatagramSocket MiSocket = new DatagramSocket(1400); byte[] buffer= new byte[15]; DatagramPacket Paquete = new DatagramPacket(buffer, buffer.length); MiSocket.receive(Paquete); System.out.println(new String(Paquete.getData())); MiSocket.close(); }catch (Exception e){ System.out.println("Error"); }//try }//main }//UDPRecibe Otros ejemplos de datagramas en Java El paquete java.net contiene tres clases para el uso de datagramas, es decir, para enviar y recibir paquetes en la red: DatagramSocket, DatagramPacket y MulticastSocket. Una aplicación puede enviar y recibir DatagramPackets a través de un DatagramSocket. También, se puede hacer un broadcast a múltiples recipientes escuchando a un MulticastSocket. DatagramPacket: esta clase proporciona un constructor que crea una instancia compuesta por una cadena de bytes que almacena el mensaje, la longitud del mensaje y la dirección Internet y el número de puerto local del conector destino, tal y como sigue: Paquete del datagrama cadena de bytes conteniendo el mensaje, longitud del mensaje. dirección Internet, número de puerto. Las instancias de DatagramPacket podrán ser transmitidas entre procesos cuando uno las envía, y el otro las recibe . La clase DatagramSocket proporciona varios métodos que incluyen los siguientes: - send y receive: estos métodos sirven para transmitir datagramas entre un par de conectores. El argumento de send es una instancia de DatagramPacket conteniendo el mensaje y su destino. El argumento de receive es un DatagramPacket vacío en el que - - se coloca el mensaje, su longitud y su origen. Tanto el método send como receive pueden lanzar una excepción IOException. setSoTimeout: este método permite establecer un tiempo de espera límite. Cuando se fija un límite, el método receive se bloquea durante el tiempo fijado y después lanza una excepción InterruptedIOException. connect: este método se utiliza para conectarse a un puerto remoto y a una dirección Internet concretos, en cuyo caso el conector sólo podrá enviar y recibir mensajes de esa dirección. Programa UDP: Un cliente UDP enviando un mensaje a un servidor y recoge su respuesta. UDPClient.java import java.net.*; import java.io.*; public class UDPClient{ public static void main(String args[]){ // args give message contents and server hostname DatagramSocket aSocket = null; try { aSocket = new DatagramSocket(); byte[] m = args[0].getBytes(); InetAddress aHost = InetAddress.getByName(args[1]); int serverPort = 6789; DatagramPacket request = new DatagramPacket(m, args[0].length(), aHost, serverPort); aSocket.send(request); byte[] buffer = new byte[1000]; DatagramPacket reply = new DatagramPacket(buffer, buffer.length); aSocket.receive(reply); System.out.println("Reply: " + new String(reply.getData())); }catch (SocketException e){ System.out.println("Socket: " + e.getMessage()); }catch (IOException e){ System.out.println("IO: " + e.getMessage()); }finally {if(aSocket != null) aSocket.close();} } } Programa UDP: Un servidor UDP recibe peticiones y las devuelve al cliente de forma repetitiva UDPServer.java import java.net.*; import java.io.*; public class UDPServer{ public static void main(String args[]){ DatagramSocket aSocket = null; try{ aSocket = new DatagramSocket(6789); byte[] buffer = new byte[1000]; while(true){ DatagramPacket request = new DatagramPacket(buffer, buffer.length); aSocket.receive(request); DatagramPacket reply = new DatagramPacket(request.getData(), request.getLength(), request.getAddress(), request.getPort()); aSocket.send(reply); } }catch (SocketException e){System.out.println("Socket: " + e.getMessage()); }catch (IOException e) {System.out.println("IO: " + e.getMessage()); }finally {if(aSocket != null) aSocket.close();} } } Configuración de las comunicaciones A continuación se muestran los métodos más utilizados de las clases DatagramPacket y DatagramSocket. DatagramPacket Métodos InetAddress getAddress() Acción Dirección del nodo remoto en la comunicación byte[] getData() Devuelve el mensaje que contiene el datagrama int getLength() Devuelve la longitud del mensaje del datagrama int getOffset() Devuelve el desplazamiento que indica el inicio del mensaje (dentro del array de bytes) int getPort() Devuelve el valor del puerto remoto void setAddress(InetAddress d) Establece el nodo remoto en la comunicación void setData(byte[] Mensaje) Establece el mensaje que contiene el datagrama Void setData(byte[] Mensaje, int Establece el mensaje que contiene el Dezplazamiento, int Longitud) datagrama, indicando su desplazamiento en el array de bytes y su longitud. Void setLength(int Longitud) Establece la longitud del mensaje del datagrama void setPort() Establece el valor del puerto remoto Void setSocketAddress(SocketAddress d) Establece la dirección (nodo+ puerto) remota en la comunicación DatagramSocket Metódos void bind(SocketAddress a) void close() void connect(SocketAddress a) void connect(InetAddress a, int puerto) Acción Asigna la dirección establecida al socket Cierra el socket Conecta el socket a la dirección remota establecida Conecta el socket a la dirección establecida y el puerto especificado void disconnect() InetAddress getInetAddress() int getLocalPort() OutputStream getOutputStream() int getPort() Int getSoTimeout() Boolean isBound() Boolean isClosed() Boolean isConneted Void setSoTimeout(int ms) Desconecta el socket Devuelve la dirección a la que está conectada el socket. Devuelve el número de puerto asociado al socket Devuelve el stream de salida asociado al socket Devuelve el valor del puerto remoto al que está conectado Devuelve el valor en milisegundos que el socket espera al establecimiento de comunicación Indica si el socket está vinculado Indica si el socket está cerrado Indica si el socket está conectado Indica el valor en milisegundos que el socket espera al establecimiento de comunicación 2.4.3 Diferencias entre Sockets Stream y Datagrama En UDP, cada vez que se envía un datagrama, hay que enviar también el descriptor del socket local y la dirección del socket que va a recibir el datagrama, luego los mensajes son más grandes que los TCP. Como el protocolo TCP está orientado a conexión, hay que establecer esta conexión entre los dos sockets, lo que implica un cierto tiempo empleado en el establecimiento de la conexión, que no es necesario emplear en UDP. En UDP hay un límite de tamaño de los datagramas, establecido en 64 kilobytes, que se pueden enviar a una localización determinada, mientras que TCP no tiene límite; una vez que se ha establecido la conexión, el par de sockets funciona como los streams: todos los datos se leen inmediatamente, en el mismo orden en que se van recibiendo. UDP es un protocolo desordenado, no garantiza que los datagramas que se hayan enviado sean recibidos en el mismo orden por el socket de recepción. Al contrario, TCP es un protocolo ordenado, garantiza que todos los paquetes que se envíen serán recibidos en el socket destino en el mismo orden en que se han enviado. En resumen, TCP parece más indicado para la implementación de servicios de red como un control remoto (rlogin, telnet) y transmisión de ficheros (ftp); que necesitan transmitir datos de longitud indefinida. UDP es menos complejo y tiene una menor sobrecarga sobre la conexión; esto hace que sea el indicado en la implementación de aplicaciones cliente/servidor en sistemas distribuidos montados sobre redes de área local.