Diferencia entre revisiones de «Programación de comunicacións en rede con Java»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 51 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
 +
= 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 =
 
= 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:
+
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 baixo nivel''' que permiten representar as principales entidades '''dos niveis de enlace, rede e transporte''';  
−
# Clases de alto nivel relacionadas con protocolos de nivel de aplicación
+
# '''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.
+
Comezaremos revisando as clases para '''interfaces de rede e enderezos IP''', para despois progresar en algo central 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.
  
 
{| class="wikitable"
 
{| class="wikitable"
Línea 13: Línea 26:
 
|-
 
|-
 
|Enderezos IP
 
|Enderezos IP
−
|InetAddress: enderezo IP <br/> Inet4Address: enderezo IPv <br/> Inet6Address: enderezo IPv6 <br/> InterfaceAddress:enderezo IP máis información relativa á rede á que pertenece a interfaz
+
|InetAddress: enderezo IP <br/> Inet4Address: enderezo IPv4 <br/> Inet6Address: enderezo IPv6 <br/> InterfaceAddress:enderezo IP máis información relativa á rede á que pertenece a interfaz
 
|-
 
|-
 
|Sockets
 
|Sockets
Línea 20: Línea 33:
  
 
= Interfaces de rede =
 
= Interfaces de rede =
−
As interfaces de red presentes no sistema podense amosar cos comandos:
+
As '''interfaces de rede''' presentes no sistema podense '''amosar cos comandos''':
 
* ifconfig ou ip en Linux
 
* ifconfig ou ip en Linux
 
* ipconfig en Windows.
 
* ipconfig en Windows.
  
−
A clase NetworkInterface ten métodos estáticos que permiten obter unha enumeración de todas as interfaces de rede ou  
+
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.
 
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.
+
Unha 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).
+
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.
 
Na táboa explicanse alguns dos métodos de NetworkInterface.
Línea 41: Línea 54:
 
|-
 
|-
 
|String getDisplayName() <br/> String getName()
 
|String getDisplayName() <br/> String getName()
−
| Devuelven o nome da interfaz
+
| Devolven o nome da interfaz
 
|-
 
|-
 
| byte[] getHardwareAddress()
 
| byte[] getHardwareAddress()
Línea 50: Línea 63:
 
<br/> Un obxecto InterfaceAddress proporciona máis información que InetAddress. Por exemplo:
 
<br/> Un obxecto InterfaceAddress proporciona máis información que InetAddress. Por exemplo:
 
* a lonxitude da máscara de rede
 
* a lonxitude da máscara de rede
−
* o enderezo de broadcast para a redhe á que pertence a interfaz (para IPv4).
+
* o enderezo de broadcast para a rede á que pertence a interfaz (para IPv4).
 
|-
 
|-
 
| boolean isLoopback()
 
| boolean isLoopback()
−
| Determina se unha interfaz é de tipo loopback.
+
| Determina se unha interfaz é de tipo ''loopback''.
 
|}
 
|}
  
Línea 70: Línea 83:
  
 
= Enderezos IP =
 
= 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.
+
Unha '''interfaz de rede pode ter configurada un ou varios enderezos IPv4 e''' un ou varios enderezos '''IPv6'''; e cada un '''coas súas correspondentes máscaras de rede'''.
  
 
As clases que permiten xestionar esta información:
 
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:
+
* 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)
+
** '''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
+
** 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:
 
* InetAddress: Representa un enderezo IP. Ten dúas subclases para representar enderezos IPv4 e IPv6:
 
** Inet4Address
 
** Inet4Address
 
** Inet6Address  
 
** Inet6Address  
  
−
A clase InterfaceAddresss es muy sencilla y sus principales métodos se muestran en el cuadro 3.6. Para entender mejor su funcionamiento, hay que tener en cuenta que representa la conguración para IP de una interfaz de red. Por lo tanto, puede corresponder a cualquiera de las versiones de este protocolo, IPv4 o IPv6. Tiene un método para obtener la dirección IP y métodos adicionales para obtener otros parámetros de la conguración.
+
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 configuración'''.
  
 
{| class="wikitable"
 
{| class="wikitable"
−
|+Métodos da clase InterfaceAddress
+
|+ Métodos da clase InterfaceAddress
 
|-
 
|-
 
! Método
 
! Método
Línea 92: Línea 109:
 
|-
 
|-
 
| short getNetworkPrefixLength()
 
| short getNetworkPrefixLength()
−
| Devolve a lonxitude da máscara de rede (o número de bits dentro do enderezo IP que especifican a rede
+
| Devolve a lonxitude da máscara de rede (o número de bits dentro do enderezo IP que especifican a rede)
 
|-
 
|-
 
| InetAddress getBroadcast()
 
| 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
+
| Devolve o enderezo de broadcast para a rede á que pertence o enderezo IP (no caso de que se trate dun enderezo de IPv4) <br> Nota. 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 '''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 utilizarse as 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
+
A táboa amosa algúns métodos desta clase. Ten ademáis algúns '''métodos estáticos para resolución de nomes''' que se verán ne seguinte sección.
  
 
{| class="wikitable"
 
{| class="wikitable"
Línea 109: Línea 126:
 
|-
 
|-
 
| static InetAddress getByName(String host)
 
| 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)
+
| Obten unha InetAddress para o host. En host pódese proporcionar:
 +
* un enderezo IP (analízase e validase 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()
 
| String getHostAddress()
Línea 124: Línea 143:
 
|-
 
|-
 
| boolean isSiteLocalAddress()
 
| boolean isSiteLocalAddress()
−
| Determina se é enderezo é local ou privado. Os rangos de enderezos para redes privadas especifícanse na [https://tools.ietf.org/html/rfc1918 RFC 1918]. As redes privadas de clase C para IPv4 (as máis frecuentes) son 192.168.X.0/24, con 0 <= X <= 255
+
| Determina se é enderezo é local ou privado. <br> Os rangos de enderezos para redes privadas especifícanse na [https://tools.ietf.org/html/rfc1918 RFC 1918]. <br> As redes privadas de clase C para IPv4 (as máis frecuentes) son 192.168.X.0/24, con 0 <= X <= 255
 
|-
 
|-
 
| boolean isLoopbackAddress()
 
| boolean isLoopbackAddress()
Línea 131: Línea 150:
 
| static InetAddress getLoopbackAddress()
 
| static InetAddress getLoopbackAddress()
 
| Devolve o enderezo de loopback:
 
| Devolve o enderezo de loopback:
−
* 127.0.0.1 para IPv4 (en realidade segundo os estándares de internet é calquera da forma 127/8; isto é, cos 8 primeiros bits formando o valor 127, coa forma 127.*.*.*)
+
* 127.0.0.1 para IPv4 (segundo os estándares de internet calquera coa forma 127.*.*.*)
 
* ::1 para IPv6
 
* ::1 para IPv6
 
|}
 
|}
Línea 141: Línea 160:
  
 
{| class="wikitable"
 
{| class="wikitable"
−
|+ Métodos da clase InetAddress relacionados con resolución denomes
+
|+ Métodos da clase InetAddress relacionados con resolución de nomes
 
! Método
 
! Método
 
! Funcionalidade
 
! Funcionalidade
Línea 168: Línea 187:
  
 
= Sockets UDP =
 
= Sockets 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).
+
== 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.
 
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.
Línea 202: Línea 222:
 
|}
 
|}
  
−
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 puerto orixen é opcional; en caso de non ser utilizado, o porto orixen debe ser posto a cero.
+
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.  
  
−
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 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:
+
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:
 
   
 
   
 
{| class="wikitable"
 
{| class="wikitable"
Línea 218: Línea 240:
 
|-
 
|-
 
|InetAddress GetAddress() <br/> int getPort()
 
|InetAddress GetAddress() <br/> 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.
+
|Obteñen enderezos IP e porto desde onde se enviou o datagrama. <br> Poderían non estar informados. <br/> 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 dos procesos.
+
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'''.
 +
 
 +
{| class="wikitable"
 +
|+Métodos da clase DatagramSocket
 +
|-
 +
!Método
 +
!Funcionalidade
 +
|-
 +
| DatagramSocket() <br/> DatagramSocket(int port) <br/>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) <br/> 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
 +
|}
 +
 
 +
{| class="wikitable"
 +
|+Métodos da clase DatagramPacket
 +
|-
 +
!Método
 +
!Funcionalidade
 +
|-
 +
| DatagramPacket(byte[] buf, int length) <br/> DatagramPacket(byte[] buf, int offset, int length) <br/> DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) <br/> DatagramPacket(byte[] buf, int length, InetAddress address, int port)
 +
| Diversos constructores. Os parámetros corresponden a:
 +
* Secuencia de bytes que enviar no datagrama. Esta está no array de bytes buff. Hai que enviar un número de bytes dado por length
 +
* Enderezo IP e porto de destino, dados por address e port . Podense asignar tamén con setAddress(InetAddress iaddr) e setSocketAddress(SocketAddress address)
 +
|-
 +
|getAddress(), setAddress() <br/>
 +
getData(), setData() <br/>
 +
getLength(),setLength() <br/>
 +
getOffset(),setOffset() <br/>
 +
getPort(),setPort() <br/>
 +
getSocketAddress(),setSocketAddress() <br/>
 +
| 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:
 +
 
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
package udp_echoserver;
 +
import java.io.IOException;
 +
import java.net.DatagramPacket;
 +
import java.net.DatagramSocket;
 +
import java.net.InetAddress;
 +
import java.net.SocketException;
 +
public class UDP_EchoServer {
 +
  private final static int MAX_BYTES = 1400;
 +
  private final static String COD_TEXTO = “UTF-8”;
 +
  public static void main(String[] args) {
 +
    if (args.length < 1) {
 +
      System.err.println("ERROR, indicar: puerto.");
 +
      System.exit(1);
 +
    }
 +
    int numPuerto = Integer.parseInt(args[0]);
 +
    try(DatagramSocket serverSocket=new DatagramSocket(numPuerto)) {
 +
      System.out.printf(“Creado socket de datagramas para puerto %s.\n”,
 +
        numPuerto);
 +
      while (true) {
 +
        System.out.println("Esperando datagramas.");
 +
        byte[] datosRecibidos = new byte[MAX_BYTES];
 +
        DatagramPacket paqueteRecibido = new DatagramPacket(
 +
          datosRecibidos,datosRecibidos.length);
 +
        serverSocket.receive(paqueteRecibido);
 +
        String lineaRecibida = new String(paqueteRecibido.getData(),
 +
            0, paqueteRecibido.getLength(), COD_TEXTO);
 +
        InetAddress IPCliente = paqueteRecibido.getAddress();
 +
        int puertoCliente = paqueteRecibido.getPort();
 +
        System.out.printf("Recibido datagrama de %s:%d (%s)\n",
 +
          IPCliente.getHostAddress(), puertoCliente, lineaRecibida);
 +
        String respuesta = "#" + lineaRecibida + "#";
 +
        byte[] b = respuesta.getBytes(COD_TEXTO);
 +
        DatagramPacket paqueteEnviado = new DatagramPacket(
 +
          b,  b.length, IPCliente, puertoCliente);
 +
        serverSocket.send(paqueteEnviado);
 +
      }
 +
    } catch (SocketException ex) {
 +
      System.out.println("Excepción de sockets");
 +
      ex.printStackTrace();
 +
    } catch (IOException ex) {
 +
      System.out.println("Excepción de E/S");
 +
      ex.printStackTrace();
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 
 +
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.
 +
 
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
package udp_echoclient;
 +
import java.io.BufferedReader;
 +
import java.io.IOException;
 +
import java.io.InputStreamReader;
 +
import java.net.DatagramPacket;
 +
import java.net.DatagramSocket;
 +
import java.net.InetAddress;
 +
import java.net.SocketException;
 +
public class UDP_EchoClient {
 +
  private final static int MAX_BYTES = 1400;
 +
  private final static String COD_TEXTO =”UTF-8”;
 +
  public static void main(String[] args) {
 +
    if (args.length < 2) {
 +
      System.err.println("ERROR, indicar: host_servidor puerto");
 +
      System.exit(1);
 +
    }
 +
    String nomHost = args[0];
 +
    int numPuerto = Integer.parseInt(args[1]);
 +
    try (DatagramSocket clientSocket = new DatagramSocket();
 +
      InputStreamReader isrStdIn = new InputStreamReader(System.in,COD_TEXTO);
 +
      BufferedReader brStdIn = new BufferedReader(isrStdIn)) {
 +
      String lineaLeida;
 +
      System.out.println("Introducir líneas. Línea vacía para terminar.");
 +
      System.out.print("Línea>");
 +
      while ((lineaLeida = brStdIn.readLine()) != null && lineaLeida.length() > 0) {
 +
        InetAddress IPServidor = InetAddress.getByName(nomHost);
 +
        byte[] b = lineaLeida.getBytes(COD_TEXTO);
 +
        DatagramPacket paqueteEnviado = new DatagramPacket(b, b.length, IPServidor, numPuerto);
 +
        clientSocket.connect(IPServidor, numPuerto);
 +
        clientSocket.send(paqueteEnviado);
 +
        byte[] datosRecibidos = new byte[MAX_BYTES];
 +
        DatagramPacket paqueteRecibido =  new DatagramPacket(datosRecibidos, datosRecibidos.length);
 +
        clientSocket.receive(paqueteRecibido);
 +
        String respuesta = new String(paqueteRecibido.getData(), 0, paqueteRecibido.getLength(), COD_TEXTO);
 +
      System.out.printf("Datagrama recibido de %s:%d: %s\n", paqueteRecibido.getAddress().getHostAddress(),
 +
paqueteRecibido.getPort(), respuesta);
 +
        System.out.print("Línea>");
 +
      }
 +
    } catch (SocketException ex) {
 +
      System.out.println("Excepción de sockets");
 +
      ex.printStackTrace();
 +
    } catch (IOException ex) {
 +
      System.out.println("Excepción de E/S");
 +
      ex.printStackTrace();
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 
 +
=== 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 =
 
= 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.
 +
 +
{| class="wikitable"
 +
|+ Comunicación con sockets de TCP (clases ''ServerSocket'' e ''Socket'')
 +
! 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:
 +
* Se non se especifica o porto, deberá ligarse máis tarde a un porto co método bind.
 +
* Se se especifica o enderezo IP dunha interfaz activa (bindAddr) ligarase a ela. Se non os enderezos IP de todas as interfaces activas (''wildcard address'')
 +
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
 +
|}
 +
 +
 +
{| class="wikitable"
 +
|+ Métodos da clase ''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.
 +
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
package socketstcp_servidoreco;
 +
import java.net.ServerSocket;
 +
import java.net.Socket;
 +
import java.io.IOException;
 +
import java.io.InputStream;
 +
import java.io.InputStreamReader;
 +
import java.io.OutputStream;
 +
import java.io.OutputStreamWriter;
 +
import java.io.BufferedReader;
 +
import java.io.BufferedWriter;
 +
public class SocketsTCP_ServidorEco {
 +
  private final static String COD_TEXTO =”UTF-8”;
 +
  public static void main(String[] args) {
 +
    if (args.length < 1) {
 +
      System.err.println(“ERROR, indicar: porto.”);
 +
      System.exit(1);
 +
    }
 +
    int numPuerto = Integer.parseInt(args[0]);
 +
    try (ServerSocket socketServidor = new ServerSocket(numPuerto)) {
 +
      System.out.printf("Creado socket de servidor en porto %d. Esperando conexións de clientes.\n",
 +
        numPuerto);
 +
   
 +
      while (true) {    // Acepta unha conexión de cliente tras outra
 +
        try (Socket socketComunicacion = socketServidor.accept()) {
 +
          System.out.printf("Cliente conectado desde %s:%d.\n", socketComunicacion.getInetAddress().getHostAddress(), socketComunicacion.getPort());
 +
 +
          try (InputStream isDeCliente = socketComunicacion.getInputStream();
 +
              OutputStream osACliente = socketComunicacion.getOutputStream();
 +
              InputStreamReader isrDeCliente = new InputStreamReader(isDeCliente, COD_TEXTO);
 +
              BufferedReader brDeCliente = new BufferedReader(isrDeCliente);
 +
              OutputStreamWriter oswACliente = new OutputStreamWriter(osACliente, COD_TEXTO);
 +
              BufferedWriter bwACliente = new BufferedWriter(oswACliente)) {
 +
            String lineaRecibida;
 +
            while ((lineaRecibida = brDeCliente.readLine()) != null && lineaRecibida.length() > 0) {
 +
              bwACliente.write("#" + lineaRecibida + "#");
 +
              bwACliente.newLine();
 +
              bwACliente.flush();
 +
            }
 +
          }
 +
        }
 +
        System.out.println("Cliente desconectado.");
 +
     
 +
}
 +
   
 +
} catch (IOException ex) {
 +
      System.out.println("Excepción de E/S");
 +
      ex.printStackTrace();
 +
      System.exit(1);
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
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.
 +
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
package socketstcp_clienteeco;
 +
import java.io.BufferedReader;
 +
import java.io.BufferedWriter;
 +
import java.io.IOException;
 +
import java.io.InputStream;
 +
import java.io.InputStreamReader;
 +
import java.io.OutputStream;
 +
import java.io.OutputStreamWriter;
 +
import java.net.Socket;
 +
import java.net.UnknownHostException;
 +
public class SocketsTCP_ClienteEco {
 +
  private final static String COD_TEXTO ="UTF-8";
 +
  public static void main(String[] args) {
 +
    if (args.length < 2) {
 +
      System.err.println("ERRO, indicar: host_servidor porto");
 +
      System.exit(1);
 +
    }
 +
    String nomHost = args[0];
 +
    int numPuerto = Integer.parseInt(args[1]);
 +
    try (Socket socketComunicacion = new Socket(nomHost, numPuerto)) {
 +
      System.out.println(“Conectado a servidor.”);
 +
      try (OutputStream osAServidor = socketComunicacion.getOutputStream();
 +
          InputStream isDeServidor = socketComunicacion.getInputStream();
 +
          InputStreamReader isrDeServidor = new InputStreamReader(isDeServidor, COD_TEXTO);
 +
          OutputStreamWriter oswAServidor = new OutputStreamWriter(osAServidor, COD_TEXTO);
 +
          BufferedWriter bwAServidor = new BufferedWriter(oswAServidor);
 +
          BufferedReader brDeServidor = new BufferedReader(isrDeServidor);
 +
          InputStreamReader isrStdIn = new InputStreamReader(System.in);
 +
          BufferedReader brStdIn = new BufferedReader(isrStdIn)) {
 +
        String lineaLeida;
 +
        System.out.println("Introducir líñas. Líña vacía para terminar.");
 +
        System.out.print("Liña>");
 +
        while ((lineaLeida = brStdIn.readLine()) != null && lineaLeida.length() > 0) {
 +
          bwAServidor.write(lineaLeida);
 +
          bwAServidor.newLine();
 +
          bwAServidor.flush();
 +
          System.out.println("resposta: " + brDeServidor.readLine());
 +
          System.out.print("Liña>");
 +
        }
 +
      }
 +
    } catch (UnknownHostException e) {
 +
      System.err.println("Host descoñecido: " + nomHost);
 +
      System.exit(1);
 +
    } catch (IOException ex) {
 +
      System.err.println("Excepción de E/S");
 +
      ex.printStackTrace();
 +
      System.exit(1);
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
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:
 +
 +
{| class="wikitable"
 +
|+ Interacción de clientes cun servidor dun só fío
 +
! 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.
 +
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
package socketstcp_servidormultihiloeco;
 +
import java.net.ServerSocket;
 +
import java.net.Socket;
 +
import java.io.IOException;
 +
import java.io.InputStream;
 +
import java.io.InputStreamReader;
 +
import java.io.OutputStream;
 +
import java.io.OutputStreamWriter;
 +
import java.io.BufferedReader;
 +
import java.io.BufferedWriter;
 +
class HiloServidor implements Runnable {
 +
  private final static String COD_TEXTO = “UTF-8”;
 +
  private final Socket socketComunicacion;
 +
  HiloServidor(Socket socketComunicacion) {
 +
    this.socketComunicacion = socketComunicacion;
 +
  }
 +
  @Override
 +
  public void run() {
 +
try (InputStream isDeCliente = this.socketComunicacion.getInputStream();
 +
        OutputStream osACliente = this.socketComunicacion.getOutputStream();
 +
        InputStreamReader isrDeCliente = new InputStreamReader(isDeCliente,COD_TEXTO);
 +
        BufferedReader brDeCliente = new BufferedReader(isrDeCliente);
 +
        OutputStreamWriter oswACliente = new OutputStreamWriter(osACliente,COD_TEXTO);
 +
        BufferedWriter bwACliente = new BufferedWriter(oswACliente)) {
 +
      String lineaRecibida;
 +
      while ((lineaRecibida = brDeCliente.readLine()) != null &&
 +
          lineaRecibida.length() > 0) {
 +
        bwACliente.write(“#” + lineaRecibida + “#”);
 +
        bwACliente.newLine();
 +
        bwACliente.flush();
 +
      }
 +
    } catch (IOException ex) {
 +
      System.out.println(“Excepción de E/S”);
 +
      ex.printStackTrace();
 +
      System.exit(1);
 +
    } finally {
 +
      if (this.socketComunicacion != null) {
 +
        try {
 +
          this.socketComunicacion.close();
 +
          System.out.println(“Cliente desconectado.”);
 +
        } catch (IOException ex) {
 +
        }
 +
      }
 +
    }
 +
  }
 +
}
 +
 +
class SocketsTCP_ServidorMultihiloEco {
 +
  public static void main(String[] args) {
 +
    if (args.length < 1) {
 +
      System.err.println(“ERRO, indicar: porto.”);
 +
      System.exit(1);
 +
    }
 +
    int numPuerto = Integer.parseInt(args[0]);
 +
    try (ServerSocket socketServidor = new ServerSocket(numPuerto)) {
 +
      System.out.printf(
 +
        “Creado socket de servidor en porto %d. Esperando conexións.\n”,
 +
          numPuerto);
 +
      while (true) {    // Acepta unha conexión de cliente tras outra
 +
        Socket socketComNuevoCliente = socketServidor.accept();
 +
        System.out.printf(“Cliente conectado desde %s:%d.\n”,
 +
          socketComNuevoCliente.getInetAddress().getHostAddress(),
 +
          socketComNuevoCliente.getPort());
 +
        Thread hiloSesion = new Thread(
 +
          new HiloServidor(socketComNuevoCliente));
 +
        hiloSesion.start();
 +
      }
 +
    } catch (IOException ex) {
 +
      System.out.println(“Excepción de E/S”);
 +
      ex.printStackTrace();
 +
      System.exit(1);
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
A comprobación do funcionamento debería constatar que é posible establecer varias conexións, operando simultáneamente.
 +
 +
== Programación distribuída baseada en TCP ==
  
 
= Creditos e referencias=
 
= Creditos e referencias=
 
* [https://www.sintesis.com/desarrollo%20de%20aplicaciones%20multiplataforma-341/programaci%C3%B3n%20de%20servicios%20y%20procesos-ebook-2910.html Programación de servicios y procesos. Carlos Alberto Cortijo Bon ISBN: 9788413571041]
 
* [https://www.sintesis.com/desarrollo%20de%20aplicaciones%20multiplataforma-341/programaci%C3%B3n%20de%20servicios%20y%20procesos-ebook-2910.html Programación de servicios y procesos. Carlos Alberto Cortijo Bon ISBN: 9788413571041]
 +
 +
== Sockets ==
 +
<div style="column-count:2;-moz-column-count:2;-webkit-column-count:2">
 +
* [https://docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html Reading from and Writing to a Socket]
 +
* [https://www.cs.uic.edu/~troy/spring05/cs450/sockets/socket.html Socket Examples]
 +
</div>
 +
 +
=== Exemplo de xogo de adiviñar números ===
 +
<div style="column-count:2;-moz-column-count:2;-webkit-column-count:2">
 +
* [https://programmerclick.com/article/5830172001/ Ejemplo de juego de números de adivinanzas de operaciones de sockets multiproceso]
 +
* [http://ayudavariables.blogspot.com/2013/08/secreto.html?m=1 Ejemplo Cliente/Servidor número secreto]
 +
* [https://github.com/RNogales94/Practicas-Redes/blob/master/P2%20-%20Cliente%20:%20Servidor/Ejercicio%205/src/fr/practica2/pkg5/ClienteAdivinador.java Otro Cliente/Servidor número secreto]
 +
</div>
 +
 +
= Tarefas =
 +
* [[Detectar portos abertos]]
 +
* [[Prácticas con NetworkInterface]]
 +
* [[Prácticas con InetAddress]]
 +
* [[Prácticas con scokets]]

Revisión actual del 19:26 23 nov 2021

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:

  1. Clases de baixo nivel que permiten representar as principales entidades dos niveis de enlace, rede e transporte;
  2. 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 algo central 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.

Clases de baixo nivel do paquete java.net
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 rede 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.

Unha 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étodos da clase 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()
Devolven 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.


Un obxecto InterfaceAddress proporciona máis información que InetAddress. Por exemplo:

  • a lonxitude da máscara de rede
  • o enderezo de broadcast para a rede á que pertence a interfaz (para IPv4).
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 coas súas 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 configuración.

Métodos da clase InterfaceAddress
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() Devolve o enderezo de broadcast para a rede á que pertence o enderezo IP (no caso de que se trate dun enderezo de IPv4)
Nota. 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 utilizarse as 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 nomes que se verán ne seguinte sección.

Métodos da clase InetAddress
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 validase 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:
  • 127.0.0.1 para IPv4 (segundo os estándares de internet calquera coa forma 127.*.*.*)
  •  ::1 para IPv6

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étodos da clase InetAddress relacionados con resolución de nomes
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.


Campos do datagrama UDP
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étodos getter da clase DatagramPacket
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étodos da clase DatagramSocket
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étodos da clase DatagramPacket
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:
  • Secuencia de bytes que enviar no datagrama. Esta está no array de bytes buff. Hai que enviar un número de bytes dado por length
  • Enderezo IP e porto de destino, dados por address e port . Podense asignar tamén con setAddress(InetAddress iaddr) e setSocketAddress(SocketAddress address)
getAddress(), setAddress()

getData(), setData()
getLength(),setLength()
getOffset(),setOffset()
getPort(),setPort()
getSocketAddress(),setSocketAddress()

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: 
  1. Obter a secuencia de bytes para un String s con s.getBytes("UTF-8") .
  2. 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.

Comunicación con sockets de TCP (clases ServerSocket e Socket)
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:
  • Se non se especifica o porto, deberá ligarse máis tarde a un porto co método bind.
  • Se se especifica o enderezo IP dunha interfaz activa (bindAddr) ligarase a ela. Se non os enderezos IP de todas as interfaces activas (wildcard address)
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étodos da clase 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", socketComunicacion.getInetAddress().getHostAddress(), socketComunicacion.getPort());
26  
27           try (InputStream isDeCliente = socketComunicacion.getInputStream();
28               OutputStream osACliente = socketComunicacion.getOutputStream();
29               InputStreamReader isrDeCliente = new InputStreamReader(isDeCliente, COD_TEXTO);
30               BufferedReader brDeCliente = new BufferedReader(isrDeCliente);
31               OutputStreamWriter oswACliente = new OutputStreamWriter(osACliente, COD_TEXTO);
32               BufferedWriter bwACliente = new BufferedWriter(oswACliente)) {
33             String lineaRecibida;
34             while ((lineaRecibida = brDeCliente.readLine()) != null && lineaRecibida.length() > 0) {
35               bwACliente.write("#" + lineaRecibida + "#");
36               bwACliente.newLine();
37               bwACliente.flush();
38             }
39           }
40         }
41         System.out.println("Cliente desconectado.");
42       
43 }
44     
45 } catch (IOException ex) {
46       System.out.println("Excepción de E/S");
47       ex.printStackTrace();
48       System.exit(1);
49     }
50   }
51 }

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 = new InputStreamReader(isDeServidor, COD_TEXTO);
25           OutputStreamWriter oswAServidor = new OutputStreamWriter(osAServidor, COD_TEXTO);
26           BufferedWriter bwAServidor = new BufferedWriter(oswAServidor);
27           BufferedReader brDeServidor = new BufferedReader(isrDeServidor);
28           InputStreamReader isrStdIn = new InputStreamReader(System.in);
29           BufferedReader brStdIn = new BufferedReader(isrStdIn)) {
30         String lineaLeida;
31         System.out.println("Introducir líñas. Líña vacía para terminar.");
32         System.out.print("Liña>");
33         while ((lineaLeida = brStdIn.readLine()) != null && lineaLeida.length() > 0) {
34           bwAServidor.write(lineaLeida);
35           bwAServidor.newLine();
36           bwAServidor.flush();
37           System.out.println("resposta: " + brDeServidor.readLine());
38           System.out.print("Liña>");
39         }
40       }
41     } catch (UnknownHostException e) {
42       System.err.println("Host descoñecido: " + nomHost);
43       System.exit(1);
44     } catch (IOException ex) {
45       System.err.println("Excepción de E/S");
46       ex.printStackTrace();
47       System.exit(1);
48     }
49   }
50 }

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:

Interacción de clientes cun servidor dun só fío
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.

Programación distribuída baseada en TCP

Creditos e referencias

Sockets

Exemplo de xogo de adiviñar números

Tarefas