Diferencia entre revisiones de «Programación de comunicacións en rede con Java»
| Línea 426: | Línea 426: | ||
} | } | ||
} catch (SocketException ex) { | } catch (SocketException ex) { | ||
| − | System.out.println( | + | System.out.println("Excepción de sockets"); |
ex.printStackTrace(); | ex.printStackTrace(); | ||
} catch (IOException ex) { | } catch (IOException ex) { | ||
| − | System.out.println( | + | System.out.println("Excepción de E/S"); |
ex.printStackTrace(); | ex.printStackTrace(); | ||
} | } | ||
Revisión del 19:06 23 nov 2021
Sumario
- 1 Introducción
- 2 Comunicaciones en rede
- 3 Interfaces de rede
- 4 Enderezos IP
- 5 Resolución de nomes
- 6 Sockets UDP
- 7 Sockets TCP
- 8 Creditos e referencias
- 9 Tarefas
Introducción
A nivel pedagóxico, estes son os resultados de aprendizaxe e criterios de avaliación:
- RA1. Programa mecanismos de comunicación en rede empregando sockets, e analiza o escenario de execución.
- CA1.1. Identificáronse escenarios que precisen establecer comunicación en rede entre varias aplicacións.
- CA1.2. Identificáronse os papeis de cliente e de servidor, e as súas funcións asociadas.
- CA1.3. Recoñecéronse librarías e mecanismos da linguaxe de programación que permitan programar aplicacións en rede.
- CA1.4. Analizouse o concepto de socket, os seus tipos e as súas características.
- CA1.5. Utilizáronse sockets para programar unha aplicación cliente que se comunique cun servidor.
- CA1.6. Desenvolveuse unha aplicación servidor en rede e verificouse o seu funcionamento.
- CA1.7. Desenvolvéronse aplicacións que utilicen sockets para intercambiar información.
- CA1.8. Utilizáronse fíos para implementar os procedementos das aplicacións relativos á comunicación en rede.
- CA1.9. Monitorizáronse os tempos de resposta das aplicacións e depuráronse con base nos resultados obtidos.
Comunicaciones en rede
O paquete java.net proporciona clases que permiten levar a cabo comunicacións entre procesos utilizando os protocolos estándares da familia de TCP/IP. Pode dividirse en dúas seccións:
- Clases de baixo nivel que permiten representar as principales entidades dos niveles de enlace, rede e transporte;
- Clases de alto nivel relacionadas con protocolos de nivel de aplicación
Comezaremos revisando as clases para interfaces de rede e enderezos IP, para despois progresar en algocentral nas comunicacións en rede: a programación de sockets de UDP e de TCP, cos que se desenvolverán programas servidores e clientes que se comunican utilizando os devanditos protocolos de transporte UDP e TCP; e que se pode considerar que implementan sencillos protocolos de nivel de aplicación.
| Interfaces de rede | NetworkInterface: interfaz de rede |
| Enderezos IP | InetAddress: enderezo IP Inet4Address: enderezo IPv4 Inet6Address: enderezo IPv6 InterfaceAddress:enderezo IP máis información relativa á rede á que pertenece a interfaz |
| Sockets | Socket: socket TCP de cliente, para conectar cun proceso servidor mediante TCP ServerSocket: socket TCP de servidor, que acepta conexións desde un socket de cliente mediante TCP. DatagramSocket: socket de UDP, que se usa para enviar e recibir datagramas de UDP |
Interfaces de rede
As interfaces de reed presentes no sistema podense amosar cos comandos:
- ifconfig ou ip en Linux
- ipconfig en Windows.
A clase NetworkInterface ten métodos estáticos que permiten obter unha enumeración de todas as interfaces de rede ou NetworkInterface existentes no sistema.
Una NetworkInterface pode ter máis dunha configuración IP asociada,e normalmente cando menos unha para IPv4 e outra para IPv6. Con distintos métodos da clase podense obter para cada unha obxectos de tipo InterfaceAddress e InetAddress.
A clase InterfaceAddress proporciona información adicional a InetAddress coma por exemplo a lonxitude da máscara de rede ou o enderezo de broadcast da rede á que pertence (para un enderezo IPv4).
Na táboa explicanse alguns dos métodos de NetworkInterface.
| Método | Funcionalidade |
|---|---|
| static Enumeration<NetworkInterface> getNetworkInterfaces() | Devolve todas as interfaces de rede existentes na máquina. A partir do Enumeration pódese obter un Iterator para iterar sobre elas. |
| String getDisplayName() String getName() |
Devuelven o nome da interfaz |
| byte[] getHardwareAddress() | Devolve o enderezo MAC en forma de array de 6 bytes (48 bits). |
| Enumeration<InetAddress> getInetAddresses() List<InterfaceAddress> getInterfaceAddresses() |
Devolven os enderezos IP asociados á interfaz. Será en xeral cando menos un enderezo IPv4 e un IPv6.
|
| boolean isLoopback() | Determina se unha interfaz é de tipo loopback. |
O exemplo permitirache familiarizarte co entorno de rede no que está integrado o teu ordenador; o coñecemento poderá ser de utilidade para crear programas que se comuniquen con outros desenvoltos por outros compañeiros, utilizando os protocolos de transporte UDP e TCP.
Exemplo con programa que amosa información das interfaces de rede
Aquí está o código dun programa que amosa información de todas as interfaces de rede presentes no sistema.
Amosa información da súa configuración para IPv4 e IPv6 (enderezos IP, máscaras de rede, enderezos de broadcast, etc).
Cuestións. Antes de executar o programa asegúrate de que o equipo ten conexión a internet ou a unha rede local. ¿En qué cambia a saída do programa cando se desconecta o cable de rede ou se desactiva a interfaz de rede desde o sistema operativo? ¿Para qué tipos de interfaces se amosa información? ¿Hay alguna que esté sempre activa? ¿Cal? ¿Por qué?
Enderezos IP
Unha interfaz de rede pode ter configurada un ou varios enderezos IPv4 e un ou varios enderezos IPv6; e cada un cos seus correspondentes máscaras de rede.
As clases que permiten xestionar esta información:
- InterfaceAddress: Representa a configuración IP dunha interfaz. Esto inclúe o enderezo IP e parámetros adicionais:
- Lonxitude da máscara de rede (número de bits que no enderezo IP especifican a rede)
- Cando se trata dun enderezo IPv4, enderezo de broadcast para a rede á que pertence a interfaz
- InetAddress: Representa un enderezo IP. Ten dúas subclases para representar enderezos IPv4 e IPv6:
- Inet4Address
- Inet6Address
A clase InterfaceAddresss é moi sinxela e os seus principais métodos amosanse na seguinte táboa.
Para entender mellor o seu funcionamiento hai que ter en conta que representa aa configuración para IP dunha interfaz de rede; pode corresponder polo tanto a calquiera das versións deste protocolo (IPv4 ou IPv6).
Ten un método para obter o enderezo IP e métodos adicionais para obter outros parámetros da conguración.
| Método | Funcionalidade |
|---|---|
| InetAddress getAddress() | Devolve o enderezo IP. |
| short getNetworkPrefixLength() | Devolve a lonxitude da máscara de rede (o número de bits dentro do enderezo IP que especifican a rede) |
| InetAddress getBroadcast() | Devoelve o enderezo de broadcast para a rede á que pertence o enderezo IP (so no caso de que se trate dun enderezo de IPv4; con enderezo de IPv6 devolve null |
A clase InetAddress ten toda a funcionalidade precisa para realizar calquer tipo de operación sen preocuparse de se o enderezo IP é de IPv4 o IPv6; porén na práctica non adoitan utilizarseas súas subclases Inet4Address e Inet6Address.
A táboa amosa algúns métodos desta clase. Ten ademáis algúns métodos estáticos para resolución de nombres que se verán ne seguinte sección
| Método | Funcionalidade |
|---|---|
| static InetAddress getByName(String host) | Obten unha InetAddress para o host. En host pódese proporcionar un enderezo IP (analízase e valçidase o enderezo IP e obtense un obxecto de clase Inet4Address ou Inet6Address) ou un nome de host (utilizarase o mecanismo de resolución de nomes para obter un enderezo IP) |
| String getHostAddress() | Devolve a representación textual do enderezo IP |
| byte[] getAddress() | Devolve un array cos bytes que compoñen o enderezo IP (4 con IPv4 e 16 con IPv6) |
| static InetAddress getByAddress(byte[] addr) | Devolve unha InetAddress para o enderezo IP dado, especificado como un array de bytes |
| boolean isReachable(int timeout) | Determina se o enderezo é alcanzable, especificando un tempo máximo de espera (timeout) en milisegundos |
| boolean isSiteLocalAddress() | Determina se é enderezo é local ou privado. Os rangos de enderezos para redes privadas especifícanse na RFC 1918. As redes privadas de clase C para IPv4 (as máis frecuentes) son 192.168.X.0/24, con 0 <= X <= 255 |
| boolean isLoopbackAddress() | Determina se o enderezo IP é dunha interfaz de tipo loopback |
| static InetAddress getLoopbackAddress() | Devolve o enderezo de loopback:
|
Resolución de nomes
A clase InetAddress ten métodos estáticos:
- que realizan resolución de nomes: dado un nome, permiten obtener o enderezo IP asociado.
- que realizan resolución inversa de nomes: permiten obter un nome de host a partir do seu enderezo IP.
| Método | Funcionalidade |
|---|---|
| static InetAddress getByName(String host) | Obten un enderezo IP a partir dun nome de host, utilizando servizos de resolución de nomes ou ben a partir dunha representación textual dun enderezo IP |
| static InetAddress[] getAllByName(String host) | Similar o método anterior pero máis completo; devolve un array con todos os enderezos IP que se pueden obter utilizando servizos de resolución de nomes; tamén se pode especificar o host mediante a representación textual dun enderezo IPv4 ou IPv6 |
| public String getHostName() | Este método permite realizar unha resolución inversa de nome: obter un nome de host a partir do seu enderezo IP. Se a InetAddress creouse sen especificar un nome para o host, enton realízase unha resolución inversa de nomes para obter o nome do host a partir do seu enderezo IP. |
| static InetAddress getByAddress (String host, byte[] addr) | Crea unha InetAddress indicando un enderezo IP e un nome de host. Devolve o nome proporcionado ó crear a InetAddress sen realizar resolución inversa de nomes |
| String getCanonicalHostName() | Devuelve o nome de host canónico ou FQDN (fully qualified domain name) utilizando o protocolo DNS para obter esta información. |
| static InetAddress getLocalHost() | Devolve o enderezo IP do propio host. En Linux normalmente obtense o seu nome de /etc/hostname , e resolvese o nome para obter un enderezo IP. Dependendo do sistema poderíase obter o enderezo da interfaz de loopback, un enderezo IP privado pertencente á rede local, ou incluso unha IP externa ou pública. Se non se pode resolver este nombre, lánzase unha excepción de tipo UnknownHostException |
Tarefa Crea un programa que amose o nome e enderezo IP do propio host utilizando o método getLocalHost.
Sockets UDP
Introdución ó protocolo UDP
O protocolo de transporte UDP non é orientado a conexión. So permite o envío de paquetes de datos individuales chamados datagramas. Estos poden chegar en orde distinto a orden en que se enviaron, e incluso non chegar. Neste sentido, o protocolo de transporte UDP non aporta ningún valor engadido ó servizo de envío de datagramas IP proporcionado polo nivel de rede (ou de internet).
Un datagrama de UDP está encapsulado nun datagrama de IP, como se amosa na figura. Cada liña amosa 4 bytes. Arriba amosanse nunha regra os bits do 0 ó 31.
| Porto orixe | Porto destino | ||
|---|---|---|---|
| Lonxitude da mensaxe | Checksum |
| Bits | 0 - 15 | 16 - 31 | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Porto orixen | Porto destino | ||||||||||||||||||||||||||||||
| 32 | Lonxitude da mensaxe | Suma de verificación | ||||||||||||||||||||||||||||||
| 64 | Datos | |||||||||||||||||||||||||||||||
A cabecera UDP consta de 4 campos dos cales 2 son opcionais (con fondo vermello na táboa). Os campos dos portos orixen e destino son campos de 16 bits que identifican o proceso de emisión e recepción. Xa que UDP carece dun servidor de estado e a orixe UDP non solicita respostas, o porto orixe é opcional; en caso de non ser utilizado, o porto orixe debe ser posto a cero.
Pódese ver que o conxunto dun datagrama de UDP encapsulado nun datagrama de IP conten, ademáis dos datos que se envían do proceso de orixe ó de destino, o enderezo IP e o porto de orixe e o enderezo IP e porto de destino; toda a información necesaria para que os datos cheguen o seu destino e toda a información necesaria para que o proceso que o recibe poida enviar datagramas como resposta ó proceso de orixe.
A comunicación entre dous procesos mediante o protocolo UDP é moi similar á comunicación entre dúas personas por correo (postal ou electrónico): xunto coa mensaxe e os datos para que chegue o seu destino, están os datos para enviar unha resposta.
A API de Java permite enviar e recibir datagramas mediante UDP en forma de obxectos da clase DatagramPacket. Este clase ten métodos setter e getter para toda esta información. Amosanse a continuación os métodos getter:
| Método | Funcionalidade |
|---|---|
| byte[] getData() int getLength() |
Obteñen os datos que conten o datagrama coma un array de bytes, e a lonxitude do array. |
| InetAddress GetAddress() int getPort() |
Obteñen enderezos IP e porto desde onde se enviou o datagrama. Poderían non estar informados. Un datagrama no que no están informados sería análogo a unha carta anónima, sen remitente. |
Na figura amósase cómo se pode utilizar a clase DatagramSocket para enviar e recibir datagramas de UDP entre dous procesos.
UDP é un protocolo sinxelo que pode ser apropiado para o intercambio de pequenas cantidades de información, ou cando é preferible rápidez na transmisión aínda que se poida perder algún paquete (coma no caso do streaming); nese caso ten coma vantaxes a súa sinxelez e rapidez.
Por estas características é utilizado por protocolos de nivel de aplicación tales como DNS (aínda que os servidores de DNS tamén utilizan TCP para determinadas operacións que requiren a transferencia de cantidades maiores de información entre eles), SNMP ou DHCP.
Hai que ter moi en conta as limitacións de UDP e utilizaro só para aplicacións para as que sea axeitado.
A clase DatagramSocket
Un 'socket de UDP é a combinación dun enderezo IP máis un número de porto. Un proceso servidor que presta un servizo utilizando UDP crea un socket de UDP ligado a un porto e a unha interfaz de rede local, identificada polo seu enderezo. Entón, pode recibir datagramas enviados por procesos clientes. Se cuando se crea un socket non se especifica un enderezo IP, entonces pode recibir datagramas a través de calquera interfaz de rede local: dise que se ligou o socket a unha wildcard address.
A clase DatagramPacket utilízase para representar datagramas de UDP. Pódense enviar e recibir DatagramPacket a través dun DatagramSocket.
UDP permite ademáis de enviar e recibir datagramas de enderezos particulares (comunicación unicast):
- Envío broadcast'. Cando se especifica o enderezo de broadcast da rede local, todos os hosts da rede local reciben o datagrama. Isto é moi sinxelo e poder ser de utilidade.
- Comunicación multicast. É posible utilizando a clase MulticastSocket. Un grupo de multicast está identificado por un enderezo IP de clase D. Pódese crear un MulticastSocket ligado a un porto local e logo unilo ó grupo especificando o seu enderezo IP.
Os cadros amosan algúns dos métodos das clases DatagramSocket e DatagramPacket. Casi todas as operacións realizadas cun DatagramSocket poden provocar excepcións de tipo IOException . Esta clase implementa a interfaz Closeable polo que pódese empregar nun bloque try … catch con recursos.
| Método | Funcionalidade |
|---|---|
| DatagramSocket() DatagramSocket(int port) DatagramSocket(int port, InetAddress laddr) |
Diversos constructores para un DatagramSocket para envío e recepción de datagramas de UDP.
O porto e o enderezo IP especificados son locales. O enderezo IP e o porto de destino para datagramas enviados van especificados no propio datagrama. Se se especifica o enderezo IP dunha interfaz activa, o socket lígase a ela. Se non, lígase a todas as interfaces activas (wildcard address). Pódese especificar un porto para enviar e recibir datagramas por él. Un proceso servidor normalmente fará iso. Un programa cliente normalmente non especificará un porto, e entón o sistema elixirá un porto efímero |
| void receive(DatagramPacket p) | Recibe un datagrama desde a interfaz (especificada por un enderezo IP) e o porto o que está ligado o socket. Se non está ligado a ningunha interfaz específica pode recibir o datagrama por calquera interfaz. O enderezo IP e o puerto de orixen virán indicados no propio datagrama recibido |
| void send(DatagramPacket p) | Envía un datagrama. O enderezo IP e o porto deben estar especificados no propio datagrama |
| setSoTimeout(int timeout) | Establece un tempo máximo de espera cando se recibe un datagrama co método receive. Se pasado ese tempo non se recibíu un datagrama xenerarase unha excepción de tipo SocketTimeoutException. Un valor 0 para timeout significa espera indefinida, a funcionalidade por defecto de receive() |
| void close() | Pecha o socket |
| void connect (InetAddress address, int puerto) void connect (SocketAddress addr) |
Conecta o socket a un enderezo e porto remotos de xeito que só se poden enviar datagramas a ese enderezo e porto ou recibilos dese enderezo e porto.
O nombre deste método non debe inducir a confusión. Que exista un método connect non é contradictorio co feito de que UDP sexa un protocolo non orientado a conexión, a diferencia de TCP |
| void disconnect{} | Desconecta o socket |
| Método | Funcionalidade |
|---|---|
| DatagramPacket(byte[] buf, int length) DatagramPacket(byte[] buf, int offset, int length) DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) DatagramPacket(byte[] buf, int length, InetAddress address, int port) |
Diversos constructores. Os parámetros corresponden a:
|
| getAddress(), setAddress() getData(), setData() |
Diversos métodos getter e setter. |
Programación de aplicacións servidores e clientes baseadas en UDP
Os programas de exemplo virán en parellas: un programa servidor e un programa cliente. Os enderezos e portos que utilizar se especificarán mediante un argumento de liña de comandos.
Texto entre procesos. Codificación
Moitos dos programas transmiten textos entre procesos; xeneralmente será texto codificado en UTF-8.
Importante. Codificaciones de texto e codificación en UTF-8. Na comunicación entre procesos é preciso facelo de acordo a un protocolo preestablecido de xeito que ambos poidan interpretar correctamente a información recibida desde o outro e así dar a resposta axeitada en cada caso; porén debemos asegurar que ambos utilizan o mesmo formato para os datos que se transmiten entre eles. No caso dun texto o proceso emisor debe utilizar sempre a mesma codificación, e o receptor debe interpretalo seempre conforme a esa mesma codificación. Se ambos procesos se executan en sistemas distintos poderían non usar a mesma codificación (que normalmente será UTF-8) para codificar o texto en orixen e para decodificarlo en destino; por iso non se pode asumir que será sempre UTF-8. Nas aplicaciones típicas co protocolo UDP, a codificación de texto interven cuando se obten unha secuencia de bytes a partir dunha cadena de caracteres ou viceversa. Conven utilizar a codificación UTF-8. Para iso hai que facer o seguiente:
- Obter a secuencia de bytes para un String s con s.getBytes("UTF-8") .
- Crear un String a partir de un byte[] b con new String(b, "UTF8") se todos os datos no array de bytes b son datos válidos. Nembargantes con UDP os datos dun DatagramPacket dg están en dg.getData(), e o número de bytes de datos válidos está en dg.getLength(). Por iso é preciso indicar o número de bytes para crear o String; e se pode crear con new String(dg.getData(), 0, dg.getLength(), "UTF-8") .
Exemplo. Echo de servidor e cliente
No seguiente exemplo o cliente envía unha cadena de caracteres ó servidor e este envía como resposta ó cliente a mesma liña pero antepoñendo un carácter #.
Tanto en cliente coma servidor seguense as pautas para obter un array de byte coa codificación en UTF-8 dun String; e inversamente para crear un String a partir da súa codificación en UTF-8 dada en un array de byte.closeable .
O porto para o que se crea o DatagramSocket pásase o programa servidor por liña de comandos. Pásaselle ó programa cliente ademáis o host servidor (por exemplo "localhost" se ambos se executan na mesma máquina.
Unha vez creado o DatagramSocket, úsase para recibir un datagrama (DatagramPacket) do proceso cliente co método receive. Para recibir os datos, débese crear un array de tipo byte[] datosRecibidos o suficiente grande como para recibir todos os datos que vaia a enviar o proceso cliente. Do datagrama recibido obtense o enderezo IP e o porto de procedencia do datagrama. Con eles crease un DatagramPacket de resposta que se envía polo mesmo DatagramSocket co método send.
Comezamos co código do servidor:
1 package udp_echoserver; 2 import java.io.IOException; 3 import java.net.DatagramPacket; 4 import java.net.DatagramSocket; 5 import java.net.InetAddress; 6 import java.net.SocketException; 7 public class UDP_EchoServer { 8 private final static int MAX_BYTES = 1400; 9 private final static String COD_TEXTO = “UTF-8”; 10 public static void main(String[] args) { 11 if (args.length < 1) { 12 System.err.println("ERROR, indicar: puerto."); 13 System.exit(1); 14 } 15 int numPuerto = Integer.parseInt(args[0]); 16 try(DatagramSocket serverSocket=new DatagramSocket(numPuerto)) { 17 System.out.printf(“Creado socket de datagramas para puerto %s.\n”, 18 numPuerto); 19 while (true) { 20 System.out.println("Esperando datagramas."); 21 byte[] datosRecibidos = new byte[MAX_BYTES]; 22 DatagramPacket paqueteRecibido = new DatagramPacket( 23 datosRecibidos,datosRecibidos.length); 24 serverSocket.receive(paqueteRecibido); 25 String lineaRecibida = new String(paqueteRecibido.getData(), 26 0, paqueteRecibido.getLength(), COD_TEXTO); 27 InetAddress IPCliente = paqueteRecibido.getAddress(); 28 int puertoCliente = paqueteRecibido.getPort(); 29 System.out.printf("Recibido datagrama de %s:%d (%s)\n", 30 IPCliente.getHostAddress(), puertoCliente, lineaRecibida); 31 String respuesta = "#" + lineaRecibida + "#"; 32 byte[] b = respuesta.getBytes(COD_TEXTO); 33 DatagramPacket paqueteEnviado = new DatagramPacket( 34 b, b.length, IPCliente, puertoCliente); 35 serverSocket.send(paqueteEnviado); 36 } 37 } catch (SocketException ex) { 38 System.out.println("Excepción de sockets"); 39 ex.printStackTrace(); 40 } catch (IOException ex) { 41 System.out.println("Excepción de E/S"); 42 ex.printStackTrace(); 43 } 44 } 45 }
O programa cliente crea un DatagramSocket, mais non ligado a un porto como o facía o programa servidor. Le liña tras liña de entrada estándar, obtén a súa codificación en UTF-8, e a envía ó programa servidor. Despois agarda a recibir unha resposta: de non ser por clientSocket.connect(IPServidor, numPuerto), a chamada posterior a client-Socket.receive(paqueteRecibido) podería obter un datagrama enviado desde calquer outro enderezo IP ou porto. Porén é recomendable utilizar este método por precaución, a non ser que se desexe que o programa poida recibir datagramas desde máis dunha posible combinación de enderezo IP e porto.
1 package udp_echoclient; 2 import java.io.BufferedReader; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.net.DatagramPacket; 6 import java.net.DatagramSocket; 7 import java.net.InetAddress; 8 import java.net.SocketException; 9 public class UDP_EchoClient { 10 private final static int MAX_BYTES = 1400; 11 private final static String COD_TEXTO =”UTF-8”; 12 public static void main(String[] args) { 13 if (args.length < 2) { 14 System.err.println("ERROR, indicar: host_servidor puerto"); 15 System.exit(1); 16 } 17 String nomHost = args[0]; 18 int numPuerto = Integer.parseInt(args[1]); 19 try (DatagramSocket clientSocket = new DatagramSocket(); 20 InputStreamReader isrStdIn = new InputStreamReader(System.in,COD_TEXTO); 21 BufferedReader brStdIn = new BufferedReader(isrStdIn)) { 22 String lineaLeida; 23 System.out.println("Introducir líneas. Línea vacía para terminar."); 24 System.out.print("Línea>"); 25 while ((lineaLeida = brStdIn.readLine()) != null && lineaLeida.length() > 0) { 26 InetAddress IPServidor = InetAddress.getByName(nomHost); 27 byte[] b = lineaLeida.getBytes(COD_TEXTO); 28 DatagramPacket paqueteEnviado = new DatagramPacket(b, b.length, IPServidor, numPuerto); 29 clientSocket.connect(IPServidor, numPuerto); 30 clientSocket.send(paqueteEnviado); 31 byte[] datosRecibidos = new byte[MAX_BYTES]; 32 DatagramPacket paqueteRecibido = new DatagramPacket(datosRecibidos, datosRecibidos.length); 33 clientSocket.receive(paqueteRecibido); 34 String respuesta = new String(paqueteRecibido.getData(), 0, paqueteRecibido.getLength(), COD_TEXTO); 35 System.out.printf("Datagrama recibido de %s:%d: %s\n", paqueteRecibido.getAddress().getHostAddress(), 36 paqueteRecibido.getPort(), respuesta); 37 System.out.print("Línea>"); 38 } 39 } catch (SocketException ex) { 40 System.out.println("Excepción de sockets"); 41 ex.printStackTrace(); 42 } catch (IOException ex) { 43 System.out.println("Excepción de E/S"); 44 ex.printStackTrace(); 45 } 46 } 47 }
Probar funcionamento do servidor sen necesidade do cliente
Co comando nc podense enviar datagramas de UDP cun texto, e amosar o texto contido no datagrama de resposta recibido. Con iso podemos probar o funcionamiento dun programa servidor sen necesidade de crear un programa cliente. Se o servidor de eco anterior está funcionando no puerto 8000 do mesmo ordenador (localhost), se lle pode enviar un datagrama co texto "ola echo" e amosar a resposta con:
$ echo -n "ola eco" | nc -u localhost 8000 #ola eco#
Programación distribuída baseada en UDP
Programación de servidores multifío baseados en UDP
Sockets TCP
O protocolo de transporte TCP é orientado a conexión; o que significa que para que dous procesos se comuniquen entre sí debe establecerse antes unha conexión.
Establecemento da conexión
Un proceso servidor está á escoita de solicitudes de conexión nun socket de servidor ligado a un porto fixo (no rango de portos de sistema ou well-known ports, ou en portos rexistrados).
Un proceso cliente envía unha solicitude de conexión de TCP desde un porto efímero a ese porto. Cando se recibe esa solicitude, resérvase un porto efímero no servidor para a comunicación co cliente, e comunícaselle o seu número ó cliente. Con isto queda establecida a conexión entre un porto efímero do servidor e un porto efímero do cliente, a través de sendos sockets de comunicación.
O proceso de establecimiento da conexión é relativamente rápido, e esta pode durar todo o tempo que sexa preciso. Se se crea un novo fío de execución para atender cada conexión poderanse atender múltiples clientes simultáneamente.
Comunicacion
A comunicación a través dun socket de comunicación realízase en forma dunha secuencia de bytes. TCP permite que os datos se reciban no proceso de destino na mesma orde en que se envían desde o proceso de orixe. Porén en Java utilízanse streams tanto para o envío coma para a recepción de datos a través dun socket de TCP.
A comunicación pódese realizar simultáneamente en ambos sentidos (modo full duplex) e de xeito independente. Cada proceso escribe os datos nun OutputStream conectado a un InputStream no outro extremo.
Funcionamento global
Para atender peticións de conexión desde procesos clientes, o proceso servidor crea un ServerSocket ou socket de conexión. Este debe ligarse (bind en inglés) a un porto:
- Se se especifica o enderezo IP dunha das interfaces de rede do servidor, lígase a esa interfaz.
- Se non se especifica ninguna, lígase a todas as súas interfaces de rede activas (wildcard address).
Coa chamada ó método accept, o servidor queda á espera de recibir peticións de conexión. Dise tamén que o servidor queda escoitando (listening).
A conexión establécese cando un cliente crea un novo Socket indicando o enderezo IP e o porto do servidor. De resultas, tanto accept() no lado do servidor coma new Socket() no do cliente devolven un socket de comunicación local, ambos conectados entre sí.
Asociado a cada socket de TCP hai dous streams, un de entrada e outro de saída. O que cada proceso envía polo stream de saída asociado o seu socket o outro o recibe polo stream de entrada asociado o seu.
Case todas as operacións realizadas con sockets e streams asociados poden provocar excepcións de tipo IOException. Todos estos recursos, tanto os sockets coma os streams, son de clases que implementan Closeable ou AutoCloseable, polo que se poden utilizar bloques try … catch con recursos para xestionalos.
| Método | Funcionalidade |
|---|---|
ServerSocket() ServerSocket(int port) ServerSocket(int port, int backlog) ServerSocket(int port, int backlog, InetAddress bindAddr) |
Distintos creadores para la clase ServerSocket:
backlog é o tamaño máximo dunha cola de peticións de conexión de clientes pendentes de ser atendidas. |
Socket accept() |
Queda á espera dunha petición de conexión dun cliente. Unha vez recibida a acepta. Devuelve o novo socket creado para a comunicación co cliente |
void close() |
Pecha o socket |
| Método | Funcionalidade |
|---|---|
InetAddress getLocalAddress() int getLocalPort() |
Devolven enderezos IP e portos locales para o socket |
InetAddress getInetAddress() int getPort() |
Devolven enderezo IP e portos remotos para o socket |
InputStream getInputStream() |
Devuelve o InputStream asociado ó socket, desde o que se pode recibir información enviada desde o outro extremo. |
OutputStream getOutputStream() |
Devuelve o OutputStream asociado ó socket, co que se pode enviar información hacia o outro extremo |
void close() |
Pecha o socket |
Programación de aplicacións servidores e clientes baseadas en TCP
Codificación
Para implementar un protocolo baseado en texto (coma é o caso do servidor de eco que imos desenvolver) podense enviar e recibir liñas de texto (normalmente codificado en UTF-8).
Hai que ter sempre presente que nas operacións de lectura e de escritura de texto normalmente se vai utilizar a codificación UTF-8 por defecto, pero podería no ser así en algunha contorna particular; porén e moi convinte indicar expresamente esta codificación e non asumir que é a que se utilizará por defecto.
Exemplo. Echo de servidor e cliente
O exemplo seguinte é similar ó servidor de eco implementado no primeiro exemplo para o protocolo UDP.
O servidor crea primeiro un ServerSocket e despois acepta unha conexión tras outra sobre él.
Con cada conexión le liña tras liña de texto, e cada vez que recibe unha línea, envía coma resposta ó cliente o texto da liña entre dous caracteres "#".
Cando recibe unha liña vacía pecha a conexión co cliente, e queda á espera de recibir a seguinte conexión.
Adoita chamarse sesión ó intercambio de mensaxes entre cliente e servidor desde o momento en que se establece unha conexión ata o momento en que se pecha.
Para poder escribir e ler texto liña a liña, constrúense sendos BufferedWriter e BufferedReader sobre os OutputStream e InputStream asociados ó socket. Tras cada operación de escritura, execútase o método flush para que o texto se envíe inmediatamente.
1 package socketstcp_servidoreco; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.io.OutputStream; 8 import java.io.OutputStreamWriter; 9 import java.io.BufferedReader; 10 import java.io.BufferedWriter; 11 public class SocketsTCP_ServidorEco { 12 private final static String COD_TEXTO =”UTF-8”; 13 public static void main(String[] args) { 14 if (args.length < 1) { 15 System.err.println(“ERROR, indicar: porto.”); 16 System.exit(1); 17 } 18 int numPuerto = Integer.parseInt(args[0]); 19 try (ServerSocket socketServidor = new ServerSocket(numPuerto)) { 20 System.out.printf(“Creado socket de servidor en porto %d. Esperando conexións de clientes.\n”, 21 numPuerto); 22 23 while (true) { // Acepta unha conexión de cliente tras outra 24 try (Socket socketComunicacion = socketServidor.accept()) { 25 System.out.printf(“Cliente conectado desde %s:%d.\n”, 26 socketComunicacion.getInetAddress().getHostAddress(), 27 socketComunicacion.getPort()); 28 29 try (InputStream isDeCliente = socketComunicacion.getInputStream(); 30 OutputStream osACliente = socketComunicacion.getOutputStream(); 31 InputStreamReader isrDeCliente = 32 new InputStreamReader(isDeCliente, COD_TEXTO); 33 BufferedReader brDeCliente = new BufferedReader(isrDeCliente); 34 OutputStreamWriter oswACliente = 35 new OutputStreamWriter(osACliente, COD_TEXTO); 36 BufferedWriter bwACliente = new BufferedWriter(oswACliente)) { 37 String lineaRecibida; 38 while ((lineaRecibida = brDeCliente.readLine()) != null 39 && lineaRecibida.length() > 0) { 40 bwACliente.write(“#” + lineaRecibida + “#”); 41 bwACliente.newLine(); 42 bwACliente.flush(); 43 } 44 } 45 } 46 System.out.println(“Cliente desconectado.”); 47 48 } 49 50 } catch (IOException ex) { 51 System.out.println(“Excepción de E/S”); 52 ex.printStackTrace(); 53 System.exit(1); 54 } 55 } 56 }
O programa cliente establece unha conexión co servidor, lee liña tras liña de texto da entrada estándar System.in, as envía ó servidor, e recibe as respostas do servidor.
1 package socketstcp_clienteeco; 2 import java.io.BufferedReader; 3 import java.io.BufferedWriter; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.io.OutputStream; 8 import java.io.OutputStreamWriter; 9 import java.net.Socket; 10 import java.net.UnknownHostException; 11 public class SocketsTCP_ClienteEco { 12 private final static String COD_TEXTO =”UTF-8”; 13 public static void main(String[] args) { 14 if (args.length < 2) { 15 System.err.println(“ERRO, indicar: host_servidor porto”); 16 System.exit(1); 17 } 18 String nomHost = args[0]; 19 int numPuerto = Integer.parseInt(args[1]); 20 try (Socket socketComunicacion = new Socket(nomHost, numPuerto)) { 21 System.out.println(“Conectado a servidor.”); 22 try (OutputStream osAServidor = socketComunicacion.getOutputStream(); 23 InputStream isDeServidor = socketComunicacion.getInputStream(); 24 InputStreamReader isrDeServidor = 25 new InputStreamReader(isDeServidor, COD_TEXTO); 26 OutputStreamWriter oswAServidor = 27 new OutputStreamWriter(osAServidor, COD_TEXTO); 28 BufferedWriter bwAServidor = new BufferedWriter(oswAServidor); 29 BufferedReader brDeServidor = new BufferedReader(isrDeServidor); 30 InputStreamReader isrStdIn = new InputStreamReader(System.in); 31 BufferedReader brStdIn = new BufferedReader(isrStdIn)) { 32 String lineaLeida; 33 System.out.println(“Introducir líñas. Líña vacía para terminar.”); 34 System.out.print(“Liña>”); 35 while ((lineaLeida = brStdIn.readLine()) != null && 36 lineaLeida.length() > 0) { 37 bwAServidor.write(lineaLeida); 38 bwAServidor.newLine(); 39 bwAServidor.flush(); 40 System.out.println(“resposta: “ + brDeServidor.readLine()); 41 System.out.print(“Liña>”); 42 } 43 } 44 } catch (UnknownHostException e) { 45 System.err.println(“Host descoñecido: “ + nomHost); 46 System.exit(1); 47 } catch (IOException ex) { 48 System.err.println(“Excepción de E/S”); 49 ex.printStackTrace(); 50 System.exit(1); 51 } 52 } 53 }
Para a proba de servidores baseados en TCP pódese utilizar:
- a utilidad nc
- o comando telnet
Isto pode facer innecesario desenvolver ou utilizar outros programas clientes.
O uso de nc e de telnet con TCP é moi similar; aconséllase nc porque é máis versátil e tamén se pode utilizar para interactuar con servidores baseados en UDP.
Para probar o servidor anterior, un exemplo de interacción con telnet e nc sería o seguinte:
| Servidor | Sesión con nc | Sesión con telnet |
|---|---|---|
Creado socket de servidor en puerto 7000. Esperando conexiones de clientes. |
||
$ nc localhost 7000 |
||
Cliente conectado desde 127.0.0.1:49782. |
||
hola #hola# hola otra vez #hola otra vez# |
||
$ telnet localhost 7000 Trying 127.0.0.1... Connected to localhost. Escape character is ‘^]’. | ||
Cliente desconectado. Cliente conectado desde 127.0.0.1:49784 |
||
hola desde otro cliente #hola desde otro cliente# hola de nuevo desde otro cliente #hola de nuevo desde otro cliente# | ||
Cliente desconectado. |
||
Connection closed by foreign host. |
Coma se pode observar, unha vez establecida a conexión con nc , cada liña introducida envíase o cliente e se amosa a resposta recibida.
Cando se intenta a conexión con telnet durante a sesión con nc, non se establece nese momento. Aínda que se amosa unha mensaxe que di que si, podese ver que o servidor non escribe ningunha mensaxe dicindo que se estableceu a conexión.
Cando se pulsa ENTER (na sesión con nc) , o servidor pecha a conexión. Entón sí que amosa o servidor unha mensaxe indicando que se estableceu unha conexión. De novo, cada vez que se introduce unha liña, envíase ó servidor a amósase a resposta recibida.
Cando se pulsa ENTER (na sesión de telnet) péchase tamén a conexión.
Como era esperable, este servidor só puede atender unha conexión de cliente á vez, e só pode atender un cliente tras rematar o anterior. Será preciso programar un servidor multifío que poida atender a varios clientes á vez, cada un nunha conexión.
Programación de servidores multifío baseados en TCP
Os motivos para desenvolver servidores baseados en UDP comparten con TCP un requisito fundamental: melloraro tempo de resposta. Por ejemplo, para un servidor web que presta os seus servizos a través de TCP, deséxase que calquier páxina solicitada por un cliente se empece a obter o antes posible; en TCP hai ademáis outra motivación, que é a posibilidade de manter varias sesións interactivas de manera simultánea con distintos clientes.
Non sería admisible que un usuario tivera que esperar a que terminaran a súa sesión os usuarios xa conectados; podería tardar moito tempo, ou incluso o usuario deixar a sesión aberta.
O que se fai é crear e lanzar un fío para atender cada conexión dun cliente.
Programa servidor de eco: Versión multifío
Esta crea un fío para atender cada conexión dun cliente, e lle pasa ó constructor o Socket devolto por accept. Desa forma o fió principal pode atender inmediatamente outras peticións de conexión de clientes.
É moi importante sacar a chamada a accept do bloque try con recursos no que está, que queda así vacío e hai que eliminar. Se non se fai isto, pecharíase o Socket antes incluso de que o fío puidera facer nada con el.
O código para atender a conexión do cliente copiase no método run . E o final deste método péchase o Socket con close nun bloque finally.
1 package socketstcp_servidormultihiloeco; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.io.OutputStream; 8 import java.io.OutputStreamWriter; 9 import java.io.BufferedReader; 10 import java.io.BufferedWriter; 11 class HiloServidor implements Runnable { 12 private final static String COD_TEXTO = “UTF-8”; 13 private final Socket socketComunicacion; 14 HiloServidor(Socket socketComunicacion) { 15 this.socketComunicacion = socketComunicacion; 16 } 17 @Override 18 public void run() { 19 try (InputStream isDeCliente = this.socketComunicacion.getInputStream(); 20 OutputStream osACliente = this.socketComunicacion.getOutputStream(); 21 InputStreamReader isrDeCliente = new InputStreamReader(isDeCliente,COD_TEXTO); 22 BufferedReader brDeCliente = new BufferedReader(isrDeCliente); 23 OutputStreamWriter oswACliente = new OutputStreamWriter(osACliente,COD_TEXTO); 24 BufferedWriter bwACliente = new BufferedWriter(oswACliente)) { 25 String lineaRecibida; 26 while ((lineaRecibida = brDeCliente.readLine()) != null && 27 lineaRecibida.length() > 0) { 28 bwACliente.write(“#” + lineaRecibida + “#”); 29 bwACliente.newLine(); 30 bwACliente.flush(); 31 } 32 } catch (IOException ex) { 33 System.out.println(“Excepción de E/S”); 34 ex.printStackTrace(); 35 System.exit(1); 36 } finally { 37 if (this.socketComunicacion != null) { 38 try { 39 this.socketComunicacion.close(); 40 System.out.println(“Cliente desconectado.”); 41 } catch (IOException ex) { 42 } 43 } 44 } 45 } 46 } 47 48 class SocketsTCP_ServidorMultihiloEco { 49 public static void main(String[] args) { 50 if (args.length < 1) { 51 System.err.println(“ERRO, indicar: porto.”); 52 System.exit(1); 53 } 54 int numPuerto = Integer.parseInt(args[0]); 55 try (ServerSocket socketServidor = new ServerSocket(numPuerto)) { 56 System.out.printf( 57 “Creado socket de servidor en porto %d. Esperando conexións.\n”, 58 numPuerto); 59 while (true) { // Acepta unha conexión de cliente tras outra 60 Socket socketComNuevoCliente = socketServidor.accept(); 61 System.out.printf(“Cliente conectado desde %s:%d.\n”, 62 socketComNuevoCliente.getInetAddress().getHostAddress(), 63 socketComNuevoCliente.getPort()); 64 Thread hiloSesion = new Thread( 65 new HiloServidor(socketComNuevoCliente)); 66 hiloSesion.start(); 67 } 68 } catch (IOException ex) { 69 System.out.println(“Excepción de E/S”); 70 ex.printStackTrace(); 71 System.exit(1); 72 } 73 } 74 }
A comprobación do funcionamento debería constatar que é posible establecer varias conexións, operando simultáneamente.