Una de las prácticas de la carrera, de la asignatura Redes II, consistía en hacer un servidor HTTP básico, que manejara comandos GET, POST, y mostrara páginas y ficheros de cualquier tipo. Esto en C es un auténtico coñazo, ya que tardas más tiempo pegándote con los punteros que trabajando en la solución, así que casi que además voy a hacer una copia en C#, y lo dejo por escrito ;).
Las bases
Está basado en el ejemplo que se define en la Documentación de la clase TcpListener en MDSN Library
Lo primero que necesitamos es conocer la estructura, o por lo menos el funcionamiento básico de un servidor web:
- Se crea un enlace con una IP y un puerto, donde nos mantendremos a la escucha de datos.
- Cuando recibamos datos, aceptamos la conexión y obtenemos la cadena de datos que nos pasan.
- Analizamos (Parse) la cadena de datos, obteniendo información de interés como:
- Instrucción (GET, POST, …)
- Ruta (fichero al que se aplica la instrucción)
- Protocolo (HTTP/1.1, HTTP/1.0 …)
- Creamos una respuesta acorde a la petición recibida
- Cerramos la conexión, quedandonos a la espera de nuevas peticiones.
Todas estas opciones están definidas para un servidor que opere en un único hilo de manera tal que no se pueden realizar peticiones de manera concurrente.
Traducido en código…
En este post haremos una escucha simple, detectaremos la barra «/» como sinónimo de home y mostraremos mediante el navegador una página web específica. Es un ejemplo simple, pero habrá tiempo para complicarlo:
Lo primero es definir la dirección IP y el puerto en el que vamos a permanecer a la escucha
Int32 port = 5000; //O cualquier otro siempre que no interfiera con los ya existentes
IPAddress localAddr = IPAddress.Parse("127.0.0.1"); //Nos mantenemos a la escucha en "localhost"
Posteriormente inicializamos nuestro Listener e iniciamos el servidor:
TcpListener server = null; server = new TcpListener(localAddr, port); server.Start(); //Nos mantenemos a la espera de nuevas peticiones Byte[] bytes = new Byte[1000]; //Array donde guardaremos el resultado String data = null; //Cadena de caracteres que contendrá los datos una vez procesados
Queremos mantenernos a la espera de nuevas conexiones, con lo cual el resto del código lo vamos a colocar dentro de un bucle infinito. Una vez recibida la conexión el siguiente paso será aceptarla:
while (true)
{
Console.Write("Waiting for a connection... ");
TcpClient client = server.AcceptTcpClient(); //Aceptamos la conexión entrante
Console.WriteLine("Connected!");
... //El resto del código dentro de este bucle
}
Una vez hemos aceptado la conexión, nos interesará saber qué estamos aceptando, para lo que necesitamos un stream con la información a procesar, en este caso un NetworkStream.
Tras definirlo, haremos una lectura del stream en el array bytes que hemos definido anteriormente, y lo convertiremos a una cadena de caracteres ASCII con la que podremos trabajar:
NetworkStream stream = client.GetStream(); //Obtenemos el stream
int i = stream.Read(bytes, 0, bytes.Length); //Leemos en el array "bytes" y almacenamos en i el numero de bytes leidos.
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i); //Convertimos la cadena
Console.WriteLine("Received: {0}", data); //Mostramos por pantalla el resultado.
La cabecera HTTP en detalle
Hasta aquí hemos creado un enlace, hemos abierto una conexión y hemos recibido un paquete, pero… qué hemos recibido? Os presento una cabecera HTTP:
Waiting for a connection... Connected! Received: GET / HTTP/1.1 Host: localhost:5000 Connection: keep-alive Cache-Control: max-age=0 User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 ( KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16 Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,i mage/png,*/*;q=0.5 Accept-Encoding: gzip,deflate,sdch Accept-Language: es-ES,es;q=0.8,en;q=0.6,en-GB;q=0.4 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
En este caso lo que nos interesa es la primera línea, que nos devuelve los tres datos que queríamos saber:
- Instrucción: GET
- Fichero: /
- Protocolo: HTTP/1.1
Lo que viene a decir es: Quiero que me des el fichero / usando la versión 1.1 del protocolo HTTP. Eso lo vemos nosotros, pero para que el programa lo entienda necesitamos echarle una mano.
//Dividimos el mensaje en un array de strings
var lista = data.Split(' ');
//Por si acaso la petición viene vacía.
if (lista.Length < 3) continue;
//El primer elemento de la lista será la instrucción
var instruccion = lista[0];
//El segundo elemento de la lista será la ruta
var ruta = lista[1];
//El tercer elemento antes del salto de carro, será el protocolo.
string protocolo = lista[2].Split('\n')[0];
//Finalmente mostramos los datos por pantalla
Console.WriteLine("Instruccion: {0}\nRuta: {1}\nProtocolo: {2}", instruccion, ruta, protocolo);
Para completar esta primera aproximación vamos a ver como, una vez tenemos la solicitud, podemos generar una respuesta acorde. Vamos a devolver un fichero por defecto si nos piden la raíz, y una respuesta estándar 404 si nos solicitan otro fichero.
byte[] msg;
//Comprobamos que estemos recibiendo la peticion de la home
if (ruta.Equals("/"))
{
//Leemos todo el contenido del fichero especificado
var fichero = File.ReadAllText("home.html");
//Redactamos la cabecera de respuesta.
string response = "HTTP/1.1 200 OK\r\n\r\n\r\n";
//Agregamos a la cabecera la informacion del fichero.
response = response + fichero;
//Mostramos por pantalla el resultado
Console.WriteLine("Sent: {0}", response);
//Codificamos el texto que hemos cargado en un array de bytes
msg = System.Text.Encoding.ASCII.GetBytes(response);
//Escribimos en el stream el mensaje codiificado
stream.Write(msg, 0, msg.Length);
}
else {
//Redactamos una cabecera de fichero no encontrado
string response = "HTTP/1.1 404 Not Found";
//Mostramos por pantalla el resultado
Console.WriteLine("Sent: {0}", response);
//Codificamos, exactamente igual que en la parte superior
msg = System.Text.Encoding.ASCII.GetBytes(response);
//Escribimos en el stream el mensaje codificado
stream.Write(msg, 0, msg.Length);
}
Finalmente, y no menos importante, cerramos la conexión:
client.Close();
En el momento en que cerremos la petición, se enviará todo el contenido al cliente y veremos el contenido de la página en nuestro navegador favorito.
Resumen
Hemos creado un servidor web sencillo, que se quede a la espera de un cliente y devuelva una única página. La utilidad es discutible, pero hemos visto cómo recibir una cabecera HTTP, procesar los datos, y devolver una respuesta estandar.
Características adicionales
Esto, como hemos visto anteriormente, es una primera, y muy básica aproximación a un servidor web. Quedarían pendientes para futuras entregas:
- Multi-threading: Poder procesar más de una petición de manera simultánea.
- Acceder al contenido de un directorio concreto, como hacen los servidores web reales.
- Generar una cabecera HTTP más completa y estándar, ya que nos han faltado cosas como el Content-type, o el Content-length, entre otros.
- Cargar y guardar ficheros de configuración, con la ruta del fichero encargado de /
- Interpretar cabeceras POST
El código fuente completo (Proyecto de Visual Studio 2010) está disponible aquí
Actualización: El código fuente también se encuentra disponible en GitHub
Espero que os haya resultado interesante.
Replica a Roberto Luis Bisbé Cancelar la respuesta