Datagramas cliente-servidor
El ejemplo generado en esta sección está comprendido por dos aplicaciones: un cliente y un
servidor. El servidor recibe continuamente paquetes de datagramas a través de un socket
datagramas. Cada paquete recibido por el servidor indica una petición del cliente de una cita
famosa. Cuando el servidor recibe un datagrama, le responde enviando un datagrama que contiene
un texto de sólo una línea que contiene "la cita del momento" al cliente.
La aplicación cliente de este ejemplo es muy sencilla -- envía un datagrama al servidor que
indica que le gustaría recibir una cita del momento. Entonces el cliente espera a que el
servidor le envíe un datagrama en respuesta.
Dos clases implementan la aplicación servidor: QuoteServer y QuoteServerThread. Una sóla clase
implementa la aplicación cliente: QuoteClient.
Investiguemos estas clases empezando con la clase que contiene el método
main() de la aplicación servidor.
Contiene una versión de applet de la clase QuoteClient.
La Clase QuoteServer
La clase QuoteServer contiene un sólo método: el método main() para la
aplicación servidor de citas. El método main() sólo crean un nuevo objeto
QuoteServerThread y lo arranca.
class QuoteServer {
public static void main(String[] args) {
new QuoteServerThread().start();
}
}
El objeto QuoteServerThread implementa la lógica principal del servidor de citas.
La Clase QuoteServerThread
La clase QuoteServerThread es
un Thread que se ejecuta contínuamente esperando peticiones a través del un socket de datagramas.
QuoteServerThread tiene dos variables de ejemplar privadas. La primera, llamada
socket, es una referencia a un objeto DatagramSocket object. Esta variable
se inicializa a null. La segunda, qfs, es un objeto DataInputStream que se
ha abierto sobre un fichero de texto ASCII que contiene una lista de citas. Cada vez que se
llegue al servidor una petición de cita, el servidor recupera la sigueinte línea desde el
stream de entrada.
Cuando el programa principal crea el QuoteServerThread que utiliza el único constructor
disponible.
QuoteServerThread() {
super("QuoteServer");
try {
socket = new DatagramSocket();
System.out.println("QuoteServer listening on port: " + socket.getLocalPort());
} catch (java.net.SocketException e) {
System.err.println("Could not create datagram socket.");
}
this.openInputFile();
}
La primera línea de este cosntructor llama al constructor de la superclase (Thread) para
inicializar el thread con el nombre "QuoteServer". La siguiente sección de código es la parte
crítica del constructor de QuoteServerThread -- crea un DatagramSocket. El QuoteServerThread
utiliza este DatagramSocket para escuchar y responder las peticiones de citas de los clientes.
El socket es creado utilizando el constructor de DatagramSocket que no requiere arguementos.
socket = new DatagramSocket();
Una vez creado usando este constructor, el nuevo DatagramSocket se asigna a algún puerto local
disponible. La clase DatagramSocket tiene otro constructor que permite especificar el puerto
que quiere utilizar para asignarle el nuevo objeto DatagramSocket. Deberías observar que ciertos
puertos estás dedicados a servicios "bien-conocidos" y que no pueden ser utilizados. Si se
especifica un puerto que está siendo utilizado, fallará la creación del DatagramSocket.
Después de crear con éxito el DatagramSocket, el QuoteServerThread muestra un mensaje indicando
el puerto al que se ha asignado el DatagramSocket. El QuoteClient necesita este número de puerto
para construir los paquetes de datagramas destinados a este puerto. Por eso, se debe utilizar
este número de puerto cuando
ejecute el QuoteClient.
La última línea del constructor de QuoteServerThread llama a un método privado,
openInputFile(), dentro de QuoteServerThread para abrir un fichero llamado
one-liners.txt que
contiene una lista de citas. Cada cita del fichero debe ser un línea en sí misma.
Ahora la parte interesante de
QuoteServerThread -- es el
método run(). (El método run() sobreescribe el método
run() de la clase Thread y proporciona la implementación del thread. Para
información sobre los Threads, puedea ver
Threads de Control.
El método run() QuoteServerThread primero comprueba que se ha creado un
objeto DatagramSocket válido durante su construcción. Si socket es null,
entonces el QuoteServerThread no podría desviar el DatagramSocket. Sin el socket, el servidor
no puede operar, y el método run() retorna.
De otra forma, el método run() entra en un bucle infinito. Este bucle
espera continuamente las peticiones de los clientes y responde estas peticiones. Este bucle
contiene dos secciones críticas de código: la sección que escucha las peticiones y la que las
responde, primero veremos la sección que recibe la peticiones.
packet = new DatagramPacket(buf, 256);
socket.receive(packet);
address = packet.getAddress();
port = packet.getPort();
La primera línea de código crea un nuevo objeto DatagramPacket encargado de recibir un datagrama
a través del socket. Se puede decir que el nuevo DatagramPacket está encargado de recibir datos
desde el socket debido al constructor utilizado para crearlo. Este constructor requiere sólo dos
argumentos, un array de bytes que contiene los datos específicos del cliente, y la longitud de
este array. Cuando se construye un DatagramPacket para enviarlo a través de un DatagramSocket,
también debe suministrar la dirección de internet y el puerto de destino del paquete. Verás esto
más adelante cuando expliquemos cómo responde un servidor a las peticiones del cliente.
La segunda línea de código recibe un datagrama desde el socket. La información contenida dentro
del mensaje del datagrama se copia en el paquete creado en la línea anterior. El método
receive() se bloquea hasta que se reciba un paquete. Si no se recibe ningún
paquete, el servidor no hace ningún progreso y simplemente espera.
Las dos líneas siguientes obtienen la dirección de internet y el número de puerto desde el que
se ha recibido el datagrama. La dirección Internet y el número de puerto indicado de donde vino
el paquete. Este es donde el servidor debe responder. En este ejemplo, el array de bytes del
datagrama no contiene información relevante. Sólo la llegada del paquete indica una petición por
parte del cliente que puede ser encontrado en la dirección de Internet y el número de puertos
indicados en el datagrama.
En este punto, el servidor ha recibido un petición de una cita desde un cliente. Ahora el
servidor debe responder. Las seis líneas de código siguientes construyen la respuesta y la
envian.
if (qfs == null)
dString = new Date().toString();
else
dString = getNextQuote();
dString.getBytes(0, dString.length(), buf, 0);
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
Si el fichero de citas no se puede abrir por alguna razón, qfs es null. En
este caso, el servidor de citas sirve la hora del día en su lugar. De otra forma, el servidor de
citas obtiene la siguiente cita del fichero abierto. La línea de código de la sentencia
if convierte la cadena en un array de bytes.
La tercera línea de código crea un nuevo objeto DatagramPacket utilizado para enviar el mensaje
a través del socket del datagrama. Se puede decir que el nuevo DatagramPacket está destinado a
enviar los datos a través del socket porque el constructor lo utiliza para eso. Este cosntructor
requiere cuatro argumentos. El primer argumento es el mismo que el utilizado por el constructor
utilizado para crear los datagramas receptores: un array de bytes que contiene el mensaje del
emisor al receptor y la longitud de este array. Los dos siguientes argumentos son diferentes:
una dirección de Internet y un número de puerto. Estos dos argumentos son la dirección completa
del destino del datagrama y debe ser suministrada por el emisor del datagrama.
La cuarta línea de código envía el DatagramPacket de esta forma.
El último método de interés de QuoteServerThread es el método finalize().
Este método hace la limpieza cuando el QuoteServerThread recoge la basura cerrando el
DatagramSocket. Los puertos son recursos limitados y los sockets asignados a un puerto deben
cerrarse cuando no se utilizan.
La Clase QuoteClient
La clase QuoteClient implementa una
aplicación cliente para el QuoteServer. Esta aplicación sólo envía una petición al QuoteServer,
espera una respuesta, y cuando ésta se recibe la muestra en la salida estandard. Echemos un
vistazo al código.
La clase QuoteClient contiene un método -- el método main() para la
aplicación cliente. La parte superior de main() declara varias variables
locales para su utilización.
int port;
InetAddress address;
DatagramSocket socket = null;
DatagramPacket packet;
byte[] sendBuf = new byte[256];
La siguiente sección procesa los argumentos de la línea de comandos utilizados para invocar la
aplicación QuoteClient.
if (args.length != 2) {
System.out.println("Usage: java DatagramClient <hostname> <port#>");
return;
}
Esta aplicación requiere dos argumentos: el nombre de la máquina en la que se está ejecutando
QuoteServer, y el número de puerto por que el QuoteServer está escuchando. Cuando arranca el
QuoteServer muestra un número de puerto. Este es el número de puerto que debe utilizar en la
línea de comandos cuando arranque QuoteClient.
Luego, el método main() contiene un bloque try que
contiene la lógica principal del programa cliente. Este bloque try contiene
tres secciones principales: una sección que crea un DatagramSocket, una sección que envía una
petición al servidor, y una sección que obtiene la respuesta del servidor.
Primero veremos el código que crea un DatagramSocket.
socket = new DatagramSocket();
El cliente utiliza el mismo constructor para crear un DatagramSocket que el servidor. El
DatagramSocket es asignado a cualquier puerto disponible.
Luego, el programa QuoteClient envía una petición al servidor.
address = InetAddress.getByName(args[0]);
port = Integer.parseInt(args[1]);
packet = new DatagramPacket(sendBuf, 256, address, port);
socket.send(packet);
System.out.println("Client sent request packet.");
La primera línea de código obtiene la dirección Internet del host nombrado en la línea de
comandos. La segunda línea de código obtiene el número de puerto de la línea de comandos. Estas
dos piezas de información son utilizadas para crear un DatagramPacket destinado a esa dirección
de Internet y ese número de puerto. La dirección Internet y el número de puertos deberían
indicar la máquina en la que se arrancó el servidor y el puerto por el que el servidor está
escuchando.
La tercera línea del código anterior crea un DatagramPacket utilizado para envíar datos. El
paquete está construido con un array de bytes vacíos, su longitud, y la dirección Internet y el
número de puerto de destino del paquete. El array de bytes está vacío porque este datagrama sólo
pide la información del servidor. Todo lo que el servidor necesita saber para responder -- la
dirección y el número de puerto donde responder -- es una parte automática del paquete.
Luego, el cliente obtiene una respuesta desde el servidor.
packet = new DatagramPacket(sendBuf, 256);
socket.receive(packet);
String received = new String(packet.getData(), 0);
System.out.println("Client received packet: " + received);
Para obtener una respuesta del servidor, el cliente crea un paquete receptor y utiliza el
método receive() del DatagramSocket para recibir la respuesta del servidor.
El método receive() se bloquea hasta que un datagrama destinado al cliente
entre a través del socket. Observa que si, por alguna razón, se pierde la respuesta del servidor,
el cliente quedará bloqueado débido a la política de no garantías del modelo de datagrama.
Normalmente, un cliente selecciona un tiempo para no estár esperando eternamente una respuesta
-- si la respuesta no llega, el temporizador se cumple, y el servidor retransmite la petición.
Cuando el cliente recibe una respuesta del servidor, utiliza el método
getData() para recuperar los datos del paquete. El cliente convierte los
datos en una cadena y los muestra.
Ejecutar el Servidor
Después de haber compilado con éxito los programas cliente y servidor, puedes ejecutarlos.
Primero debes ejecutar el servidor porque necesitas conocer el número de puerto que muestra
antes de poder arrancar el cliente. Cuando el servidor asigna con éxito su DatagramSocket,
muestra un mensaje similar a este.
QuoteServer listening on port: portNumber
portNumber es el número de puerto al que está
asignado el DatagramSocket. Utiliza este número para arrancar el cliente.
Ejecutar el Cliente
Una vez que has arrancado el servidor y mostrado el mensaje que indica el puerto en el que está
escuchando, puedes ejecutar el programa cliente. Recuerda ejecutar el programa cliente con dos
argumentos en la línea de comandos: el nombre del host en el se está ejecutando el QuoteServer,
y el número de puerto que mostró al arrancar.
Después de que el cliente envíe una petición y reciba una respuesta desde el servidor,
deberías ver una salida similar a ésta.
Quote of the Moment: Life is wonderful. Without it we'd all be dead.