Hola, les saluda Luis y aquí les traigo este nuevo tutorial.
En la ingeniería de software nos encantan las abstracciones. Cuidan los tediosos detalles y nos permiten poner nuestra atención donde corresponde. Sin embargo, es valioso comprender cómo hacen lo que hacen (sigue este consejo de Joel). Siguiendo esta guía, decidí abordar una vergonzosa brecha de larga data en mis conocimientos; traducir paquetes tcp en respuestas HTTP.
Esta publicación describe los pasos para escribir un cliente HTTP similar a curl. Los pasos son;
- Creando un enchufe
- Estableciendo una conexión tcp
- Envío de una solicitud http
- Leyendo la respuesta http
Los ejemplos de código de esta publicación están en C. El propósito de esta elección es estar lo más cerca posible de las llamadas al sistema que proporciona el kernel.
El boilerplate
Ahora que estamos bajo el capó, estamos expuestos a la sobrecarga de establecer una conexión tcp. Primero necesitamos crear un socket. Luego usa eso para iniciar un apretón de manos tcp.
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
El ejemplo anterior usa el constructor de socket de la biblioteca estándar de C. El tipo `SOCK_STREAM` refleja la naturaleza orientada a la transmisión del protocolo tcp. Esto será relevante más adelante.
Un servidor web es una máquina que espera a que los clientes se conecten a él. Hacemos eso con el conectar mando. Este comando le dice al kernel que inicie el protocolo de enlace mencionado anteriormente. El apretón de manos es un proceso costoso. Por lo tanto, los clientes http suelen proporcionar formas de optimizarlo. Nuestra implementación ingenua no hará eso.
int sockfd, portno; // port is 80 struct sockaddr_in serv_addr; struct hostent *server; server = gethostbyname(“www.wikipedia.com"); if (server == NULL) { fprintf(stderr,”ERROR, no such hostn”); exit(0); } bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length); serv_addr.sin_port = htons(portno); if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error(“ERROR connecting”);
Lo que pasa arriba es eso; nosotros resuelve el nombre de host en una dirección, usamos eso y el número de puerto para crear una dirección de socket, usamos la dirección del socket para conectarnos al host. Si las construcciones del lenguaje C no le resultan familiares, no se preocupe. Es poco probable que alguna vez necesite aprenderlos. Si eres un curioso indefenso, Aquí están todos los detalles desearías no haberlo pedido.
Acabamos de superar la barrera de entrada de tcp. Estamos listos para la primera solicitud. Iniciaremos la conversación con la línea de recogida más conocida en el libro de jugadas http. La solicitud `GET /`. Cada servidor se enamora de eso.
char get_req[] = “GET / HTTP/1.1rnrn”; int byte_count = write(sockfd, get_req, strlen(get_req)); if (byte_count < 0) error(“ERROR writing to socket”);
¡A la parte divertida! Empecemos a leer RFC 2616 para entender lo que está pasando. La cadena de solicitud sigue la estructura identificada en sección 5. No tiene encabezados, lo que también indica que no hay cuerpo de mensaje. De ahí el doble ` r n` (CRLF) marca el final de la solicitud.
El siguiente es el escribir mando. Además del socket y la cadena de solicitud, este comando espera la longitud de la entrada. En lenguajes de alto nivel, esto se hace por ti (ver el método de envío de Python). En C, la firma del comando refleja la llamada al sistema. Es por eso que estamos escribiendo en C.
El regreso
Es hora de recibir nuestra primera respuesta http. Aquí es donde la naturaleza orientada a la transmisión de tcp hace que las cosas sean interesantes.
En un flujo de datos no existe la noción de un mensaje individual. Tcp no proporciona límites entre un fragmento de datos y otro. Por tanto, al igual que el comando de escritura, leer el comando también espera un argumento de longitud (recuento).
El argumento de conteo le dice al kernel cuánto del flujo de bytes entrante quiere leer el usuario. El kernel lee esta cantidad de datos desde el principio de la transmisión. Elimina los datos de la transmisión y se los devuelve a la persona que llama. La siguiente llamada de lectura repite esta operación, comenzando desde el nuevo comienzo.
Un escenario común es que la cantidad de datos que estamos solicitando aún no haya llegado al socket. En ese caso, el kernel devuelve tantos datos como el socket. También devuelve la longitud de los datos que leyó de la secuencia. Depende de la persona que llama manejar las respuestas parciales. Un enfoque es sondear el socket hasta que los datos estén disponibles (o se haya alcanzado un tiempo de espera).
Muy bien, volvamos a nuestra respuesta http.
Bienvenido HTTP
El desafío es que los mensajes http tienen una longitud variable. Para comprender cuánto debemos leer de la secuencia, debemos inspeccionar la especificación http. Sección 6 define la estructura de respuesta http como;
En pocas palabras, la respuesta http tiene una línea de estado, una sección de encabezados (puede estar vacía) y un cuerpo de respuesta (opcional). La sección de encabezados está separada del cuerpo del mensaje por el mismo delimitador CRLF que usamos en la solicitud. Además, la línea de estado y cada encabezado terminan con el mismo delimitador.
El cuerpo del mensaje no termina con un delimitador. En cambio, el protocolo http proporciona formas alternativas de informar al cliente. Uno de ellos es el encabezado de longitud de contenido. Sección 4-4 explica otros métodos.
Bien, armados con este conocimiento, podemos elaborar un plan de juego para las respuestas http que incluya un encabezado de longitud de contenido;
- Leer desde el socket hasta el primer delimitador, esta es la línea de estado
- Leer desde el socket hasta el siguiente delimitador, este es el primer encabezado
- Repita el último paso hasta que nos encontremos con dos delimitadores consecutivos, este es el final de la sección de encabezados
- Lea la longitud del contenido de los encabezados, lea todos los datos que sugiera la longitud del socket, este es el cuerpo del mensaje
- Misión cumplida, leemos el mensaje completo exacto, ni un byte menos o más
Un último obstáculo que hay que abordar es leer el zócalo hasta cierto delimitador. El comando de lectura no proporciona tal funcionalidad. Por tanto, tenemos que leer un fragmento de tamaño fijo. Busque los datos para el delimitador. Si no lo encuentra, cargue otro fragmento. En algún momento recibiremos el delimitador.
Una vez que se encuentra el delimitador, podemos procesar todos los datos hasta ese punto. La implementación debe mantener una referencia de los datos que vienen después del delimitador. Estos datos constituyen el comienzo de la siguiente estructura en la respuesta.
A continuación se muestra una implementación simplificada;
void parse_headers(int sockfd, char *buffer, char *content_length) { bool reached_message_body = false; while(!reached_message_body){ // append CHUNK_SIZE data to the buffer load_buffer(sockfd, buffer, CHUNK_SIZE); // look for the delimiter index in the buffer, // if not found return -1 int next_line_start = find_next_line(buffer); // loop while there is a delimiter and // we have not reached to the message body yet while(next_line_start != -1 && !reached_message_body) { log_header(buffer, next_line_start); // keep a reference of the content length header // for reading the message body copy_if_content_length(buffer, next_line_start, content_length); // remove the parsed headers from the buffer release_line(buffer, next_line_start); // check if we reached the start of the message body // this looks for two delimiters back to back reached_message_body = is_message_body_start(buffer); // we might have read multiple headers in one read. // before we read more data check if // there are any other headers we can consume if(!reached_message_body) { next_line_start = find_next_line(buffer); } } } }
El resto de la implementación está disponible en github en grandbora / knowledge_gap . Maneja solo las respuestas http que tienen un encabezado de longitud de contenido. Ampliando la funcionalidad para cubrir respuestas http fragmentadas se deja como ejercicio para el usuario.
Gracias por leer.
Añadir comentario