Creando un servidor HTTP básico con C#

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:

  1. Se crea un enlace con una IP y un puerto, donde nos mantendremos a la escucha de datos.
  2. Cuando recibamos datos, aceptamos la conexión y obtenemos la cadena de datos que nos pasan.
  3. 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 …)
  4. Creamos una respuesta acorde a la petición recibida
  5. 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.

Autor: Roberto Luis Bisbé

Software Developer, Computer Engineer

6 opiniones en “Creando un servidor HTTP básico con C#”

  1. hola tengo una duda, yo necesito de una aplicacion que me envia datos por http post, resivirlos y guardarlos en una base de dato, este codigo tuyo me serviria, por que no me es claro, como depurarlo

    1. Está bastante claro el código y más con la explicación, eso de «depurarlo» me suena más a «como se hace» así como redactas tu duda suena fácil ya que solo bastaría tomar los datos de la variable ‘lista’ para que los registre en tu base de datos como información recibida y en cuanto los datos enviados es aun más fácil ya que tu eres el que facilita dicha petición así que en el punto donde se asigna la variable ‘response’ tienes lo que envías, así que allí deberías mandarlo también a tu BD para cumplir tus fines.

      Por cierto gracias Roberto por el ejemplo, esta muy bueno como base.

  2. Wow genial :D es mucho más fácil de lo que pensaba, solo tengo unas preguntas:

    1) Como se haría para cargar en el URL del explorador el archivo que yo quisiera?
    2) Como puedo implementar ese código para que no toque especicificar el puerto?, osea que yo escriba localhost y ya :P

    Gracias.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.