-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial 5: Comunicação com HTTP
Seja bem-vindo ao tutorial de comunicação com servidor com HTTP! ☕ O Hypertext Transfer Protocol é o protocolo base de qualquer troca de dados na Web entre clientes e servidores. As requisições são iniciadas pelo destinatário (cliente), geralmente um navegador da Web. O vídeo relacionado a este tutorial, apresentado pela aluna Ana Carolina, pode ser assistido clicando aqui.
No modelo OSI (Open Systems Interconnection) de redes de computadores, o HTTP é implementado na sétima camada, a camada de Aplicação, responsável por prover serviços para aplicações de modo a separar a existência de comunicação em rede entre processos de diferentes computadores. Protocolos dessa camada atuam junto aos protocolos da quarta camada, a de Transporte, ou seja: UDP, TCP, entre outros. A camada de Transporte é responsável por assegurar que os dados sejam transferidos de um ponto A a um ponto B de forma confiável e evitando erros. Pode garantir até que esses dados sejam enviados e recebidos em uma determinada ordem.
No HTTP, clientes e servidores se comunicam trocando mensagens individuais (ao contrário de um fluxo de dados). As mensagens enviadas pelo cliente, geralmente um navegador da Web, são chamadas de solicitações (requests), ou também requisições, e as mensagens enviadas pelo servidor como resposta são chamadas de respostas (responses).
Neste tutorial, será construído um servidor HTTP simples a fim de demonstrar como a comunicação é feita utilizando o protocolo HTTP. Utilizaremos o protocolo TCP para o transporte, por ele ser mais confiável.
Para começar, precisamos implementar a nossa camada de Transporte, que utilizará o protocolo TCP/IP. O código apresentado nesta etapa é compatível apenas com sistemas baseados em UNIX, como Linux e MacOS. Para implementar o TCP, utilizaremos a linguagem C e programação de sockets. O passo-a-passo é basicamente: (1) criar o socket; (2) identificá-lo; (3) esperar no servidor por uma conexão; (4) enviar e receber mensagens e; (5) fechar o socket.
Primeiro, criamos um socket com a chamada de sistema socket
: int server_fd = socket(domain, type, protocol);
. Todos os parâmetros, assim como o valor de retorno, são inteiros e representam, respectivamente, o domínio de comunicação no qual o socket é criado, o tipo de serviço e o protocolo que dá suporte à operação dos sockets. No nosso caso, iremos utilizar AF_INET (família de endereços IP) e SOCK_STREAM (serviço de circuitos virtuais). O último parâmetro é zero, pois não há variação de protocolo.
#include <sys/socket.h>
...
...
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror(“cannot create socket”);
return 0;
}
Nós identificamos o socket ao atribuir um endereço de transporte a ele. Em sockets, isso se chama binding e é feito pela chamada de sistema bind
. Nela, utilizamos a seguinte assinatura: int bind(int socket, const struct sockaddr *address, socklen_t address_len);
. Os argumentos utilizados significam, respectivamente: o socket server_fd, criado anteriormente; uma estrutura genérica para definir a família de endereços; e o tamanho da estrutura, já que ela pode variar baseada no tipo de transporte utilizado. Para fazer o binding de um socket, podemos utilizar o seguinte código:
#include <sys/socket.h>
…
struct sockaddr_in address;
const int PORT = 8080; //Where the clients can reach at
/* htonl converts a long integer (e.g. address) to a network representation */
/* htons converts a short integer (e.g. port) to a network representation */
memset((char *)&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(PORT);
if (bind(server_fd,(struct sockaddr *)&address,sizeof(address)) < 0)
{
perror(“bind failed”);
return 0;
}
Antes que um cliente possa se conectar a um servidor, é preciso que o servidor disponha de um socket que esteja preparado para aceitar conexões. A chamada de sistema listen
informa a um socket que ele é capaz de receber conexões. A assinatura é int listen(int socket, int backlog);
, onde o segundo parâmetro, backlog, define o número máximo de conexões pendentes que podem ser enfileiradas antes das conexões serem recusadas. Também utilizamos a chamada accept
, de assinatura int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
, onde o segundo parâmetro é a estrutura de endereço que é arquivada com o endereço do cliente que está fazendo a conexão. O terceiro parâmetro é o tamanho dessa estrutura. O código utilizado para listen
e accept
é:
if (listen(server_fd, 3) < 0)
{
perror(“In listen”);
exit(EXIT_FAILURE);
}
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
{
perror("In accept");
exit(EXIT_FAILURE);
}
Com isso, conectamos os sockets entre um cliente e um servidor. Para fazer com que eles se comuniquem, é só usar chamadas de read
e write
, que também funcionam em arquivos, como no exemplo. Note que o funcionamento do servidor HTTP acontece baseado no conteúdo da variável char *hello
.
char buffer[1024] = {0};
int valread = read( new_socket , buffer, 1024);
printf(“%s\n”,buffer );
if(valread < 0)
{
printf("No bytes are there to read");
}
char *hello = "Hello from the server"; //IMPORTANTE!
write(new_socket , hello , strlen(hello));
Ao fim da comunicação, é só fechar o socket com a assinatura close(new_socket);
.
Para testar o código do servidor, precisamos de um código para o cliente. A ideia é rodar cada um desses códigos em um terminal diferente, em ordem: primeiro o código do servidor, depois o do cliente. Teremos saídas estruturadas de formas diferentes, mas que significam a mesma coisa.
Em uma comunicação HTTP, o que acontece é o representado no diagrama abaixo:
Inicialmente, o cliente (navegador Web) envia uma requisição HTTP para o servidor, que processa a requisição e envia uma resposta. O cliente deve se conectar ao servidor para qualquer comunicação, enquanto o servidor não se conecta diretamente com o cliente. Quando estamos em um navegador e queremos nos conectar a um servidor, geralmente digitamos alguma URL ou endereço na barra de navegação. Geralmente, é isto que os navegadores enviam para os servidores quando navegamos em páginas Web:
Para provar que isso acontece, basta executar o programa de TCP do lado do servidor, abrir seu navegador e digitar localhost:8080/index.html
e ver a saída no terminal, que será similar à mostrada na imagem acima. Entretanto, o navegador ainda deve indicar que a "página não está funcionando" ou equivalente a essa tela no navegador de sua preferência. Isso se deve à configuração da variável char *hello
de anteriormente, que deve ser mudada para algo do tipo char *hello = "HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 12\n\nHello world!";
.
O "Hello from server!" digitado antes não envia nenhum cabeçalho ou nada relevante de verdade para a comunicação. Existem várias outras possibilidades de cabeçalhos que podem ser encontradas lendo as Referências deste tutorial. O cabeçalho que escolhemos é o mínimo para que a comunicação seja estabelecida de forma correta. Nele, indicamos a versão do HTTP, código/status de mensagem, indicamos que estamos enviando um texto puro (plain text), e a quantidade de bytes que o servidor está enviando ao cliente. Em seguida, temos o corpo da mensagem, onde enviamos os dados. É preciso calcular e informar a quantidade de bytes do corpo também. Além disso, precisamos usar códigos para informar o tipo de conteúdo no corpo. Sobre códigos de status da mensagem, foi definida uma lista de códigos que podemos usar para HTTP.
Para ver nosso servidor HTTP funcionar, é só executar o código no início desta subseção e acessar localhost:8080
no navegador. Daí, veremos a mensagem "Hello World" na tela. 🎉
Até agora, conseguimos enviar uma string dizendo "Hello World" ao nosso cliente. Mas e se quisermos enviar um arquivo, imagem, uma página inteira?
Suponha que você acessou localhost:8080/info.html
, para abrir a página info.html. No terminal do servidor, você recebe os seguintes cabeçalhos de requisição:
GET /info.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
DNT: 1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Considerando apenas a primeira linha, GET /info.html HTTP/1.1
, a ideia é simplesmente saber onde o arquivo info.html está e substituir /info.html pelo caminho correto, se necessário. Para que tudo funcione adequadamente, certifique-se que o arquivo exista e esteja no local dado, e que o cliente tenha (ou não) permissão para acessar o arquivo em questão. Selecione códigos de status e tipos de conteúdo válidos.
Daí, é só abrir o arquivo, ler os dados em uma variável, contar o número de bytes lidos e configurar o Content-Length
com base nisso. E construir o cabeçalho de resposta e enviá-lo para o cliente. Pronto!
Como um extra e para combinar com a stack utilizada no restante do trabalho, também é possível criar um servidor Web remoto ao entrar em um diretório qualquer pela linha de comando, digitar ruby -run -e httpd -- -p 5000
e dar Enter.