La creación de un cliente y un servidor TCP se hace muy simple con los componentes de la suite Internet Direct (Indy). Estos componentes vienen incluidos en la VCL de Delphi 6 y en Kylix en su versión 8. No obstante la mayoría de los detalles son aplicables, en este artículo usaré la versión 9.0.3 beta que se puede obtener de Nevrona.
Los componentes que nos interesan aquí son el IdTCPServer (para el servidor) y el IdTCPClient para el cliente.
Al arrancar el servidor, el componente IdTCPServer crea un hilo de ejecución especial con un socket puesto a la escucha (ver el articulo sobre comunicación con sockets usando la API). Este socket es del tipo ‘bloqueante’, es decir que detiene toda otra acción del hilo en el que se ejecuta hasta que recibe una petición de conexión. El programa principal puede seguir funcionando gracias al hilo de ejecución separado.
Cuando el hilo de escucha recibe una petición, crea otro hilo de ejecución para establecer la comunicación y le pasa el control. Este hilo es de tipo TIdPeerThread.
Por defecto, el componente IdTCPServer crea un hilo de respuesta cada vez que lo necesita, y cuando termina la comunicación lo destruye. Esto es aceptable en la mayoría de los casos; no obstante, en servidores de alto volumen de peticiones se puede modificar la forma de administrar los hilos. En la versión 9 de Indy tenemos dos componentes que corresponden a dos métodos de administración: la simple (componente IdThreadMgrDefault) y uno que configura y mantiene un pool de hilos (componente IdThreadMgrPool). Este último crea al iniciar su ejecución una cantidad determinada de hilos y los utiliza cuando le llegan las peticiones. No los destruye al terminar la comunicación, sino que los almacena para reutilizarlos en próximas conexiones. Para seleccionar el método de administración de hilos, basta con agregar al form donde está el componente servidor uno de los manejadores, y asociarlo al servidor usando la propiedad ThreadMgr.
Si no asociamos ningún manejador de hilos al servidor, se creará automáticamente uno de tipo IdThreadMgrDefault.
Repasemos entonces los puntos salientes del funcionamiento de los componentes Indy:
· Se utilizan sockets bloqueantes
· Para evitar el ‘congelamiento’ del sistema, se coloca a los sockets en hilos de ejecución separados.
El cliente es también muy sencillo, ya que solamente debemos colocar un componente IdTCPClient y llamar a su método Connect (supuesto que hemos indicado el host/puerto del servidor). A partir de aquí, escribimos en el socket –es decir, enviamos al servidor- usando Writeln[1] como hacíamos en Pascal para DOS. Y para leer del socket… claro, usamos Readln!
Ahora bien: el cliente no utiliza un hilo especial para esperar por los datos del servidor. Lo que significa que la aplicación cliente puede quedar bloqueada esperando un envío desde el servidor. Para evitar esto tenemos algunas alternativas:
· damos un valor distinto de cero a la propiedad ReadTimeout del IdTCPClient. Esta propiedad indica la cantidad de milisegundos que deben pasar antes que se levante una excepción de agotamiento de tiempo de espera. ‘0’ significa ‘para siempre’.
· Agregamos un componente IdAntiFreeze a la aplicación.
· Agregamos un parámetro a Readln que indica la cantidad de milisegundos que debe esperar a recibir algo antes de dar error.
Pero basta de charla y
pasemos a la acción. Vamos a crear un servidor TCP, paso a paso.
En un proyecto nuevo ponemos en el form principal un componente TIdTCPServer. Colocamos las siguientes propiedades:
· DefaultPort: un valor entero, preferiblemente mayor a 1023. Es el puerto a usar en la conexión; hasta el 1023 están reservados para servicios estándar tales como FTP (21 y 22), telnet (23), http (80), etc.
· Greeting.Text: un texto que será enviado automáticamente a cualquier cliente que se conecte.
· Active: TRUE para activar el componente.
Y ya está. Tenemos un servidor TCP funcional, sin escribir ni una línea. Para probarlo necesitamos un cliente; a continuación crearemos uno propio, pero por ahora podemos usar el cliente Telnet que viene con Windows.
Ejecute el servidor, abra una sesión de línea de comandos y escriba
telnet localhost nro_de_puerto
donde nro_de_puerto es el número del puerto que colocamos en la propiedad DefaultPort del componente servidor.
Tendríamos que ver el mensaje que colocamos en la propiedad Greeting.Text del servidor, y luego un mensaje indicando que la conexión se ha cerrado.
Este es el comportamiento por defecto: recibir, abrir canal, contestar, cerrar canal.
Ahora vamos a establecer un protocolo de comunicación: es decir, vamos a definir comandos que el cliente puede enviar y las respuestas que el servidor dará a los mismos.
Generalmente los comandos serán simples palabras (texto). Vamos a definir un comando salir que hará que el servidor se cierre.
Para los comandos de texto, el componente IdTCPServer implementa una propiedad (CommandHandlers) que es una colección de comandos. Estos comandos, de tipo TIdCommandHandler, poseen las siguientes propiedades:
|
Propiedad |
Tipo |
Función |
|
Command |
String |
Texto del comando |
|
Disconnect |
Boolean |
True si se debe desconectar la conexión activa al recibir el comando |
|
CommandDelimiter |
Char |
Caracter que indica el fin del texto del comando |
|
ParamDelimiter |
Char |
Caracter que indica el fin de cada parámetro |
|
ParseParams |
Boolean |
Si está en verdadero, los parámetros se extraen automáticamente (cada uno separado del siguiente por un caracter ParamDelimiter) |
|
ReplyNormal |
TIdRFCReply |
Texto y código de la respuesta por defecto al comando (se puede cambiar en el evento OnCommand) |
|
Response |
TStrings |
Lista de strings que son enviados uno después del
otro después de enviar la respuesta
normal (ReplyNormal), si no se especifica otra cosa en el evento OnCommand |
Cuando se recibe un string desde el cliente, el servidor extrae el comando (la primera palabra, hasta el identificador dado en CommandDelimiter) y compara contra los comandos definidos en la propiedad CommandHandlers. Si encuentra una coincidencia, le pasa el control al método Check del comando.
El comando entonces crea un objeto de tipo TIdCommand, separa los parámetros si ParseParams es verdadero, coloca los valores de ReplyNormal y Response como valores iniciales de respuesta del comando (propiedades Reply y Response del IdCommand), y luego lo pasa como parámetro del evento OnCommand si tiene asociado un método de respuesta. Una vez que termina la ejecución de este evento[2], se envían al cliente los valores de respuesta (primero el valor de Reply, si IdCommand.PerformReply está todavía en True, y luego los strings de la propiedad IdCommand.Response si existen o de IdCommandHandler.Response en caso contrario, formateados de acuerdo a lo especificado en las RFC). Finalmente, destruye el objeto de comando (IdCommand) y desconecta el thread.
Si algún error se produce en el evento OnCommand, el comando automáticamente se recupera y envía los valores de ReplyExceptionCode y el mensaje de la excepción como respuesta antes de Reply y Response.
De esta manera se puede crear un protocolo completo a través de CommandHandlers; pero por ahora implementaremos sólo un par de comandos para probar la teoría anterior.
Este comando hará que el servidor desconecte al cliente, enviando un mensaje previo.
Agregamos entonces un ítem a la colección CommandHandlers del IdTCPServer, y completamos algunas propiedades:
· Command: ‘salir’
· Disconnect: true
·
ReplyNormal
o Code: 200
o
Text: dos líneas, por ejemplo ‘Cerrando el
servidor...’ y ‘Ahora estas solo de nuevo’
Ahora ejecutamos el servidor nuevamente. En el cliente Telnet, después de conectar igual que antes, escribimos ‘salir’ (sin las comillas). No veremos nada en la pantalla; esto es normal. Al presionar <Enter> después del comando, tendríamos que ver las dos líneas de respuesta precedidas del código, y luego la desconexión normal.

Este comando debe ser seguido del nombre del usuario, a lo que el servidor responderá con un saludo personalizado.
La diferencia con el comando ‘salir’ es que ahora utilizamos un parámetro. Creamos un nuevo comando con la propiedad Command = ‘nombre’ y en el evento OnCommand escribimos lo siguiente:
procedure TForm1.IdTCPServer1cmdNombreCommand(ASender: TIdCommand);
begin
ASender.Reply.Text.Text:= 'Hola, '+ASender.Params[0];
end;

En la imagen se ve una sesión completa donde primero se envió el comando ‘nombre Ernesto’ y luego ‘salir’.