Construyendo aplicaciones No-HTML con VFP

Por Rick Strhal

 

El Web ha abierto una nueva área de desarrollo para construir aplicaciones verdaderamente distribuidas que puedan ejecutarse en multitud de lugares. Ahora es posible hacer aplicaciones que tengan un acceso público con un coste menor comparado con las infraestructuras que eran necesarias hasta ahora para construir este tipo de aplicaciones distribuidas.

Desafortunadamente este nuevo medio requiere una nueva forma de hacer las aplicaciones que se centra de una forma muy intensa en las limitaciones del interfaz del usuario que viene condicionado por el HTML. HTML es un lenguaje de scripts y produce unos resultados muy parecidos a los viejos procesadores de textos como WordStar y WordPerfect para DOS. Comparados a los interfaces que nos da Windows e incluso las posibilidades que soportan los browsers en sus versiones 4.0 queda mucho para lograr un interfaz amigable de entrad de datos que es algo típico de aplicaciones de bases de datos. Con HTML se pueden hacer muchas cosas sobre todo si tienes imaginación, pero el resultado final dejará bastante que desear en cuanto a la usabilidad.

Pero, ¿qué pasaría si haces una aplicación usando Visual FoxPro en el lado del cliente que habla a un servidor Web que también es un servidor VFP? Más que usar un chusco HTML podrías tomar ventaja del poder de los interfaces de VFP y la facilidad de uso a la hora de acceder a los datos y a la hora de construir una verdadera y amigable aplicación con las posibilidades de distribución que promete el HTML.

En este documento discutiremos como realizar esto demostrando algunas de las herramientas de libre distribución que están disponibles e implementar un ejemplo que use esta tecnología. Puedes echar un vistazo a los resultados en el área de mensajes de nuestra BBS usando el lector offline, que usa una aplicación cliente Visual FoxPro para comunicarse con el servidor Web para recuperar y poner mensajes a y desde el servidor.

 

HTTP - Más que un protocolo para transmitir HTML

La buena noticia es que no tienes que hacer aplicación Web distribuidas con HTML. Es absolutamente posible construir aplicaciones usando un rico interfaz y la simplicidad del web para transmitir datos de bases de datos. La clave para hacer todo esto es trabajar con HTTP - el HyperText Transfer Protocol.

El protocolo HyperText Transfer es el que se usa en el mundo del World Wide Web. Aunque el uso primario del protocolo es para entregar y pedir documentos HTML, realmente puede hacer mucho más que entregar un HTML plano. Esencialmente puedes usar HTTP para transportar cualquier tipo de dato entre archivos de bases de datos.

El HTTP se basa en un modelo cliente/servidor. Típicamente, el Web browser es el cliente que solicita datos al servidor de Web. El browser es el mecanismo que nos permite mostrar el contenido que ha sido servido por el servidor de Webs. El servidor no es otra cosa que un entregador de peticiones para los clientes. Pero pensar en un servidor Web y en el HTTP como algo que únicamente entrega páginas HTML es un error – cualquier tipo de datos, incluso archivos pueden ser entregados y servidos por este protocolo siempre y cuando cuides las reglas del protocolo. En breve, mostraré un ejemplo de esto.

En las pasadas versiones de Windows (Windows 95, Windows NT 4.0), Microsoft ha dotado de un soporte de alto nivel para diversos protocolos de Internet mediante una librería llamada WinInet. Esta librería soporta unos relativamente simples API de interfaces para acceder a FTP, HTTP y WinSock. Microsoft los ha adornado con una arquitectura familiar de archivos donde puedes establecer una conexión y a continuación leer o escribir directamente. Estas posibilidades del WinSock API hacen posibles a lenguajes como Visual FoxPro que puedan acceder a la funcionalidad del interfaz del sistema directamente.

Antes de que dé el salto y te muestre cómo crear una clase que pueda acceder a las funcionalidades del HTTP en el WinInet, daremos un vistazo a las implicaciones de un acceso directo a HTTP. La posibilidad de enviar y recibir datos en cualquier formato que escojas te da la posibilidad de implementar tu propia arquitectura Cliente/Servidor que se puede comunicar sobre cualquier conexión Internet.

Aquí hay algunas útiles aplicaciones que podrías hacer:

1.- Cualquier conexión en tiempo real que actualiza los datos que se muestran en un formulario de un Web – valor de acciones, gráficos o el estado del tiempo – pueden de una forma fácil acceder a un link HTTP y download los datos. Si provees regularmente los datos a tus clientes (datos financieros o una imagen capturada por ti), puedes hacer que los datos estén disponibles directamente desde una aplicación VFP. Los datos pueden ser HTML o datos que se han formado según un específico diseño de forma que sea fácil el mostrarlos en una tabla VFP (una cadena delimitada por comas por ejemplo) y a continuación mostrarlos en un lisbox o en un Grid.

2.- Las actualizaciones de software o de datos se podrían aprovechar de esta tecnología. Los servicios de subscripción podrían proveer actualizaciones a través de Internet usando HTTP a la hora de download archivos. El mismo mecanismo se puede usar para actualizar los archivos actuales de tus aplicaciones, downloar la actualización y correr un programa de actualización.

3.- Otro buen ejemplo es un lector offline para un message board online. Podrías haber visto varias versiones de newsgroup basados en HTML con formato de aplicación, tales como el Visual FoxPro Universal Thread (www.universalthread.com) o mi propio message board en www.west-wind.com/wwthreads/. Con la posibilidad de leer y escribir a/y desde un link HTTP pudiendo usar el mismo interfaz del servidor web para enviar los datos, todos los registros desde tu última visita, actualizar una tabla FoxPro con los registros y usar el Interfaz y las posibilidades de las aplicaciones Fox para mostrar los mensajes. Cuando necesites poner un mensaje, todos los mensajes se guardan en una tabla temporal que se pueden trasladar al Web la próxima vez que te conectes o que decidas actualizar los mensajes.

Una cosa que tienes que tener en cuenta: aunque no uses HTML para mostrar el output, es que necesitarás una aplicación Web que te sirva de respaldo. Necesitarás un servidor Web y algún software como el de Web Connection, Active Server Pages, FoxISAPI o FoxWeb para poder entregar al servidor tus aplicaciones. La diferencia es que aquí no envío HTML en ninguno de los ejemplos sino más bien datos que son compartidos por los clientes y el Servidor .

 

WinInet con Visual FoxPro

De cara a hacer la vida más fácil a la hora de usar las funciones del WinInet, he construido una clase que se llama wwIPStuff que tienes a tu disposición en http://www.west-wind.com/webtools.htm. Junto a los ejemplos de HTTP que comentaré a continuación, la clase también contiene la posibilidad de enviar mensajes a través de SMTP o envío de ficheros por FTP junto con algunas utilidades para llamar y colgar mediante Remote Access (RAS), y resolver direcciones IP y dominios mediante el uso de una librería DLL externa. Las funciones de HTTP que discutiré aquí se han implementado todas en un código que es puro Visual FoxPro usando llamadas DECLARE –API. En otras he usado wwIPStuff DLL cuando había que invocar al WinAPI o al Winsock y lo he hecho en código C++ .

Para que funcione todo correctamente necesitas que en tu máquina esté instalado el WinInet.dll en el directorio de Windows System. Esta DLL se instala con muchas de las últimas herramientas para Internet de Microsoft – si tienes Internet Explorer 3.0 o posterior, tendrás esta librería. Si no la tienes puedes disponer de ella en http://www.west-wind.com/files/wininet.zip. Si el ejemplo falla diciendo que 'Cannot find entry point to <DLLfunction>' seguramente no la tendrás en tu máquina.

Todas las funciones HTTP de wwIPStuff se han implementado con puro código Visual FoxPro que accede directamente a las funciones de WinInet. Puedes revisar el código en la librería de clases por ti mismo para ver como se ha realizado - podremos algunos ejemplos de cómo hacer que funcionen estas herramientas.

Empezaremos con la forma más simple de obtener datos desde el Web: El método wwIPStuff::HTTPGet. Este método únicamente devuelve una URL desde el Web y lo guarda como una cadena. La Figura 1.1 muestra un ejemplo de un simple formulario que usa este método. Añade un Editbox para mostrar el resultado, un Textbox para la URL y un botón de órdenes que realice la tarea de devolver los datos.

 

Figura 1.1El método HTTPGet permite obtener una respuesta HTTP en forma de cadena que se muestra en la caja de edición anterior. El contenido puede ser código HTML como se muestra aquí o cualquier tipo de dato que desees enviar.

En el código del botón añade lo siguiente:

SET PROCEDURE TO wwIPStuff ADDITIVE

o = CREATE("wwIPStuff")

THISFORM.edtHTML.Value = o.HTTPGet(TRIM(THISFORM.txtUrl.Value),100000)

Nota que el HTTPGet() espera un URL completo que contenga el protocolo, servidor y opcionalmente un archivo para mostrar. El http:// ¡es requerido!

El anterior link devolverá una cadena HTML como texto en el editbox del formulario. Devolver HTML de esta manera puede ser muy útil para monitorizar sitios o verificar links en directorios, o para establecer una telaraña web que investigue en varias páginas.

Sin embargo, si el servidor devolviera datos pero con formato de texto, podrías hacer algo más útil con los datos. Da un vistazo a la figura 1.3, que devuelve los datos delimitados por comas en un Web y llena un listbox con los datos devueltos..

Figura 1.3 – Devolviendo datos en el Web. En este ejemplo una cadena delimitada por comas se envía por la red y recogido por un cursor nos lo muestra en el listbox.

El código relevante aquí va en el botón Reload del formulario que tiene una pinta parecida a esto:

o=CREATE("wwIPStuff")

*** Devuelve todas las compañías que

*** empiezan con "A"

lcText = o.HTTPGet("http://www.west-wind.com/wconnect/wc.;

        dll?http~CustList1~"+;

TRIM(THISFORM.txtQueryCompany.value) )

if EMPTY(lcText) OR lcText="FAILED"

   ait window "Invalid HTTP Response..." nowait

   RETURN

ENDIF

tcFileName = SYS(3)+ '.txt'

File2Var(tcFileName,lcText)

CREATE CURSOR TCustList ( CUSTNO C (8),;

        COMPANY C (30),;

         CAREOF C (30) )

APPEND FROM (tcFileName) DELIMITED

ERASE (tcFileName)

THISFORM.lstCustList.RowSourceType = 2

THISFORM.lstCustList.RowSource = "tCustList.company, careof"

THISFORM.lstCustList.Requery

 

Has corrido una consulta en el Web y ¡has mostrado los datos con apenas 15 líneas de código! El servidor devuelve una cadena delimitada por comas que puedes importar a una tabla y usar de la forma que estimes oportuno para tu aplicación. He pasado el parámetro especificado en el listbox como parte de la cadena URL (esto sería la línea de Address/Location si se ejecuta desde un browser). Esto funciona bien con parámetros simples, pero es algo más limitado si quieres pasar más información. Nos fijaremos en otro modo de hacer lo mismo más tarde.

Como se ha comentado con anterioridad lo único que necesitas es un servidor que pueda responder a los servicios que se le solicitan. En este caso el código del servidor Web de Visual FoxPro también es muy corto y tiene la siguiente pinta:

 

********************************

* HTTPDemo :: CustList1

*********************************

*** Function: Returns a customer list based on the *** URL

*** 'parameter' passed. Delimited returned.

*** Assume:

*** wc.dll?http~CustList1~CompanySearchString

*****************************************

FUNCTION CustList1

lcCustToFind = THIS.oCGI.GetCGIParameter(3)

lcFile=SYS(3)+".TXT"

SELECT custno,Company, Careof ;

    FROM (DATAPATH + "TT_Cust") ;

    WHERE Company = lcCustToFind ;

    ORDER BY Company ;

    INTO CURSOR TQuery

COPY TO (lcFile) TYPE DELIMITED

*** Send the Delimited string over the wire

THIS.oHTML.Send(File2Var(lcFile))

ERASE (lcFile)

USE IN TQuery

USE IN TT_Cust

ENDFUNC

* CustList1

****************************************

FUNCTION File2Var

******************

*** Author: Rick Strahl

*** (c) West Wind Technologies, 1995

*** Modified: 01/28/95

*** Function: Takes a file and returns the

*** contents as a string or

*** Takes a string and stores it in a file *** if a second

*** string parameter is specified.

*** Pass: tcFilename - Name of the file

*** tcString - If specified the string *** is stored

*** in the file specified in tcFileName

*** Return: file contents as a string

*******************************

LPARAMETERS tcFileName, tcString

LOCAL lcRetVal, lcOldAlias, lnHandle, lcOldSafety

tcFileName=IIF(type("tcFileName")="C",tcFileName,"")

tcString=IIF(type("tcString")="C",tcString,"")

lcRetVal=""

lcOldAlias=ALIAS()

IF EMPTY(tcString)

  *** File to Text

 

  *** Make sure file exists and can be opened for *** READ operation

  lnHandle=FOPEN(tcFileName,0)

  IF lnHandle#-1

     DO WHILE !FEOF(lnHandle)

         lcRetVal=lcRetVal+FREAD + (lnhandle, 16484)

     ENDDO

     =FCLOSE(lnHandle) && Close the file

  ENDIF

ELSE

  *** Text to File

  lnHandle=FCREATE(tcFileName)

  IF lnHandle=-1

      RETURN .F.

  ENDIF

  =FWRITE(lnHandle,tcString)

  =FCLOSE(lnHandle)

  RETURN .T.

ENDIF

RETURN lcRetVal

*EOP File2Var

 

Fíjate que el servidor está enviando el texto sin ningún tipo de decoraciones: No hay un HTTP Header, lo cual es requerido para que se muestre en un browser. Esto es una cadena de datos delimitados por coma que es una lista de los registros que nos ha devuelto.

Demos un paso más. Una lista delimitada por comas es interesante, pero no funcionará para datos complejos o datos que contienen campos memo. ¿No sería interesante tener un mecanismo automatizado para enviar archivos de datos? Para hacer eso he creado un par de métodos de alto nivel que codifican y decodifican archivos .DBF para poder ser enviados por un HTTP. Hay dos razones para esto: la cabeceara identifica el resultado como un archivo y provee una comprobación mínima para asegurarse que el fichero está completo y en segundo lugar automatiza el proceso de transformar el fichero en una simple cadena. wwIPStuff implementa los métodos EncodeDBF() y DecodeDBF() para hacer esto.

Con estos métodos puedes ahora enviar archivos aún de una manera mucho más fácil – la lógica de convertir el archivo es realizado por el método llamado DecodeDBF y EncodeDBF. En el siguiente ejemplo, el cliente usa DecodeDBF() para decodificar el archivo que ha sido bajado desde el servidor Web:

*** Devuelve todas las compañías que

*** empiezan con "A"

lcText = o.HTTPGet("http://localhost/wconnect/wc.dll?http~CustList2~A")

*** Crea el fichero incluyendo los memo

IF !o.DecodeDBF(lcText,"TCustList.dbf")

RETURN

** DecodeDBF will display

** nowait WAIT win

ENDIF

USE TCustList

BROWSE

USE

ERASE TCustList.dbf

ERASE TCustList.FPT

RETURN

 

En la parte del servidor el código usa EncodeDBF() para codificar una consulta que se ha ejecutado y enviado a través del servidor Web:

 

****************************************

* HTTPDemo :: CustList2

*********************************

*** Function: Returns a customer list

*** based on the URL

*** 'parameter' passed. This time as file!

*** Assume: ***wc.dll?http~CustList1~CompanySearchString

***************************************

FUNCTION CustList2

lcCustToFind = THIS.oCGI.GetCGIParameter(3)

lcFile=SYS(3)+".DBF"

*** This query includes Memos

SELECT custno,Company, Careof, ;

Address, phone ;

FROM (DATAPATH + "TT_Cust") ;

WHERE UPPER(Company) = ;

UPPER(lcCustToFind) ;

ORDER BY Company ;

INTO DBF (lcFile)

USE

o=CREATE("wwIPStuff")

*** Encode with Memo File

lcText=o.EncodeDBF(lcFile,.T.)

*** Send the Delimited string over the wire

THIS.oHTML.Send(lcText)

ERASE (lcFile)

USE IN TT_Cust

ENDFUNC

* CustList1

¡No está mal! En menos de 20 líneas de código para tanto el cliente como el servidor. Con esta tecnología como base puedes actualizar datos a través de la Web de una forma muy fácil. Para hacer esto de una forma más eficiente podrías añadir un control ZIP de terceras partes para codificar y decodificar los datos y comprimirlos con el ZIP en un periquete.

Podrías usar una aplicación estándar VFP para acceder a la información que se ha devuelto por un servidor remoto. Esto podría ser un tipo de link que usara ocasionalmente un timer para actualizar datos que puedas ver en un formulario, o un sistema de operación por petición donde las peticiones del usuario se actualicen pulsando un botón.

 

Pero... ¡Espera, hay más!

Hasta ahora lo único que hemos hecho es pedir datos al servidor, pero tu aplicación podría tener la necesidad de actualizar datos directamente en el servidor. Por ejemplo, podrías tener un vendedor que al final de la jornada se conectara a la Web y entregará todas las gestiones que ha realizado. Para poder realizar esto necesitas un mecanismo para enviar datos al servidor Web desde tu aplicación VFP.

HTTP tiene un mecanismo para enviar datos al servidor llamado POST (actualmente hay varias maneras pero el POST es el más frecuentemente usado). Los datos son codificados (con formato URL). Esto se suele hacer con el POST pero en realidad con el POST puedes enviar cualquier tipo de datos.

Cuando he mostrado el método HTTPGet() he usado de forma simplificada las funciones del WinInet InternetOpenUrl() que manipula muchas de las transacciones de cargar y devolver los resultados. Cuando se usa POST necesitas usar funciones de bajo de nivel para añadir los parámetros adicionales para enviar al buffer. Esto implica llamar a tres métodos separados: HTTPConnect() para conectarse a un servidor, HTTPGetEx() para actualizar o devolver datos y HTTPClose() para cerrar la conexión. El código actual también se toma unos pocos pasos extras a la hora de hacer unas pocas conexiones adicionales a través de WinInet WinInet al ser un mecanismo que funciona a bajo nivel nos permite muchas otras opciones como permitir acceso a una autentificación del nombre del usuario y su password, conexiones seguras vía SSL, las cabeceras HTTP y el buffer del POST.

De cara a enviar datos al servidor necesitas POST datos. La clase wwIPStuff implementa esta funcionalidad mediante un cursor con un campo memo y el método AddPostKey() que se muestra a continuación. ¿Por qué un cursor? Originariamente usaba una propiedad de la clase con formato de carácter, pero desafortunadamente el código saltaba por los aires cuando intentaba usar una cadena demasiado larga (1 mega o más) conteniendo una URL codificada en la propia propiedad. Aparentemente, las propiedades de cadena no pueden tener un tamaño ilimitado. El campo memo resuelve estos problemas pero se requiere alguna lógica para proceder a eliminar los archivos (el borrado se produce en el Destroy del formulario que no lo hemos mostrado aquí).

La propiedad AddPostKey() propiamente codifica los datos y formatea la variable para simular que es un envío de una página HTML. Los datos enviados por POST deben de estar en formato URL codificado, que comprueba todos los caracteres inseguros y convierte el imput en un formato ASCII plano. Seguro en este contexto es sólo A-Z, a-z y 0-9 – todos los otros caracteres son convertidos en sus correspondientes códigos ASCII hex: %0D para un salto de carro (CHR(13)).

Este proceso puede ser muy lento, especialmente si lo haces con código VFP sobre ficheros muy largos de formato binario (recuerda que nuestro fin es enviar ficheros binarios que son imágenes de ficheros DBF). La función URLEncode realiza el proceso de conversión de la cadena. Para hacer este lento proceso un poco más rápido, el archivo wwIPStuff.dll contiene una rutina que lo hace en C cuando el buffer es más grande que un par de miles de bytes.

El ejemplo en la Figura 1.3 es un formulario que contiene un grid en el que puedes escribir una compañía, nombre y mensaje. La idea es que puedas dinámicamente crear la tabla que son enviadas al servidor. Escribe algunos datos en el grid a continuación haz click en el botón 'Send File to Server' para ejecutar el POST y enviar el archivo y una variable llamada CustFile.

El Servidor recibe los datos y decodifica el archivo en su formato .DBF. El Servidor entonces inserta un nuevo registro en la tabla ("desde el propio servidor ") vuelve a re-codificar el archivo y lo envía a su origen que es el formulario VFP en el cliente que muestra el resultado en un grid.

 

Figura 1.3 – Este formulario permite entrar datos en un Grid y envía la información al Servidor Web.

En este ejemplo, el Servidor responde devolviendo un documento HTML que es mostrado con el Browser

Aquí esta el código relevante que se deberá poner en el método Click del botón 'Send':

o=CREATE("wwIPStuff")

wait window nowait "Selecting data to;

send..."

*** Clear the result file and the result grid

SELE TGetDownload

ZAP

THISFORM.Grid2.refresh

*** Select all items from the input cursor

SELECT Company, Name, Message FROM;

TPostTest ;

ORDER BY Company, Name ;

INTO DBF TEMPFILE

USE && Must close before reading

wait window nowait "Encoding data..."

*** Encode the file and memo

lcFileText=o.EncodeDBF("TempFile.dbf",.T.)

*** Create the Post Buffer

o.AddPostKey("CustFile",lcFileText)

*** Init vars that need to be passed by

*** reference

lcBuffer=""

lnSize=100000

wait window nowait "Connecting to site..."

lnResult = o.HTTPConnect("www.west-;

wind.com")

IF lnResult # 0

wait window "HTTPConnect error: ;

"+o.cErrorMsg

RETURN

ENDIF

wait window nowait "Sending data and;

retrieving result file..."

lnResult=o.HTTPGetEx("/wconnect/wc.;

dll?http~SendCustList3",;

@lcBuffer,@lnSize)

IF lnResult # 0

wait window "HTTPGetEx error:;

"+o.cErrorMsg

RETURN

ENDIF

file2var("temp.txt",lcBuffer)

*** Decoding result file from server

lcFileText = ;

o.DecodeDBF(lcBuffer,"TempFile.dbf")

file2var("temp.txt",lcFileText)

SELE TGetDownload

ZAP

APPEND FROM TempFile

GO BOTTOM

THISFORM.Grid2.refresh

ERASE TEMPFILE.DBF

ERASE TEMPFILE.FPT

wait clear

o.HTTPClose()

RETURN

 

En el lado del Servidor sólo se necesitan unas pocas líneas de código. El código del servidor devuelve la variable "CustFile" y decodifica la cadena en la tabla tTempfile.dbf. a partir de aquí puedes hacer lo que quieras con el archivo devuelto por el cliente. En este ejemplo añadiré otro registro a este archivo para indicar que este archivo alcanza al servidor embeviendo el nombre del servidor, mi nombre y un mensaje que contiene los datos y la hora en la que tu lo ves. El archivo actualizado se vuelve a enviar al cliente.

*****************************************

* HTTPDemo :: SendCustList3

*********************************

*** Function: Sends a customer list that's *** encoded

*** in CustFile and displays result as

*** HTML

****************************************

FUNCTION SendCustList3

loHTML=THIS.oHTML

lcFileBuffer = ;

THIS.oCGI.GetFormVar("CustFile")

o=CREATE("wwIPStuff")

IF !o.DecodeDBF(lcFileBuffer,;

"TTempFile.dbf")

THIS.ErrorMsg("Invalid File info")

RETURN

ENDIF

USE TTempFile

INSERT INTO TTempFile(Company, ;

Name, Message) ;

VALUES (THIS.oCGI.GetServerName(),;

"Rick Strahl",;

"Hey there from the server at: "+TIME())

*** Close the file and delete it!

USE In TTempFile

lcFileText=o.EncodeDBF("TTempFile.dbf",.T.)

loHTML.Send(lcFileText)

ERASE TTempFile.DBF

ERASE TTempFile.FPT

RETURN

ENDFUNC

 

Juntando todos los elementos

Ahora tienes todas las piezas para construir una aplicación que pueda correr en Internet como una aplicación Cliente/Servidor usando un protocolo absolutamente abierto y no propietario que pueda acceder a tu servidor remoto desde cualquier lugar en que exista una conexión de Internet disponible.

Si quieres construir aplicaciones tipo offline como lectores de mensajes o si quieres construir aplicaciones con un front-end más sofisticado que el que el HTML te puede dar, esta simple arquitectura client/server hace posible distribuir aplicaciones con las herramientas que conoces..

Una aplicación que funcione sobre un HTTP debería tomar ventaja de las conexiones HTTP de la siguiente forma:

  • Enviando órdenes a través de la línea URL. Por ejemplo: wc.dll?http~ShowCustData~DA1111.
  • Trayendo datos desde un link HTTP mediante HTTPGet
  • Enviando datos al servidor mediante POSTing datos usando el HTTPGetEx

Volvamos atrás, al ejemplo del message board de mi página Web que he mencionado anteriormente. Mi site hospeda un mesage board que es usado para poner mensajes que sirve de soporte para Web Connection y programación general en la Web. La gente accede al site web para ver información online. Yo ahora quiero construir un lector Offline que permita a estos mismos visitantes usar una aplicación VFP para ver los mismo datos. Más que browse el site de la Web los usuarios downlearán mensajes mediante HTTP y los mezclarán en un archivo que ya exista.

Puedes construir un sistema más eficiente en UI con una aplicación VFP que en el Web y el acceso a los datos es mucho más rapido. La aplicación lectora está disponible en

http://www.west-wind.com/wwReader.asp.

 

 

Figura 1.4 - ¿Qué aplicación piensas que es más fácil de usar y más rápida? La aplicación VFP (top) tiene un rico interfaz UI y usa un Treeview para mostrar los datos que están corriendo contra datos locales downleados desde el servidor. La aplicación Web no permite el mismo tipo de flexibilidad como determinarían los mensajes que están pendientes de leer (las marcas rojas), ya que esa información no puede ser guardada en el servidor.

Daremos un vistazo a algunos de los elementos clave del código de esta aplicación. El siguiente código demuestra un mundo real que incluye control de errores y la forma cómo puedes poner esta tecnología a trabajar:

* Método que controla el download

* Pasa una clausula WHERE a la orden SELECT

*

* Filter must be timezone adjusted (in

* Download

* form)

Function DownLoadMessages

LPARAMETER lcDownLoadFilter

loIP=CREATE("wwIPStuff")

*** Add the Download Filter as a POST key

loIP.AddPostKey("Filter",lcDownLoadFilter)

THISFORM.StatusMessage ;

("Downloading Messages...",,1)

lnResult = loIP.HTTPConnect(wwt_cfg.server)

IF lnResult # 0

THISFORM.StatusMessageWAIT WINDOW ;

NOWAIT ("Error: "+loIP.cErrorMsg)

RETURN -1

ENDIF

*** Presize the result buffer

lcBUffer = SPACE(500000)

lnSize = LEN(lcBUffer)

lnStartTime = SECONDS()

lnResult = loIP.HTTPGetEx(;

"/wconnect/wc.dll?wwthreads~;

Downloadmessages",;

@lcBUffer,@lnSize)

IF lnResult # 0

THISFORM.StatusMessage WAIT WINDOW;

NOWAIT ("Error: "+loIP.cErrorMsg)

RETURN -1

ENDIF

IF lcBUffer = "ERROR - No Records"

THISFORM.StatusMessage(;

"No messages to download")

RETURN -1

ENDIF

IF EMPTY(lcBUffer)

THISFORM.StatusMessage(;

"Error: No data was ;

returned by the server...")

RETURN -1

ENDIF

IF !loIP.DecodeDbf(lcBUffer, "TImport.dbf")

THIS.StatusMessage("Error: ;

File Import failed. "+;

"Too many messages!")

RETURN -1

ENDIF

*** All went well - Now import the messages

lnReccount = 0

SELE 0

USE TImport

SCAN

SELE wwThreads

LOCATE FOR Msgid = TImport.Msgid

IF !FOUND()

SELE TImport

SCATTER MEMO MEMVAR

SELE wwThreads

APPEND BLANK

GATHER MEMO MEMVAR

lnReccount = lnReccount + 1

ENDIF

ENDSCAN

USE IN TImport

ERASE TImport.* DBF

ERASE TImport.fpt

*** Refresh the form with the new data

THISFORM.BuildTree()

THISFORM.StatusMessage(;

"Downloaded "+LTRIM(STR;

(lnReccount))+" message(s) in " +;

STR(SECONDS() - lnStartTime,2) + ";

seconds")

RETURN lnReccount

 

Hay un par de notables cuestiones aquí. Este código funciona enviando la petición al servidor con una variable POST llamada FILTER, que es una completa cláusula WHERE de una orden SELECT. Típicamente el filtro contiene un rango de datos que es ajustado según zonas. Pero este código se puede usar también desde una ventana de diálogo simplemente pasando el apropiado parámetro de búsqueda en la expresión de filtro. Esto hace este método reusable para todas las operaciones de que son requeridas por la aplicación.

Ten en cuenta que el buffer de download del HTTP está en un tamaño de 500 kbytes de forma que se pueda captura un archivo razonablemente. Incluso así, 500k únicamente te permitirán traerte 3 semanas de mensajes (de mi BBS) o algo más.

El siguiente asunto a tener en cuenta es el control de errores. Con el download de estos archivos es extremadamente importante comprobar cada llamada que devuelve el Wininet. Si ocurre un error, devolverá un mensaje bastante gracioso al usuario. Una de las cosas que tienes que decidir en el lado del servidor es qué hacer en caso de que se produzca un error. Tiendo a usar una orden simple a la hora de describir las operaciones. Por ejemplo, si corro una petición que pide al servidor que realice una tarea y no devuelva datos, devuelvo 'OK' en caso de éxito y 'ERROR – Mensaje de Error para cualquier error. En caso de que se haya solicitado un archivo se comprueba el tamaño de la cabecera codificada (esto sucede en el DecodeDBF()) que es comprobado en el resultado.

En el lado del Web Connection server el código miraría de una forma parecida a esta:

* HTTPProcess :: DownloadMessages

FUNCTION DownLoadMessages

LOCAL lcFilter, lcToDate, lcFromDate, lcForum

lcFilter = THIS.oCGI.GetFormVar("Filter")

IF EMPTY(lcFilter)

lcForum = THIS.oCGI.GetFormVar("Forum")

lcFromDate = ;

THIS.oCGI.GetFormVar("FromDate")

lcToDate = ;

THIS.oCGI.GetFormVar("ToDate")

lcFilter="Forum='"+PADR(lcForum,30);

+"' AND " +;

"timestamp >= {" + lcFromDate +"};

AND "+;

"timestamp <= {" + lcToDate + "} + 1" ENDIF

lcFile = SYS(3)

SELECT ThreadId, Msgid, Subject, ;

Message, FromName, FromEmail,;

To,Forum,;

TimeStamp ;

FROM (DATAPATH + "wwThreads") ;

WHERE &lcFilter ;

INTO DBF (lcFile)

IF _TALLY < 1

THIS.oHTML.Send("ERROR - No Records")

USE

ERASE (lcFile + ".*")

RETURN

ENDIF

USE

loIP=CREATE("wwIPStuff")

lcFileText=loIP.EncodeDBF;

(lcFile+".dbf",.T.)

IF EMPTY(lcFileText)

THIS.oHTML.Send("ERROR -;

File not encoded.")

ERASE (lcFile + ".*")

RETURN

ENDIF

IF LEN(lcFileText) >= 500000

THIS.oHTML.Send("ERROR -;

File is too large to send.")

ERASE (lcFile + ".*")

RETURN

ENDIF

 

*** This is the actual FILE Send

*** operation!

THIS.oHTML.Send(lcFileText)

 

ERASE (lcFile + ".*")

ENDFUNC

Fíjate que hay que poner el control de error en los lugares apropiados. Los mensajes de error se pueden controlar por el lado del cliente y pueden mostrarse en la línea de estado del lector. Nota en concreto la comprobación del tamaño de 500k. Si el tamaño del archivo es superior a 500k se envía un mensaje al que lo envía más que intentar enviarlo (y fallar en el intento). El cliente y el servidor necesitan estar de acuerdo sobre estos tamaños de cara a que las cosas funciones correctamente.

Para que se realice de una forma correcta aquí está el código del lado del cliente para enviar mensajes al servidor:

FUNCTION UploadMessages

loIP=CREATE("wwIPStuff")

THISFORM.StatusMessage("Uploading Messages...",,1)

SELECT * FROM wwThreads WHERE Post AND !DELETED() INTO DBF TExport

 

IF _TALLY < 1

      USE

ERASE TEXPORT.DBF

ERASE TEXPORT.FPT

THISFORM.StatusMessage("No messagesto upload...")

loAPI=CREATE("wwAPI")

loAPI.Sleep(2000)

ENDIF

 

THISFORM.StatusMessage("Uploading;

"+LTRIM(STR(_Tally))+" ;

Messages...",,1)

USE

lcFileText=loIP.EncodeDBF(;

"TExport.dbf",.T.)

ERASE TEXPORT.DBF

ERASE TEXPORT.FPT

IF EMPTY(lcFileText)

THISFORM.StatusMessage("Invalid ;

File Info - not uploaded")

RETURN

ENDIF

loIP.AddPostKey("FileText",lcFileText)

lnResult = loIP.HTTPConnect;

(wwt_cfg.server)

IF lnResult # 0

THISFORM.StatusMessage(;

"Error: "+loIP.cErrorMsg)

RETUR

ENDIF

lcBUffer = SPACE(500000)

lnSize = LEN(lcBuffer)

lnResult = loIP.HTTPGetEx("/wconnect/;

wc.dll?wwthreads~UploadMessages",;

@lcBuffer,@lnSize)

 

IF lnResult # 0

THISFORM.StatusMessage("Error: "+loIP.cErrorMsg)

RETURN

ENDIF

*** Must check if the Upload went Ok

if lcBuffer # "OK"

*** No - don't delete messages to post

THISFORM.StatusMessage("File Upload Failed")

RETURN

ENDIF

THISFORM.StatusMessage("Deleting;

Posted Messages...",,1)

DELETE FROM wwThreads WHERE Post

THISFORM.StatusMessage()

RETURN

 

Aquí esta el resultado de la operación de POST que simplemente nos devuelve un OK o un código de error que es irrelevante – ya funcione o no.

No te olvides de la Seguridad!

Enviar datos a través de un hilo, especialmente en una conexión Internet abierta, puede ser peligroso. Recuerda que es posible interceptar los datos que viajan por la red con un paquete analizador y potencialmente manipular información sensitiva. Tu primera línea de defensa es usar HTTPS (Seguro HTTP o SSL) para transmitir tus datos a y desde el Servidor web.

HTTPS requiere un certificado de seguridad en el servidor web (Mira la documentación de tu servidor web sobre la forma cómo obtener e instalar un certificado de seguridad o vete a www.verisign.com para firmar uno). Una vez instalado, todos los procesos de comunicación que se produzcan sobre HTTPS son encriptados. Desafortunadamente el proceso de encriptado también ralentiza notablemente el proceso. La clase

wwIPStuff soporta HTTPS usando las funciones de bajo nivel HTTP especificando el cuarto parámetro a .T. para que HTTPConnect() establezca un Link seguro.

FUNCTION HTTPConnect

LPARAMETER lcServer, lcUsername, lcPassword, llSecure

Otro sistema bastante simple de proteger tus datos de un acceso no autorizado es usar passwords. WinInet soporta seguridad mediante el standard Web-based Basic Authentication o mediante el NT Challenge Response que usa los sistemas de seguridad de los dominios del NT.

Te puedes conectar al servidor usando un sistema específico de seguridad que existe sólo mientras estás conectado al servidor . wwIPStuff soporta esto con el segundo y tercer parámetro del HTTPConnect(). En las conexiones Web puedes forzar una solicitud de validar el password de un usuario con el siguiente código:

*** See whether user is already Authenticated

lcUser = THIS.oCGI.GetAuthenticatedUser()

IF EMPTY(lcUser)

*** Nope – force Login dialog

THIS.oHTML.HTMLAuthenticate()

RETURN

ENDIF

HTMLAuthenticate() es un método en la clase Web Connection wwHTML que pide autentificaciones desde el Web server mediante una cabecera HTTP que es devuelta como resultado. El resultado HTTP tendría la siguiente pinta:

HTTP/1.0 401 Not Authorized

WWW-Authenticate: basic realm="localhost"

<HTML><h2>Gotta enter your password to get in!</h2></HTML>

WinInet soporta la petición y envío del username/password al servidor y esencialmente logging el usuario. La petición del Servidor es re-solitidada y esta vez el usuario autentificado y se puede procesar la solicitud. El cierre del cliente es automático con la llamada al HTTPConnect(). En el Servidor tienes que implementar el código anterior para forzar la autentificación.

Otro tema ligado con la seguridad que se debe de tener presente es que los links que download datos típicamente se pueden acceder mediante un Browser directamente. Mientras que a los usuarios no les gusta ver los Links que han llamado, es posible acceder a los mismos links que se podrían usar mediante el Location URL line en el browser si un usuario puede obtener esa línea. Nada como alguien que ha conseguido tu link de upload y enviándote continuamente ficheros hasta que consiga hacer cascar a tu servidor. Considera los nombres de tus URL cuidadosamente y usa Authentication donde se pueda aplicar para evitar estos problemas.

Ten en cuenta que tanto el Secure HTTP como el Authentication no funcionarán con el plain HTTPGet. Si quieres únicamente obtener datos de una forma segura tendrás que usar HTTPConnect() y HTTPGetEx().

 

Acerca de otros Servidores Web

Para las conexiones he usado como respaldo el Web Connection en todos los ejemplos anteriores, ya que es lo que uso para la mayor parte de mis desarrollos. Si usas otras herramientas como FoxISAPI o Active Server Pages necesitarás hacer algunos pequeños cambios. No he comprobado herramientas como FoxWeb o X-Works – deberás comprobarlas con los autores para saber si soportan el envío de ficheros binarios.

El proceso para FoxISAPI es idéntico que el que se usa en el Web Connection y se usan las mismas clases para generar tus datos binarios y enviarlos través de la red. Para enviar una petición únicamente devuelves la cadena como un resultado desde tu método FoxISAPI Automation server. Sin embargo, la versión de FoxISAPI que va con VFP no soporta datos binarios que contengan NULLs (chr(0)) como la función de conversión Variant que envía tu output al Web server y así se trunca cualquier cadena en el NULL. Yo he retocado FoxISAPI.dll para arreglar esto y la versión actualizada y compilada está en wwIPStuff ZIP.

Con las páginas Activas el problema es más complicado pues VBScript usa Variants double byte internamente lo que puede causar problemas con datos binarios. Las cadenas Variant están terminadas en nulos de forma que cuando envias un cadena binaria como output ellos terminan como NULL. Puedes evitar este problema usando el método BinaryWrite en lugar del basico Write. Este método directamente emite el resultado en un formato de cadena sin convertirlo primero a un formato double byte de VB.

Desafortunadamente, he sido incapaza de enviar datos binarios desde dentro de Visual FoxPro a través de una página ASP. El problema es que los ASP convierten el resultado de VFP Automation server en Variant lo que trunca los resultados a cualquier NULL que se encuentre ya que VFP no puede devolver una cadena como resultado (como puedes hacer con los objetos COM creados desde C, Delphi o VB created) el resultado es siempre un variant double byte. Si se envía estos variant con BinaryWrite enviará dos bytes por caracter mientras Write se truncaría en los NULLs. Esta limitación significa que tienes que enviar los datos no binarios (como cadenas delimitadas por comas o como cadenas codificadas mediante el URLEncoded) desde cualquier VFP Automation server.

 

Algunas observaciones sobre el WinInet

Hay algunos temas que deberías conocer a la hora de usar las funciones HTTP del WinInet y que deberías tener presente a la hora de realizar operaciones de transferencia.

Aunque la clase wwIPStuff contiene soporte para conectarse, recibir y enviar timeouts (WinInetSetTimeout()), una petición que se cuelgue en medio de una conexión no hace un timeout de acuerdo con estos valores. En vez de lo que suele ser habitual de los sistemas Windows que por defecto (tipicamente 30-40) segundos se suelen aplicar por WinInet. Esto podría ser un problema debido a que las funciones de WinInet no te dan un feedback mientras se están procesando y el usuario se podría pensar que se ha colgado. Microsoft dice que esto es por diseño, debido a que normalmente los Web servers típicamente controlan los timeouts – basados en el servidor Web server- para permitir largas peticiones que sea necesario completar. Te sugiero que compruebes tus peticiones en diversos entornos para ver en qué forma esto puede afectar a tu aplicación.

El envío de ficheros especialmente gordos no es una buena idea a nos ser que tengas una conexión rápida. El ancho de banda es siempre crítico, pero necesitas también pensar en algún tipo de limitaciones en el sistema. El proceso de URLEncoding a la hora de crear la cadena es lento si se usa código FoxPro para hacer el proceso de codificado. Esta es la razón por la que en caso de estar conectados online se junte una .DLL para realizar estas operaciones de una forma más rápida.

WinInet también requiere que toda la información se pase en buffers de tipo carácter y he tenido algunos problemas con archivos muy grandes que sobrepasaban la capacidad de memoria de VFP(solía suceder con los que tenían un tamaño un poco inferior a un Mega). Por ejemplo,

en mi message board he establecido un buffer de 500k bytes para recibir mensajes. Mientras eso puede parecer mucho si alguien intenta llevarse todos los mensajes desde la última vez que se conectó con facilidad se llega a las 500 K. Lo peor es que no hay forma de saber el tamaño que te va a venir si no realizas dos intentos: uno para saber cuantos te van a venir y otro para traértelos. Una vez que has empezado a traértelos no hay un feedback del progreso y si se produce un error tienes que volver a empezar desde el principio. Además del tamaño de los datos, el proceso URLEncoding también hace tus datos mucho más grandes - hasta tres veces más grandes que el original, un simple carácter puede ser cambiado por tres (%OD por ejemplo para un Salto de Carro (Chr(13)).

Puedes también conseguir reducir el tamaño mediante el uso de productos de terceras partes como el DynaZip. Los .DBF son un primer candidato para todo esto pues la comprensión puede llegar al 80% o incluso más para algunas tablas. La BBS actualmente usa DynaZip para realizar procesos de comprensión y este simple paso nos ha traído notables mejoras a la hora de poder trasladar más de 200 mensajes a 28.8 en menos de un minuto.

En referencia a las trasferencias de archivos, he tenido algunos problemas con WinInet cuando se accedía a muchos lugares diferentes en una rápida sucesión. Por ejemplo al usar wwIPStuff para construir un Web Crawler o para verificar los links de determinadas Webs, podrías realizar varias peticiones en una rápida sucesión. WinInet realiza algunas operaciones en background en un thread diferente y algunas veces cuanto se realiza una conexión rápida y una rápida desconexión desde diferentes sitios algunos threads no se borran correctamente.

Es tu turno

La implementación de esta arquitectura no significa que se produzca un remplazamiento de las aplicaciones del tipo HTML. Al actuar de esta forma estás obligando a todos los clientes a tener Win95/NT y el runtime de Visual FoxPro, así que todas las ventajas que los clientes ligeros y las ventajas que el HTML nos trae no se pueden aplicar. Pero ganas las posibilidades de hacer un entorno distribuido y que tus aplicaciones alcancen y se comuniquen con usuarios en cualquier lugar.

Si estás construyendo aplicaciones que necesitan comunicarse en un entorno muy abierto,

ésta es una forma fácil de vincular aplicaciones a unos lugares determinados. Si estás realizando ocasionales envíos de datos en el Web o solicitando datos en tiempo real te fijarás en los aspectos distribuidos de las aplicaciones Web sin tener que estar constreñido por la limitaciones de las páginas HTML.

La gran ventaja es que puedes tomar partido del rico interfaz de VFP y de sus herramientas para hacer un entorno más productivo y aún proveer la posibilidad de distribuir plug-in desde cualquier sitio en la red. Yo uso este front end para muchas de las operaciones de mantenimiento, desde downloading pedidos desde mi Web directamente a una aplicación de Punto de venta, para comprobar mis logs de error en varios sitios al mismo tiempo downlenadolos a mi máquina. En todos estos usos el front-end VFP es sólo una extensión de las existentes aplicaciones que usan el HTML. Las posibilidades son sin límites y muchas empresas están ya tomando partido por este tipo de interfaz.

 

Por Rick Strahl, West Wind Technologies

http://www.west-wind.com/