En el capítol anterior ja hem vist quines eren les característiques més rellevants del protocol HTTP, i també que els servidors Web s'implementen seguint l'esquema client/servidor donant servei a múltiples clients, de manera que ara ja podem establir els requisits que haurà de tenir el servidor XicHttpd. Començarem doncs per analitzar-ne les característiques essencials, i a continuació establirem les funcionalitats addicionals que se li volen implementar.
Cal implementar la part servidor de l'esquema client/servidor sobre el protocol TCP/IP.
El servidor ha de restar a l'espera de rebre connexions al port HTTP per defecte (port 80). Tanmateix s'hauria de permetre de canviar aquest port estàticament, és a dir, abans de l'execució del servidor ja sigui com a paràmetre de la línia de comandes, o per mitjà d'algun fitxer de configuració.
S'han de poder atendre simultàniament les peticions de múltiples clients. Això requerirà d'algun mecanisme que permeti l'atenció paral·lela de cada connexió.
Un cop acceptada la connexió d'un nou client, tot l'intercanvi d'informació de la capa d'aplicació entre aquest i el servidor es farà seguint la versió HTTP/1.1 del protocol HTTP. Donat que en aquesta versió es contempla la persistència per defecte, caldrà que el servidor tanqui les connexions després d'un cert temps d'inactivitat. En el cas que les peticions facin referència a una altra versió, el servidor haurà de respondre amb el codi d'estat 505 HTTP Version Not Supported.
Els mètodes HTTP inicialment acceptats per aquest servidor seran GET i HEAD. En cas de rebren algun altre, el servidor haurà de respondre amb el codi d'estat 501 Not Implemented.
Totes les peticions faran referència a un recurs situat en el sistema de fitxers de la màquina on s'executa el servidor, de manera que el servidor haurà de demanar el recurs al sistema de fitxers i servir-lo al client (mètode GET), o informar-ne de les característiques (mètode HEAD). Es poden donar dos situacions d'error: o bé que no es tingui permís per accedir al fitxer (o el directori on es troba), o bé que el fitxer no existeixi; en ambdós casos el servidor respondrà amb el codi d'estat 404 Not Found ja que en aquesta versió no es fa distinció de clients, ni es suporta cap tipus d'autentificació.
Per cada petició caldrà que el servidor en verifiqui la sintaxis. Algunes situacions d'error que es podrien donar són:
L'identificador del recurs (Request-URI) és massa gran. El servidor respon amb el codi d'estat 414 Request-URI Too Long.
Error general de sintàxis, o falta la capçalera Host. El servidor respon amb el codi d'estat 400 Bad Request.
Les capçaleres que incorporaran els missatges de resposta són les següents:
Date : Amb el valor de la data i hora local del servidor en format GMT.
Server : El nom i la versió del servidor Web, i la plataforma on s'executa.
Last-Modified : La data i hora de la última modificació del recurs demanat.
Content-Length : La mida en bytes del recurs demanat, si es tracta d'un GET serà la mida del cos del missatge (entity-body).
Content-Type : El tipus MIME del recurs demanat.
Content-Encoding : Aquesta capçalera només hi serà present en el cas que s'hagi comprimit el cos del missatge, per tant primer caldrà que el client accepti aquest tipus de transferència. El seu valor serà gzip (veure següent apartat).
Cal tenir present, que existeix la possibilitat de passar paràmetres per mitjà de la URL a les pàgines Web (pas per GET), caldrà doncs que a l'hora de descodificar l'URI es tingui en compte que el caràcter separador és '?'.
Pipelining. Es permetrà que el client faci múltiples peticions enlloc d'haver-se d'esperar per cada resposta, així s'aconsegueix mantenir la finestra TCP/IP el màxim de plena possible, reduint així els temps d'inactivitat de la connexió.
Compressió. Amb l'objectiu de reduir el volum de tràfic que circula per la xarxa, sempre que sigui possible s'enviarà el cos del les respostes (entity-body) comprimit. Per tant, caldrà que el client accepti aquest tipus de transferència, així el servidor haurà de comprovar que la petició contingui la capçalera Accept-Encoding amb el valor gzip (de moment no es contemplen ni compress ni deflate), i el servidor enviarà el cos del missatge comprimit amb gzip i la capçalera Content-Encoding: gzip. En cas contrari el servidor enviarà el recurs tal qual es troba en el sistema de fitxers prescindint de la capçalera Content-Encoding.
Logs. El servidor registrarà una serie d'informació útil per als administradors. A continuació es descriu el contingut de cada fitxer:
acces.log: Contindrà a cada línia l'adreça IP de cada client, la data i l'hora local del servidor, la primera línia de la petició i el codi d'estat de la resposta del servidor en el següent format:
Adreça-IP - [Hora-Local] "Mètode Recurs Versió" "Codi-Resposta Descripció"Per exemple:
80.36.24.66 - [Sun. 02 Nov 2003 11:55:38 GMT] "GET /index.html HTTP/1.1" "HTTP/1.1 200 OK"
errors.log: Contindrà els errors que es puguin produir en l'execució del servidor. De moment el seu format és lliure, i quedarà definit en la fase de programació però com a mínim hauria d'inclouré un codi d'error amb una breu descripció del mateix.
Durant les primeres setmanes de recollida d'informació, vaig poder veure algunes implementacions del mètode POST que no tenien res a veure amb els estàndards CGI[1], ni amb el pas de paràmetres a llenguatges d'script. Donat que el servidor s'executarà en un entorn controlat, si es disposa de temps s'intentarà fer una implementació "particular" del mètode POST definint les regles bàsiques que hauria de tenir l'aplicació que en processi les dades. |
Podem distingir dues parts diferenciades a partir dels requisits anteriors, així tenim que els tres primers fan referència a la implementació del servidor, i la resta a la implementació del protocol.
Per satisfer el tercer requisit es podrien utilitzar subprocessos, utilitzar algun mecanisme de selecció de connexions (com select de la llibreria sockets del C) o bé utilitzar fils d'execució diferenciats. S'obta pels fils d'execució, ja que seleccionant connexions en realitat no tindríem paral·lelisme, i utilitzant subprocessos correm el risc de sobrecarregar la màquina en excés en càrregues altes, a part de la penalització que provoca la creació del mateix (crida al sistema, assignació de memòria, etc), a banda que totes les comunicacions entre processos es farien per mitjà de crides al sistema (PIPE, mmap, IPC, signals, etc.). D'altra banda els fils comparteixen per defecte l'espai de memòria, ens ofereixen mecanismes d'accés segur a les variables compartides, i un control adient de cada fil, tot per mitjà de les funcions de la llibreria escollida. Un altre tema és el tractament que faci el sistema operatiu dels fils, però aquesta discussió queda lluny de l'objectiu d'aquest document.
Tenint en compte el que s'ha escollit, inicialment es crearan una serie de fils estàticament que s'assignaran a cada connexió entrant, un cop estiguin tots assignats sen crearan de nous dinàmicament per atendre les noves connexions, això si, prioritzant sempre els fils estàtics que ja hagin acabat la seva tasca. Cal notar que si es permet de configurar la quantitat de fils estàtics i dinàmics, així com el timeout de les connexions podrem fer un tunning del servidor i comparar els resultats dels benchmarks escollits.
Vegem doncs els diagrames de flux sobre el funcionament del servidor Web. En primer lloc tenim el diagrama del fil principal fins a la creació/assignació dels fils per cada connexió, el següent fa referència a cada fil que és on estarà implementat el protocol HTTP.
Vegem a continuació la descripció de tasques a realitzar pel procés principal del servidor Web, a partir del diagrama de flux següent:
Inicialització. Es llegiran les dades del fitxer de configuració que es creguin oportunes. Aquestes quedaran completament definides en la fase de disseny però com a mínim hi haurà el nombre de threads estàtics que es crearan, el valor del timeout, el nombre màxim de clients que es permeten i el port on haurà d'escoltar el servidor si és diferent del port HTTP per defecte. També es crearan les estructures de dades necessàries i s'inicialitzaran les variables globals que calguin per al funcionament del servidor.
Es crearà el socket del servidor per tal que escolti en el port especificat.
Caldrà també definir els senyals (signals) acceptats pel servidor per tal que el sistema operatiu el pugui tancar de manera segura.
Crear Pool de Threads. Es crearan i s'inicialitzaran tants threads com s'indiqui al fitxer de configuració deixant-los preparats per quan arribin connexions
Esperar Connexions. El servidor entrarà en un bucle infinit deixant el socket preparat per rebre les connexions. Quan arribi una connexió se li assignarà un thread del pool estàtic si n'hi ha de lliures, i si no sen crearà un de nou sempre i quan no s'hagi arribat al màxim de clients. El thread assignat rebrà per paràmetre l'identificador de la connexió del client, a més dels que defineixi la llibreria utilitzada (per exemple les funcions a executar).
A continuació tenim el diagrama de flux de les tasques que haurà d'executar cada thread des de que se li assigna una connexió, fins que aquesta es tanca:
Descodificar Request. Es llegirà la petició que arriba pel socket del client, per ordre de lectura farem les següents comprovacions:
Si el mètode no està implementat retornarem un Response amb el codi d'estat 501 Not Implemented.
Si la mida de l'URI és massa gran per la mida definida en el programa el codi d'estat de la resposta serà 414 Request-URI Too Long.
Si la versió del protocol indicada és diferent a HTTP/1.1 retornarem la resposta amb el codi d'estat 505 HTTP Version Not Supported.
Si falta la capçalera Host, o bé es detecta qualsevol altre malformació sintàctica durant tota la lectura de la petició, es retornarà la resposta amb el codi d'estat 400 Bad Request.
A mesura que es llegeixen les capçaleres de la petició, s'aniran descartant totes excepte el Host ja comentat, i la capçalera Accept-Encoding, la qual utilitzarem per determinar si es pot enviar el recurs comprimit amb gzip o no.
Cercar Recurs. S'intentarà accedir al recurs demanat en la petició, si el recurs no existeix es retornarà la resposta amb el codi d'estat 404 Not Found, si d'altra banda intentem accedir a un directori on no es té permís, o bé no es té permís sobre el recurs demanat el codi d'estat de la resposta serà 403 Forbidden. Si es troba el recurs es procedirà de la següent manera:
Si s'accepta compressió, es comprimirà el recurs per determinar-ne la mida i omplir la capçalera Content-Length, en aquest cas caldrà afegir també la capçalera i el valor Content-Encoding: gzip, sinó s'omplirà amb la primera amb la mida del fitxer sense comprimir.
S'afegira al missatge de resposta, les capçaleres Date, Server, Last-Modified, Content-Type, a més de les determinades de l'apartat anterior.
El codi d'estat de la resposta serà 200 OK, si es tracta de la resposta a un HEAD s'enviarà el missatge, si la petició era un GET s'adjuntarà el recurs, comprimit o no segons calgui, després d'una línia en blanc en el mateix missatge.
Servir Recurs i Servir Informació. Queda determinat pel punt anterior.
Escriu al Log. Cada vegada que s'envia una resposta s'enregistra al fitxer acces.log seguint el format que hem detallat en l'apartat de funcionalitats addicionals.
Activa Timeout. Cada vegada que s'envia una resposta s'activarà el timeout per tancar la connexió si no arriben més peticions en el temps establert.
Quan s'implementi el Pipelining hi haurà una cua de peticions, de manera que fins que no es respongui a la última no s'activarà el timeout. |
[1] | CGI és l'acrònim de Common Gateway Interface |