Escribir el Lado del Servidor de un Socket

Esta sección le muestra cómo escribir el lado del servidor de una conexión socket, con un ejemplo completo cliente-servidor. El servidor en el pareja cliente/servidor sirve bromas "Knock Knock". Las bromas Knock Knock son las favoritas por los niños pequeños y que normalmente son vehículos para malos juegos de palabras. Son de esta forma:

Servidor: "Knock knock!"
Cliente: "¿Quién es?"
Servidor: "Dexter."
Cliente: "¿Qué Dexter?"
Servidor: "La entrada de Dexter con ramas de acebo."
Cliente: "Gemido."

El ejemplo consiste en dos programas Java independientes ejecutandose: el programa cliente y el servidor. El programa cliente está implementado por una sóla clase KnockKnockClient, y está basado en el ejemplo EchoTest de la página anterior. El programa servidor está implementado por dos clases: KnockKnockServer y KKState. KnockKnockServer contiene el método main() para el program servidor y realiza todo el trabajo duro, de escuchar el puerto, establecer conexiones, y leer y escribir a través del socket. KKState sirve la bromas: sigue la pista de la broma actual, el estado actual (enviar konck knock, enviar pistas, etc...) y servir varias piezas de texto de la broma dependiendo del estado actual. Esta página explica los detalles de cada clase en estos programas y finalmente le muestra cómo ejecutarlas.

El servidor Knock Knock

Esta sección pasa a través del código que implemente el programa servidor Knock Knock, Aquí tienes el código fuente completo de la clase KnockKnockServer.class. El programa servidor empieza creando un nuevo objeto ServerSocket para escuchar en un puerto específico. Cuando escriba un servidor, debería elegir un puerto que no estuviera ya dedicado a otro servicio, KnockKnockServer escucha en el puerto 4444 porque sucede que el 4 es mi número favorito y el puerto 4444 no está siendo utilizado por ninguna otra cosa en mi entorno:
try {
    serverSocket = new ServerSocket(4444);
} catch (IOException e) {
    System.out.println("Could not listen on port: " + 4444 + ", " + e);
    System.exit(1);
}
ServerSocket es una clase java.net que proporciona una implementación independientes del sistema del lado del servidor de una conexión cliente/servidor. El constructor de ServerSocket lanza una excepción por alguna razón (cómo que el puerto ya está siendo utilizado) no puede escuchar en el puerto especificado. En este caso, el KnockKnockServer no tiene elección pero sale.

Si el servidor se conecta con éxito con su puerto, el objeto ServerSocket se crea y el servidor continua con el siguiente paso, que es aceptar una conexión desde el cliente.

Socket clientSocket = null;
try {
    clientSocket = serverSocket.accept();
} catch (IOException e) {
    System.out.println("Accept failed: " + 4444 + ", " + e);
    System.exit(1);
}
El método accept() se bloquea (espera) hasta que un cliente empiece y pida una conexión el puerto (en este caso 4444) que el servidor está escuchando. Cuando el método accept() establece la conexión con éxito con el cliente, devuelve un objeto Socket que apunta a un puerto local nuevo. El servidor puede continuar con el cliente sobre este nuevo Socket en un puerto diferente del que estaba escuchando originalmente para las conexiones. Por eso el servidor puede continuar escuchando nuevas peticiones de clientes a través del puerto original del ServerSocket. Esta versión del programa de ejemplo no escucha más peticiones de clientes. Sin embargo, una versión modificada de este programa, porpocionada más adelante, si lo hace.

El código que hay dentro del siguiente bloque try implememte el lado del servidor de una comunicación con el cliente. Esta sección del servidor es muy similar al lado del cliente (que vió en el ejemplo de la página anterior y que verá más adelante en el ejemplo de la clase KnockKnockClient):

  • Abre un stream de entrada y otro de salida sobre un socket.
  • Lee y escribe a través del socket.

Empecemos con las primeras 6 líneas:

DataInputStream is = new DataInputStream(
                 new BufferedInputStream(clientSocket.getInputStream()));
PrintStream os = new PrintStream(
             new BufferedOutputStream(clientSocket.getOutputStream(), 1024), false);
String inputLine, outputLine;
KKState kks = new KKState();
Las primeras dos líneas del código abren un stream de entrada sobre el socket devuelto por el método accept(). Las siguiente dos líneas abren un stream de salida sobre el mismo socket. La siguiente línea declara y crea un par de strings locales utilizadas para leer y escribir sobre el socket. Y finalmente, la última línea crea un objeto KKState. Este es el objeto que sigue la pista de la broma actual, el estado actual dentro de una broma, etc.. Este objeto implementa el protocolo -- el lenguaje que el cliente y el servidor deben utilizar para comunicarse.

El servidor es el primero en hablar, con estas líneas de código:

outputLine = kks.processInput(null);
os.println(outputLine);
os.flush();
La primera línea de código obtiene del objeto KKState la primera línea que el servidor le dice al cliente. Por ejemplo lo primero que el servidor dice es "Knock! Knock!".

Las siguientes dos líneas escriben en el stream de salida conectado al socket del cliente y vacía el stream de salida. Esta secuencia de código inicia la conversación entre el cliente y el servidor.

La siguiente sección de código es un bucle que lee y escribe a través del socket enviando y recibiendo mensajess entre el cliente y el servidor mientras que tengan que decirse algo el uno al otro. Como el servidor inicia la conversación con un "Knock! Knock!", el servidor debe esperar la respuesta del cliente. Así el bucle while itera y lee del stream de entrada. El método readLine() espera hasta que el cliente respondan algo escribiendo algo en el stream de salida (el stream de entrada del servidor). Cuando el cliente responde, el servidor pasa la respuesta al objeto KKState y le pide a éste una respuesta adecuada. El servidor inmediatamente envía la respuesta al cliente mediante el stream de salida conectado al socket, utilizando las llamadas a println() y flush(). Si la respuesta del servidor generada por el objeto KKState es "Bye.", indica que el cliente dijo que no quería más bromas y el bucle termina.

while ((inputLine = is.readLine()) != null) {
    outputLine = kks.processInput(inputLine);
    os.println(outputLine);
    os.flush();
    if (outputLine.equals("Bye."))
        break;
}
La clase KnockKnockServer es un servidor de buen comportamiento, ya que las últimas líneas de esta sección realizan la limpieza cerrando todas los streams de entrada y salida, el socket del cliente, y el socket del servidor.
os.close();
is.close();
clientSocket.close();
serverSocket.close();

El Protocolo Knock Knock

La clase KKState implementa el protocolo que deben utilizar el cliente y el servidor para comunicarse. Esta clase sigue la pista de dónde están el cliente y el servidor en su comunicación y sirve las respuestas del servidor a las setencias del cliente. El objeto KKState contiene el texto de todos las bromas y se asegura de que el servidor ofrece la respuesta adecuada a las frases del cliente. No debería decir el cliente "¿Qué Dexter?" cuando el servidor dice "Knock! Knock!".

Todos las parejas cliente-servidor deden tener algún protocolo en el que hablar uno con otro, o el significado de los datos que se pasan unos a otro. El protocolo que utilicen sus clientes y servidores dependen enteramente de la comunicación requerida por ellos para realizar su tarea.

El Cliente Knock Knock

La clase KnockKnockClient implementa el programa cliente que habla con KnockKnockServer. KnockKnockClient está basado en el programa EchoTest de la página anterior y debería serte familiar. Pero echemos un vistazo de todas formas para ver lo que sucede en el cliente, mientras tenemos en mente lo que sucedía en el servidor.

Cuando arranca el program cliente, el servidor debería estar ya ejecutándose y escuchando el puerto esperando un petición de conexión por parte del cliente.

kkSocket = new Socket("taranis", 4444);
os = new PrintStream(kkSocket.getOutputStream());
is = new DataInputStream(kkSocket.getInputStream());

Así, lo primero que hace el programa cliente es abrir un socket sobre el puerto en el que está escuchando el servidor en la máquina en la que se está ejecutando el servidor. El programa ejemplo KnockKnockClient abre un socket sobre el puerto 4444 que el mismo por el que está escuchando el servidor. KnockKnockClient utiliza el nombre de host taranis, que es el nombre de una máquina (hipotética) en tu red local. Cuando teclees y ejecutes este programa en tu máquina, deberías cambiar este nombre por el de una máquina de tu red. Esta es la máquina en la ejecutará KnockKnockServer.

Luego el cliente abre un stream de entrada y otro de salida sobre el socket.

Luego comienza el bucle que implementa la comunicación entre el cliente y el servidor. El servidor habla primero, por lo que el cliente debe escuchar, lo que hace leyendo desde el stream de entrada adosado al socket. Cuando el servidor habla, si dice "Bye,", el cliente sale del bucle. De otra forma muestra el texto en la salida estandard, y luego lee la respuesta del usuario, que la teclea en al entrada estandard. Después de que el usuario teclee el retorno de carro, el cliente envía el texto al servidor a través del stream de salida adosado al socket.

while ((fromServer = is.readLine()) != null) {
    System.out.println("Server: " + fromServer);
    if (fromServer.equals("Bye."))
        break;
    while ((c = System.in.read()) != '\n') {
        buf.append((char)c);
    }
    System.out.println("Client: " + buf);
    os.println(buf.toString());
    os.flush();
    buf.setLength(0);
}
La comunicación termina cuando el servidor pregunta si el cliente quiere escuchar otra broma, si el usuario dice no, el servidor dice "Bye.".

En el interés de una buena limpieza, el cliente cierra sus streams de entrada y salida y el socket:

os.close();
is.close();
kkSocket.close();

Ejecutar los Programas

Primero se debe arrancar el programa servidor. Haz esto ejecutando el programa servidor utilizando el intérprete de Java, como lo haría con cualquier otro programa. Recuerda que debes ejecutarlo en la máquina que el programa cliente especifica cuando crea el socket.

Luego ejecutas el programa cliente. Observa que puedes ejecuarlo en cualquier máquina de tu red, no tiene porque ejecutarse en la misma máquina que el servidor.

Si es demasiado rápido, podría arrancar el cliente antes de que el cliente tuviera la oportunidad de incializarse y empezar a escuchar el puerto. Si esto sucede verás el siguiente mensaje de error cuando intentes arrancar el programa cliente:

Exception:  java.net.SocketException: Connection refused
Si esto sucede, intenta ejecutar el programa cliente de nuevo.

Verás el siguiente mensaje de error si se te olvidó cambiar el nombre del host en el código fuente del programa KnockKnockClient.

Trying to connect to unknown host: java.net.UnknownHostException: taranis
Modifica el programa KnockKnockClient y proporciona un nombre de host válido en tu red. Recompila el programa cliente e intentalo de nuevo.

Si intentas arrancar un segundo cliente mientras el primero está conectado al servidor, el segundo colgará. La siguiente sección le cuenta como soportar múltiples clientes.

Cuando obtengas una conexión entre el cliente y el servidor verás esto en tu pantalla:

Server: Knock! Knock!
Ahora, deberás responder con :
Who's there?
El cliente repite lo que has tecleado y envía el texto al servidor. El servidor responde con la primera línea de uno de sus varias bromas Knock Knock de su repertorio. Ahora tu pantalla debería contener esto (el texto que escribiste; está en negrita):
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Ahora deberías responderle con:
Turnip who?
De nuevo, el cliente repite lo que has tecleado y envía el texto al servidor. El servidor responde con la línea graciosa. Ahora tu pantalla debería contener esto (el texto que escribiste; está en negrita):
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)
Si quieres oir otra borma teclea "y", si no, teclee "n". Si tecleas "y", el servidor empieza de nuevo con "Knock! Knock!". Si tecleas "n" el servidor dice "Bye.", haciendo que tanto el cliente como el servidor terminen.

Si en cualquier momento cometes un error al teclear, el objeto KKState lo captura, el servidor responde con un mensaje similar a este, y empieza la broma otra vez:

Server: You're supposed to say "Who's there?"! Try again. Knock! Knock!
El objeto KKState es particular sobre la ortografía y la puntuación, pero no sobre las letras mayúsculas y minúsculas.

Soportar Mútiples Clientes

El ejemplo KnockKnockServer fue diseñado para escuchar y manejar una sola petición de conexión. Sin embargo, pueden recibirse varias peticiones sobre el mismo puerto y consecuentemente sobre el mismo ServeSocket. Las peticiones de conexiones de clientes se almecenan en el puerto, para que el servidor pueda aceptarlas de forma secuencial. Sin embargo, puede servirlas simultaneamente a través del uso de threads -- un thread para procesar cada conexión de cliente.

El flujo lógico básico en este servidor sería como este:

while (true) {
    aceptar un a conexión;
    crear un thread para tratar a cada cliente;
end while
El thread lee y escribe en la conexión del cliente cuando sea necesario.

Intenta esto: Modifica el KnockKnockServer para que pueda servir a varios clientes al mismo tiempo. Aquí tienes nuestra solución, que está compuesta en dos clases: KKMultiServer y KKMultiServerThread. KKMultiServer hace un bucle continúo escuchando peticiones de conexión desde los clientes en un ServerSocket. Cuando llega una petición KKMultiServer la accepta, crea un objeto KKMultiServerThread para procesarlo, manejando el socket devuelto por accept(), y arranca el thread. Luego el servidor vuelve a escuchar en el puerto las peticiones de conexión. El objeto KKMultiServerThread comunica con el cliente con el que está leyendo y escribiendo a través del socket. Ejecute el nuevo servidor Knock Knock y luego ejecuite varios clientes sucesivamente.


Ozito