Mensajes entre aplicaciones con VFP

From codeWiki
Jump to: navigation, search

Por: VictorEspina


Contents

Introducción

Podemos encontrar varias situaciones en las que se presenta la necesidad de poder enviar y recibir "mensajes" entre diferentes estaciones de trabajo ejecutando la misma aplicación o incluso aplicaciones diferentes. Por ejemplo, podríamos necesitar informar a un servidor de impresión que cierto documento está listo para ser impreso, o por ejemplo, consultar a una estación que usuario está trabajando en dicha estación en un momento dado.

Cualquiera que sea la función que se desea implementar, un mecanismo que permita enviar y recibir mensajes entre aplicaciones ejecutando en computadoras diferentes, puede ser la mejor solución.

Este artículo busca explicar someramente una técnica para implementar esta funcionalidad en aplicaciones VFP, utilizando archivos de texto.


¿Que se necesita?

Básicamente, necesitamos implementar un mecanismo para enviar mensajes hacia un computador específico, e incluso esperar una respuesta del mismo, y también poder monitorear y reaccionar ante los mensajes enviados al computador de origen.

El mecanismo básico se muestra en la siguiente figura:


InterAppMsg1.gif


Como vemos, cada aplicación debe ser capaz no solo de enviar peticiones, sino de monitorear y responder las peticiones que le son enviadas por otras estaciones ejecutando la misma aplicación (o incluso otra diferente).

Ya programáticamente hablando, esta funcionalidad la podemos lograr a través de una subclase de Timer, que incluya métodos que implementen la funcionalidad necesaria:

 
DEFINE CLASS APPMessenger AS TIMER

  SharedFolder = ".\"
  RequestTimeOut = 10
  Interval = 10000    && 10 segundos

  PROCEDURE ReadMessages
  ENDPROC

  PROCEDURE SendMessage
  ENDPROC

  PROCEDURE ProcessMessage
  ENDPROC

  PROCEDURE SendAnswer
  ENDPROC

  PROCEDURE Start
  ENDPROC

  PROCEDURE Stop
  ENDPROC

ENDDEFINE


Implementación

Veamos esto en detalle. La propiedad SharedFolder nos permitirá indicar la ubicación de la carpeta donde se leerán los mensajes recibidos y se pondrán los mensajes enviados a otros computadores. Algunos mensajes podrían requerir de una respuesta inmediata; sin embargo no es conveniente esperar por esa respuesta indefinidamente. La propiedad RequestTimeOut nos permitirá indicar cuanto tiempo estamos dispuestos a esperar por la respuesta a un mensaje enviado a otra estación. Finalmente, podemos indicar cada cuanto tiempo queremos verificar los mensajes recibidos, a través de la propiedad Interval de la clase Timer, la cual como sabemos se expresa en milisegundos. Debemos tener cuidado con esto; un valor muy pequeño en Interval puede afectar sensiblemente el desempeño general de la aplicación.

Ahora veamos la funcionalidad propiamente dicha. Los métodos Stop() y Start(), nos pemiten arrancar o detener el sistema de mensajería, según lo necesitemos. Para esto utilizamos la propiedad Enabled de la clase Timer:

 
PROCEDURE Start
 THIS.Enabled=.T.
ENDPROC

PROCEDURE Stop
 THIS.Enabled=.F.
ENDPROC

El método SendMessage() nos permite enviar un mensaje a un computador dado. Para simplificar el proceso, asumiremos que un mensaje es un archivo de texto plano que contiene la siguiente información:

ENVIADO POR
TIPO (MSG o REQ)
MENSAJE
DATA ADICIONAL

Un ejemplo de un archivo de mensaje podría ser:

EST08
MSG
SHOW-MSG
Esto es una prueba

En este ejemplo, se le está indicando a un computador que, de parte del computador EST08, muestre un mensaje (SHOW-MSG) con el texto indicado (Esto es una prueba). Ahora, ¿como sabemos a quien va dirigido el mensaje? Esto lo sabremos a través del nombre del archivo de texto. El nombre de un archivo de mensaje tendrá la siguiente estructura:

EST10-34567.MSG

Donde EST10 corresponde al nombre de la máquina a donde va dirigido el mensaje y 34567 es el valor de la función SECONDS() al momento en que fue emitido el mensaje.

Siguiendo estas pautas, podemos expresar el método SendMessage() de la siguiente forma:

#DEFINE CRLF	CHR(13)+CHR(10)

PROCEDURE SendMessage(pcComputer,pcMessageType,pcMessage,pcMessageData)
 LOCAL cThisComputer,cFileName,cFileData
 cThisComputer=SYS(0)
 cThisComputer=ALLTRIM(LEFT(cThisComputer,AT("#",cThisComputer)-1))
 cFileName=pcComputer + "-" + ALLTRIM(STR(SECONDS())) + ".MSG"
 cFileName=ADDBS(THIS.SharedFolder) + cFileName
 cFileData=cThisComputer + CRLF + ;
           upper(pcMessageType) + CRLF + ;
           pcMessage + CRLF + ;
           iif(vartype(pcMessageData)<>"C",pcMessageData,"")
 STRTOFILE(cFileData,cFileName)
ENDPROC

Como vemos, el parámetro pcMessageData es considerado opcional, por lo que se hace necesario verificar su tipo con la función VARTYPE(), antes de incluirlo en el texto del mensaje. También debemos notar que esta primera versión de SendMessage() no incluye el código necesario para esperar una respuesta en caso de que pcMessageType sea "REQ". Luego veremos una versión modificada de este método que incluye esa lógica.

Ahora vamos con el verdadero meollo del asunto. El método ReadMessages() debe encargarse no solo de leer los mensajes enviados al computador, sino de procesarlos y devolver las respuestas necesarias. Todo el método gira en torno al uso de la función SYS(2000) para obtener los nombres de los archivos de mensajes dirigidos al computador:

PROCEDURE ReadMessages
 THIS.Stop()
 LOCAL cFile,cComputer,cSender,cMsgText,cMsgType,cMsg,cMsgData
 cComputer=SYS(0)
 cComputer=ALLTRIM(LEFT(cComputer,AT("#",cComputer)-1))
 cFile=sys(2000,ADDBS(THIS.SharedFolder)+cComputer+"-*.MSG")
 DO WHILE NOT EMPTY(cFile)
  cFile=FORCEPATH(cFile,THIS.SharedFolder)
  cMsgText=FILETOSTR(cMsgText)
  cSender=MLINE(cMsgText,1)
  cMsgType=MLINE(cMsgText,2)
  cMsg=MLINE(cMsgText,3)
  cMsgData=MLINE(cMsgText,4)
  THIS.ProcessMessage(cFile,cSender,cMsgType,cMsg,cMsgData)
  IF FILE(cFile)
   ERASE (cFile)
  ENDIF
  cFile=sys(2000,ADDBS(THIS.SharedFolder)+cComputer+"-*.MSG")
 ENDDO
 THIS.Start()
ENDPROC

Aqui hay varios puntos interesantes. En primer lugar, nótese como se detiene el timer al empezar el ciclo de lectura de mensajes y como se reinicia al finalizar el proceso. Esto es necesario para evitar que se acumulen llamadas al evento Timer mientras se está procesando los mensajes, cosa que puede pasar si hay muchos mensajes o los mismos no son procesados dentro del intervalo de tiempo dado en la propiedad Interval.

Otro punto importante es la omisión del tercer parámetro en la segunda llamada a la función SYS(2000). Como sabemos, ese tercer parámetro permite indicar a la función que nos dé el siguiente archivo que coincida con la máscara indicada. Si no se indica, entonces la función devolverá el primer archivo coincidente. Dado que cada archivo de mensaje es eliminado luego de ser procesado, la omisión de ese parámetro nos permitirá seguir leyendo archivos MSG, incluso si uno es creado mientras se está dentro del ciclo DO WHILE.

Una vez obtenido el texto del mensaje, se extrae cada una de sus partes. Este truco lo logramos fácilmente con las funciones FILETOSTR() y MLINE():

cMsgText=FILETOSTR(cMsgText)
cSender=MLINE(cMsgText,1)
cMsgType=MLINE(cMsgText,2)
cMsg=MLINE(cMsgText,3)
cMsgData=MLINE(cMsgText,4)

Finalmente, una vez obtenido el contenido del mensaje, el mismo es procesado a través del método ProcessMessage(). Es en este método donde se procesan los mensajes recibidos y donde se generan las respuestas a que haya lugar, segun el tipo de mensaje recibido. Este método deberá ser modificado por cada programador, para adaptarlo a las necesidades específicas de su aplicación. Veamos un ejemplo de como debería lucir este método:

PROCEDURE ProcessMessage(pcMSGFile,pcSender,pcType,pcMsg,pcData)
 LOCAL cMsgAnswer
 cMsgAnswer=""
 DO CASE
    CASE pcMsg="SHOW-MSG"
         MESSAGEBOX("MENSAJE DE "+pcSender+": "+pcData)

    CASE pcMsg="GET-USER-NAME"
         cMsgAnswer=SYS(0)
         cMsgAnswer=ALLTRIM(SUBSTR(cMsgAnswer,AT("#",cMsgAnswer)+1))
 ENDCASE

 IF pcMsg="REQ"
  THIS.SendAnswer(pcMSGFile,cMsgAnswer)
 ENDIF
ENDPROC


PROCEDURE SendAnswer(pcMSGFile,pcData)
 LOCAL cFile
 cFile=ADDBS(THIS.SharedFolder) + FORCEEXT(pcMSGFile,"ANS")
 STRTOFILE(pcData,cFile)
ENDPROC

Como vemos, la estructura es bastante sencilla. El método ProcessMessage() procesa el mensaje recibido y utiliza la variable cMsgAnswer para almacenar la respuesta a enviar al computador que envió el mensaje, en el caso de que sea necesario. Si el tipo de mensaje era "REQ", se invoca el método SendAnswer() para enviar la respuesta al computador de origen. Como podemos ver en el método, la respuesta no es más que un archivo de texto con el mismo nombre del archivo de mensaje original, pero con la extensión "ANS", y cuyo único contenido es la respuesta generada por el mensaje enviado.

La ultima pieza en este esquema es invocar el método ReadMessages() cada vez que se disparé el evento Timer de la clase base:

PROCEDURE Timer
    THIS.ReadMessages()
ENDPROC

Ahora bien, ya llegados a este punto es hora de ver como seria la versión final del método SendMessage(), incluyendo el código necesario para la recepción de un archivo de respuesta:

#DEFINE CRLF	CHR(13)+CHR(10)

PROCEDURE SendMessage(pcComputer,pcMessageType,pcMessage,pcMessageData)

 LOCAL cThisComputer,cFileName,cFileData
 cThisComputer=SYS(0)
 cThisComputer=ALLTRIM(LEFT(cThisComputer,AT("#",cThisComputer)-1))
 cFileName=pcComputer + "-" + ALLTRIM(STR(SECONDS())) + ".MSG"
 cFileName=ADDBS(THIS.SharedFolder) + cFileName
 cFileData=cThisComputer + CRLF + ;
           upper(pcMessageType) + CRLF + ;
           pcMessage + CRLF + ;
           iif(vartype(pcMessageData)<>"C",pcMessageData,"")
 STRTOFILE(cFileData,cFileName)

 IF pcMessageType="REQ"
  *
  LOCAL cANSFile,nStarted
  cANSFile=FORCEEXT(cFileName,"ANS")
  nStarted=SECONDS()
  DO WHILE (SECONDS() - nStarted) <= THIS.RequestTimeOut
   DOEVENTS()
   IF FILE(cANSFile)
    EXIT
   ENDIF
  ENDDO
 
  IF FILE(cANSFile)  && Se recibió el archivo de respuesta
   LOCAL cAnswer,nFH
   nFH=-1
   DO WHILE nFH=-1
    nFH=FOPEN(cANSFile,1)
   ENDDO
   FCLOSE(nFH)
   cAnswer=FILETOSTR(cANSFile)
   ERASE (cANSFile)
   RETURN cAnswer
  ELSE               && No se recibió el archivo de respuesta
   RETURN ""
  ENDIF
  *
 ENDIF

ENDPROC
De nuevo, encontramos varios puntos interesantes en este método. El ciclo:
DO WHILE (SECONDS() - nStarted) <= THIS.RequestTimeOut
 DOEVENTS()
 IF FILE(cANSFile)
  EXIT
 ENDIF
ENDDO

nos permite esperar por el archivo de respuesta, hasta completar el número de segundos indicados en la propiedad RequestTimeOut o hasta verificar que el archivo existe. La llamada a DOEVENTS() en cada iteración nos garantiza que la función FILE() devolverá .T. en cuanto el archivo sea creado en la carpeta compartida.

Otro ciclo interesante es el que se realiza si, una vez finalizado el ciclo de espera por el archivo ANS, el mismo existe en la carpeta compartida:

nFH=-1
DO WHILE nFH=-1
 nFH=FOPEN(cANSFile,1)
ENDDO
FCLOSE(nFH)

Este ciclo es necesario pues el hecho de que el archivo exista NO IMPLICA que el mismo haya sido completado. Este ciclo lo que hace es asegurarse que el archivo ANS ha sido cerrado por la aplicación que lo creó, antes de proceder a leerlo.

Bien, una vez completados todos los componentes del objeto, veamos como podemos usarlo. Lo primero es cargar y configurar el procesador de eventos al inicio del programa principal de la aplicación:

PUBLIC goAppMessenger
goAppMessenger=CREATEOBJECT("cAppMessenger")
goAppMessenger.SharedFolder=".\APPMSG"
goAppMessenger.Start()

Una vez hecho esto, es cuestión de llamar al método SendMessage() según sea necesario:

goAppMessenger.SendMessage("EST02","MSG","SHOW-MSG","Esto es una prueba")

o por ejemplo, podemos obtener el nombre del usuario activo en la estación EST02:

LOCAL cUser
cUser=goAppMessenger.SendMessage("EST02","REQ","GET-USER-NAME")
IF NOT EMPTY(cUser)
 MESSAGEBOX("El usuario activo en la estación EST02 es: "+cUser)
ENDIF


Conclusiones

El código fuente mostrado en este artículo es solo una muestra elemental de como aplicar la técnica para pase de mensajes entre aplicaciones, usando archivos de texto. En ningún momento se debe asumir este código como funcional. Mas que esto, lo mostrado en este breve artículo pretende ser una guia con la cual cualquier programador puede desarrollar su propio procesador de mensajes, de acuerdo a sus necesidades particulares.


Comentarios

Escriba aqui sus comentarios (solo para usuarios registrados). Recuerde firmar sus comentarios, poniendo cuatro tildes (~) juntas.


Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox