Sockets: Criando um httpd simples (Parte 1)

Me pediram, há tempos, que escrevesse alguma coisa sobre sockets aqui e em meu livro “C & Assembly para arquitetura x86-64”. Não o fiz, até hoje, porque trabalhar com sockets exige um nível de abstração superior ao material proposto tanto no blog, quando lá, no livro… Recentemente vi um projeto no Github (este aqui) que implementa um httpd bem simples e eficiente e resolvi explicar como um httpd pode ser incorporado ao seu projeto sem que tenhamos que usar bibliotecas como libmicrohttpd ou usar a interface com o Apache (APR, ou Apache Portable Runtime).

Deixo claro que esse texto inicial serve apenas para “dar um gostinho”. O código de exemplo, no final, tem vários problemas que sequer citarei, ainda, deixando espaço para refinamentos em outros artigos.

Syscalls relacionadas a arquivos:

A maneira mais fácil de começarmos a entender sockets é fazer comparações com as syscalls que lidam com arquivos: open()read()write()close(). Essas funções lidam com um valor inteiro que simboliza ou descreve um arquivo. Esse valor inteiro é chamado de file descriptor e é sempre positivo. Um file descriptor negativo é inválido e é usado como valor de retorno das funções que indica que um erro ocorreu. Assim, para ao abrir um arquivo, podemos fazer algo assim:

int fd;

/* Tenta abrir o arquivo 'myfile.txt',
   para leitura apenas. */
if ((fd = open("myfile.txt", O_RDONLY)) == -1)
{
  ... trata erro aqui ...
}

Note a comparação do descritor fd contra o valor -1. Alguns preferem compara contra 0, assim:

if ((fd = open("myfile.txt", O_RDONLY)) < 0)
{
  ... trata erro aqui ...
}

Isso não é correto. A especificação das syscalls é clara: O valor de retorno, no caso de erros, é -1.

Estou falando de syscalls aqui. Funções como fopen(), fscanf(), fprintf() e fclose() são abstrações usadas pela glibc que implementam “streams” através de buffers contidos dentro de uma estrutura opaca chamada FILE. Essas funções fazem parte da glibc, não do kernel do seu sistema operacional.

Lendo e escrevendo em um arquivo usando syscalls:

De posse do descritor de arquivo, e se não há erros, podemos usá-lo para realizar operações de leitura e escrita através das funções read() e write(), respectivamente. Ambas retornam um valor inteiro que nos informa o tamanho daquilo que a syscall conseguiu ler (no caso do read()) ou escrever (no caso do write()). Esse valor inteiro pode ser negativo também, indicando erro. De novo, devemos compará-lo contra -1:

ssize_t count;

if ((count = read(fd, buffer, buffer_length, 0)) == -1)
{
  ... trata erro aqui ...
}

A especificação de read() nos diz que count pode ser zero e, neste caso, indica que chegamos ao final do arquivo. Mas a especificação também nos diz que, embora tenhamos pedido para read() ler buffer_length bytes do descritor fd, ele pode ler menos do que esse taamnho e a quantidade de bytes será retornada e colocada em count. Ou seja, count pode ser menor que buffer_length. Isso significa que, se você quer ter certeza que leu buffer_length bytes, deve fazer algo assim:

int read_entire_buffer(int fd, void *buffer, size_t size)
{
  /* Aponta para o início do buffer. */
  char *p = buffer;

  ssize_t count;

  /* Se ainda temos bytes a ler... */
  while (size > 0)
  {
    /* ...tenta lê-los! 
       'count' pode retornar menos que
       'size'! */
    count = read(fd, p, size, 0);

    /* Se chegamos ao final do arquivo ou
       se temos um erro, retorna -1. */
    if ((count == 0) || (count == -1))
      return -1;

    /* Diminui o tamanho para lermos apenas
       o que falta. */
    size -= count;

    /* Avança o ponteiro para o buffer. */
    p += count;
  }

  /* 0 significa OK */
  return 0;
}

Tome cuidado ao usar uma rotina assim. A função read() bloqueará a thread em execução até que tenha algum dado disponível para ser lido ou até que ocorra um erro. Se você requisitar a leitura de 1000 bytes através dessa função e só chegaram 500, a rotina ficará bloqueada até que os outros 500 bytes cheguem… Não é essa a aproximação que usarei no exemplo deste artigo. Aqui, verificarei se uma requisição chegou a seu fim através da verificação da sequência de caracteres terminais (“\r\n\r\n”, como mostrarei), de acordo com a especificação do protocolo HTTP. Enquanto não encontrarmos esses 4 bytes, a leitura continuará… Isso é mais seguro, no momento.

Com relação ao tamanho de transmissão de blocos, algo semelhante aplica-se à função write(). Ela pode escrever menos do que você mandou escrever e retornará esse tamanho. Diferente de read() o valor zero no retorno só indica que nada foi escrito. E, outra vez, temos que verificar se o valor -1 foi retornardo, em caso de erros.

Encerrando a comunicação:

A função close(), é claro, fecha o arquivo. A mesma função é usada para encerrar uma conexão, no caso dos sockets, realizando a sequência de encerramento correta para o par de protocolos TCP/IP.

Sockets e TCP/IP:

Um socket é uma abstração de software. Ele é representado por um file descriptor, da mesma maneira que a função open(), acima faz. A ideia é a de que fazemos uma ligação de uma “tomada” (socket) num dispositivo de rede, de um lado, e de alguma maneira outra ligação é feita do outro lado (na máquina remota), estabelecendo a “conexão” entre ambas. Para que essa conexão aconteça é necessária uma troca de informações entre os dois computadores, de maneira ordenada. Essa sequência de troca de informações é chamada de protocolo (lembra um comportamento diplomático, com suas regras restritas).

Existem diversos protocolos para diversas finalidades. Estamos interessados em apenas dois: IP e TCP. O Internet Protocol é um conjunto de dados que diz, para ambos os interlocutores, de onde a mensagem veio e para onde ela vai. Contém outras informações como o tempo de vida do “pacote” (TTL, Time To Live, é a quantidade de retransmissões que podem ocorrer com o pacote — ou seja, por quantos “roteadores” o pacote pode passar); e o tamanho do pacote (tem mais, mas não é importante aqui).

Dentro do “pacote” IP temos o pacote TCP (Transmission Control Protocol). Esse pacote contém informações sobre o “serviço” ao qual o dado é destinado (e de onde veio), chamado de “porta”. Contém ainda uma série de informações e flags destinados ao controle da transmissão confiável dos pacotes.

Dentro do pacote TCP podem existir outros pacotes, mas, no nosso contexto, temos ou pacotes TCP de controle ou aqueles que contém dados. O importante aqui é saber que TCP é responsável pelo estabelecimento e encerramento de uma conexão, bem como a garantia da entrega de pacotes na sequência correta.

As syscalls para sockets:

Embora sockets sejam uma abstração e são usados através de file descriptors, as funções usadas para manipulá-los são ligeiramente mais especializadas do que open()read()write(). Usamos socket() para criar um socket e retornar um descritor de arquivo. Eis o prototipo e seus 3 argumentos:

int socket(int domain, int type, int protocol);

O “domínio” é a família ao qual o protocolo que pretendemos usar pertence. Existem constantes no header “sys/socket.h” prefixadas com “PF_” (de Protocol Family) que devem ser usadas aqui. Duas delas são importantes para nós: PF_INET e PF_INET6. A primeira diz que usaremos um dos protocolos da família do IPv4. A segunda, que usaremos um da família do IPv6. No momento estou interessado apenas em PF_INET.

O “tipo” refere-se a como o socket tratará os pacotes. As constantes SOCK_STREAM e SOCK_DGRAM são particularmente importantes. A primeira está associada ao TCP e a segunda ao protocolo UDP. O protocolo UDP é mais simples que o TCP porque ele não garante que os pacotes cheguem na sequência correta ou que chegem! Ele também não possui quaisquer mecanismos de conexão. Ou seja, UDP é um protocolo conectionless.

O significado desse “tipo” é mais amplo do que um protocolo de transporte específico. SOCK_STREAM significa que a família de protocolos deve ser “confiável” e “conectada” (TCP é um exemplo), enquanto SOCK_DGRAM seleciona a família de protocolos “não confiáveis” e “desconectados” (exemplo: UDP)…

E, finalmente, fornecemos o “protocolo” ao socket(). No nosso caso usarei a constante IPPROTO_TCP para garantir que TCP será usado. É possível informar um protocolo zerado e, neste caso, se a família de protocolos possuir apenas um que atenda os dois outros argumentos, ele será usado. Mas, se possuir mais que um, o protocolo tem, obrigatoriamente, que ser fornecido… Particularmente, não acho uma boa prática usar zero nesse parâmetro, mas já vi várias implementações que o fazem.

Ligando um socket a uma interface de rede:

Com a chamada à socket(), tudo o que fizemos até o momento foi alocar um file descriptor. Ele não serve para nada, ainda… No caso de um httpd devemos ligá-lo a um endereço de um dispositivo de rede (NIC). Para tanto usamos a função bind():

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

O argumento sockfd, obviamente, é o file descriptor do socket. O último argumento, addrlen, contém o tamanho do buffer apontado por addr. Isso é necessário porque o formato de endereços de dispositivos de rede dependem não somente do protocolo usado pelo socket, mas também da família de endereços que usaremos ao preencher esse buffer… A socket API nos dá uma estrutura genérica, básica, chamada sockaddr, mas ela não é, geralmente, usada. Trata-se de um tipo base. No caso de lidar com TCP/IP estamos interessados em outras três estruturas derivadas de sockaddr:

sockaddr

A estrutura sockaddr_store é usada quando queremos lidar com endereçamento tanto com IPv4 quanto com IPv6. As estruturas sockaddr_in sockaddr_in6 lidam, respectivamente, com IPv4 e IPv6.

Assim, bind() aceita o ponteiro genérico e preencherá o buffer de acordo com a família de protocolos citada na criação do socket e também no tamanho do buffer citado em addlen… Aqui, já que estou interessado apenas em IPv4, usarei a estrutura sockaddr_in e a preencherei com uma porta específica (8080) e um endereço genérico:

struct sockadd_in sin = {
  .sin_family = AF_INET,
  .sin_port = htons(8080),
  .sin_addr.s_addr = INADDR_ANY
};

if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
  ... trata erro aqui...
}

Isso faz com que o socket descrito por fd seja ligado à porta 8080 do endereço default fornecido pela tabela de rotas configurada no seu sistema operacional. No código acima, digo que esse endereço é da família de endereços do IPv4 via a constante AF_INET (prefixo “AF_”, de “Address Family“)… A constante INADDR_ANY, atribuída ao endereço, é simplesmente o valor zero. Cada “octeto” de um endereço IPv4 é traduzido diretamente para um byte e o endereço IPv4 todo é do tipo unsigned int. Só que a ordem em que esses bytes aparecem no endereço é chamada de big endian, onde o MSB (Most Significative Byte) aparece no menor endereço e o LSB (Less Significative Byte), no maior. Um endereço IPv4 como 127.0.0.1, na arquitetura Intel, deve ser codificado como 0x0100007F. Ao armazenar esse valor na memória o 0x7F será colocado na primeira posição, seguido dos dois zeros e o byte 0x01, por último… Esse é o padrão que os endereços da família AF_INET esperam… A mesma coisa com relação à porta: Sendo um valor do tamanho de um unsigned short, ocupando 16 bits ou 2 bytes, o MSB tem que vir primeiro. Por isso o uso da função htons() — de “host to network order short“.

E, por falar em “porta”, não é uma boa ideia atribuir qualquer valor ao membro sin_port da estrutura sockaddr_in. A IANA (Internet Assigned Numbers Authority. Site aqui) regula os valores de acordo com cada tipo de serviço. Por exemplo, a porta 22 é usada para SSH (Secure SHell), as portas 20 e 21 pelo FTP (File Transfer Protocol)… Eis algumas portas reservadas para aplicações:

  • 1194 – OpenVPN
  • 1352 – Lotus Notes
  • 1433-1434 – MS-SQL Server
  • 1521 – Oracle Database
  • 2000 – CISCO SCCP
  • 3306 – MySQL
  • 3690 – Subversion (SVN)
  • 5222 – XMPP client
  • 5269 – XMPP server
  • 6000-6007 – X11
  • 10050-10051 – Zabbix

Outra lista útil para consultas a respeito de atribuições de portas está no Wikipedia (aqui) e na lista oficial do IANA (aqui). No caso de servidores http é aconselhável usar a porta 80 ou 8080. Essa última é definida, pela IANA, como http alternative.

Quanto ao bind(), um aviso: Ele só é necessário nos casos onde você quer ligar um socket diretamente a um dispositivo. No caso de um httpd isso é importante, já que o seu daemon ficará escutando uma porta específica de um dispositivo específico. Dessa forma, você poderá ter daemons  separados respondendo em endereços diferentes e portas diferentes, se quiser… O fato de usarmos um endereço genérico no exemplo acima é somente um atalho conveniente. Se você tiver mais que uma interface de rede e não quiser usar a interface default para responder às requisições HTTP, terá que, necessariamente, ligar seu socket a um endereço específico!

No caso de aplicações cliente, geralmente não queremos que o socket esteja ligado à um endereço e porta específicos… Por quê? Bem… as primeiras 1024 portas são reservadas para o superusuário (root). Se você tentar atribuir a porta 80 no membro sin_port de sockaddr_in, e seu processo não estiver executando sob o usuário root, a função bind() retornará -1, indicando erro. Ainda, a aplicação cliente com socket “não-ligado” escolherá, automaticamente, o endereço e uma das portas maiores que 1023… Essas portas são chamadas de “efêmeras” porque são escolhidas para a conexão do cliente e, quando esse desconectar e reconectar poderá escolher outra porta completamente diferente.

Se não me engano, o sistema operacional escolherá portas não atribuídas no arquivo “/etc/services” para o cliente… Para ver essa atribuição automática funcionando você pode usar o utilitário tcpdump (ou o Wireshark, se preferir um ambiente gráfico). Com ele é comum vermos um fluxo assim:

# tcpdump -tnc 4 -i lo host 127.0.0.1 and port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 127.0.0.1.48271 > 127.0.0.1.8080: Flags [S], seq 124748774, ...
IP 127.0.0.1.8080 > 127.0.0.1.48271: Flags [S.], seq 4047593347, ack 124748775, ...
IP 127.0.0.1.48271 > 127.0.0.1.8080: Flags [.], ack 1, ...
IP 127.0.0.1.48272 > 127.0.0.1.8080: Flags [S], seq 643118673, ...
4 packets captured
40 packets received by filter
0 packets dropped by kernel

Aqui a porta 8080 é a do socket do meu daemon ligado à interface default (é o meu pequeno código de teste mais adiante). Os endereços e portas efêmeras 48271, 48272 e 48273 são usadas pelo browser e foram escolhidas pela socket API no ato da conexão… Ou seja, o browser (Chrome, no caso) não faz bind() em uma porta específica.

Esperando por conexões:

Depois de ligar (bind) o socket a um endereço e uma porta, precisamos colocá-lo para ficar escutando… Nada mais simples, chama-se a função listen():

int listen(int sockfd, int backlog);

Ela apenas marca o socket como sendo “passivo”, ou seja, incapaz de ser usado numa conexão. Também informa à API que podemos aceitar, no máximo, a quantidade de conexões especificadas no parâmetro backlog. Depois de chamar listen(), o socket passa a existir apenas para receber pedidos de conexão, que deverão ser aceitas por outra função: accept().

Atenção especial ao valor de backlog: O valor máximo depende do seu sistema. Na máquina em que usei para fazer testes o limite superior para a quantidade de file descriptors do sistema era de 65536. E, destes, uns 37000 já estavam em uso:

$ ulimit -n
65536
$ echo "$(lsof | wc -l) - 1" | bc
37550

Deixar que seu processo tenha à disposição uma lista de uns quase 28000 file descriptors não é nem mesmo aconselhável. Primeiro, é improvável que seu processo tenha velocidade e espaço em memória suficiente para atender 28 mil conexões simultâneamente e, em segundo lugar, isso deixaria “poucos” descritores disponíveis para o sistema. Um valor máximo mais aceitável para backlog dependerá do método usado para lidar com as conexões. Num sistema multi threaded, se trabalharmos com uma thread por descritor de conexões aceitas, uns 100 descritores devem ser suficientes para não causarem grande impacto devido ao chaveamento de tarefas, por exemplo.

Aceitando conexões:

Depois de colocar o socket para escutar, precisamos começar a aceitar conexões vindas do cliente. A função accept(), quando chamada, bloqueará a thread em que é executada até que um pedido de conexão chegue ao socket e seja atendida. Neste caso, accept() criará um novo socket, agora “ativo”, e o colocará numa lista associada ao socket “escutador”.Este novo socket é que é o “conectado” e usado para transmissão de dados.

O protótipo de accept() é:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

A função retorna -1 em caso de erro ou um file descriptor relacionado à conexão aceita. Ao mesmo tempo, se os ponteiros seguintes forem diferentes de NULL, o endereço do requisitante e o tamanho da estrutura são armazenados nos endereços dados pelos ponteiros. Podemos saber quem nos chamou, afinal de contas!

Lendo e escrevendo num socket:

Depois de aceita a conexão e obtido o socket correspondente podemos ler e enviar dados via funções recv()send(). Lembra-se de read() e write() no início do texto? As funções recv()send() fazem a mesma coisa, mas elas lidam melhor com condições de erro e a semântica do socket do tipo SOCK_STREAM. E, ainda, permitem informar algumas opções extras, como MSG_WAITALL, MSG_OOB e MSG_PEEK, para recv(), que não são possíveis via readNão use read() e write() com sockets.

Toda aquela história de devolver tamanho diferente do que foi requisitado também se aplica aqui, mas recv(), se retornar zero, não nos indica que chegamos ao final do arquivo, mas sim ao final da conexão… Neste caso, provavelmente, o outro lado desconectou (executou um close()).

Encerrando uma conexão:

Terminadas as leituras e escritas ao socket, podemos fechá-lo, encerrando a conexão. Isso é feito através da mesma função close() citada lá em cima. Essa syscall realizará o handshake de finalização automaticamente para nós.

Existe também uma função chamada shutdown(). Ela não “fecha” uma conexão, mas impede que dados possam ser recebidos e/ou lidos. Esse seria um procedimento preliminar antes de chamarmos close(), no entanto, ela já faz isso por nós.

Subindo o nível – HTTP:

Até agora, tudo o que vimos foi como um socket deve ser criado, colocado para “escutar”, aceitar conexões, enviar e receber dados e fechar a conexão… Mas, e quanto ao protocolo http?

Http é um protocolo onde o cliente (browser) envia um conjunto de linhas com instruções ao web server (chamado de http daemon). Depois de processada essas instruções (requisição) o servidor monta uma resposta e a envia de volta ao cliente… E isso é tudo!

As linhas que compõem uma requisição seguem um padrão bem definido: A primeira linha nos dá o método requisitado, a URI e a versão do HTTP:

GET / HTTP/1.1\r\n

Apenas dois métodos nos interessam no momento: GET e POST. O método GET é usado quando todos os argumentos que precisam ser passados para o deamon podem sê-lo sem fornecer dados adicionais, ou seja, sem um payload. POST, por outro lado, permite dados adicionais… É interessante notar que toda linha do header de uma requisição termina com o par de caracteres \r e \n.

Faço a diferenciação de header e payload aqui para deixar claro que apenas o header é textual e é composto da linha acima seguida ou não de atributos (veja mais adiante). O payload, se houver, seque o header e pode ser binário (também, veja mais adiante)…

No exemplo acima, a URI (Universal Resource Identifier) é relativa. Ela informa apenas o path, sem nos dizer nada sobre o host. Esse parece ser o padrão dos browsers modernos, mas a especficação HTTP/1.1 permite paths absolutos como “http://mysite.com.br/”. Observe como o Chrome monta suas requisições quando peço para acessar o site “http://127.0.0.1/”:

GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36\r\n
DNT: 1\r\n
Accept-Encoding: gzip, deflate, sdch\r\n
Accept-Language: en-US,en;q=0.8,pt-BR;q=0.6,pt;q=0.4\r\n
\r\n

E, agora, veja como o IceWheasel o faz:

GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0 Iceweasel/38.8.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
\r\n

Repare que, além da linha do GET, temos outras linhas com atributos, no formato “Attributo: Valor”. Geralmente uma requisição tem o atributo com o nome do Host informado pelo cliente, justamente porque a URI é relativa. Outra informação frequente é os tipos (MIME, Multipurpose Internet Mail Extensions) aceitáveis pelo browser, na resposta. Isso diz ao daemon que ele aceita HTML (text/html), XHTML e XML (application/xhtml+xml e application/xml), prioritariamente, mas também aceita qualquer outra coisa (*/*). O Chrome, como pode ser visto acima, cria uma diferenciação para o tipo “image/webp”. Outra informação que pode ser passada pelo browser é como ele aceita a codificação alternativa do payload, se houver. No caso, o Chrome aceita gzip, deflate e um compactador proprietário do Google chamado sdch.

Note que ambos os browser informam para o servidor que enviarão keep alives, mantendo a conexão viva e dizem ao servidor quem eles são (User-Agent). Essa última informação é importante para alguns sites. O site da Microsoft, por exemplo, não aceita requisições sem agentes.

Mas, e quanto ao método POST? A diferença é que a requisição terá que conter um atributo chamado “Content-Length” com a quantidade de bytes do payload. Ainda, os browsers costumam passar um “Content-Type” com a string do tipo MIME deste conteúdo. Por exemplo, poderíamos requisitar alguma coisa de nosso daemon passando uma string JSON e pedindo que a resposta seja em JSON também… Teríamos que usar o método POST mais ou menos assim:

POST /getdata.cgi HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
User-Agent: MyBrowser/1.0\r\n
Accept: application/json\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Content-Type: application/json\r\n
Content-Length: 14\r\n
\r\n
{ "key": "1" }

O atributo “Content-Length” é essencial aqui porque o servidor precisa saber quantos bytes deve esperar depois do header da requisição.

Atenção! Não devemos confiar muito no atributo “Content-Length” de uma requisição. Um cliente malicioso pode passar um valor diferente do tamanho real do payload. Valores diferentes podem causar bloqueios involuntários da função recv() — se “Content-Length” for maior que o tamanho real do payload — e, dependendo de sua implementação, buffers overruns. Noutro artigo mostro como confirmar o valor informado e, até mesmo, ignorá-lo.

Respondendo ao cliente:

Uma vez processada a requisição o servidor monta a resposta num formato também bem definido:

200 OK HTTP/1.1\r\n
Content-Type: text/html\r\n
Content-Length: 43\r\n
\r\n
<html><body><h2>Success!</h2></body></html>

A primeira linha contém um código de retorno (200), uma mensagem de erro (OK) e a versão do HTTP. As linhas seguintes, se houverem, seguem o mesmo padrão usado na requisição. São atributos repassados para o browser… O header da resposta termina da mesma forma que acontece na requisição: com uma linha vazia. E, se existir um atributo “Content-Length”, seguem n bytes do payload.

Repare que em ambos os casos o conteúdo adicional não precisa ser textual. O conteúdo depende do tipo definido no atributo “Content-Type”. A lista de tipos MIME, mantida pela IANA pode ser vista aqui. E aqui o “Content-Length” não é problemático. Nós mesmo estamos calculando-o.

Os códigos de erro da resposta:

Existem vários: Todos com 3 algarismos. Os códigos da série 100 são históricos e raramente usados. Os códigos da série 200 indicam sucesso do daemon em processar a requisição. Os da série 300 são usados em redirecionamentos. A série 400 são erros no processamento da requisição (ou seja, o erro é do cliente!) e, finalmente a série 500 são erros do próprio daemon.

Alguns desses erros são nossos velhos conhecidos: 401 (Unauthorized), 404 (Not Found), 500 (Internal Server Error)… Outros, nem tanto: 413 (Payload Too Large), 414 (URI Too Long)… Consulte a RFC 2616 (aqui) para maiores detalhes.

Código de exemplo – mostrando uma requisição:

Abaixo temos um código exemplo simples que somente mostrará a requisição feita por um browser e retornará “Success!” para ele (ou código de erro 413 se a requisição for muito grande).

/* vim: set ts=2 et sw=2: */
/* httpd_test1.c 

   Compilar com:
     gcc -o httpd_test1 httpd_test1.c

   Executar com:
     $ ./httpd_test1 8080

     Para executar o "server" com o seu usário normal e
     ouvir a porta 8080, ou...

     $ sudo ./httpd_test1 80

     Para escutar a porta 80, usando o root.
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>     /* strlen() & memcmp() */
#include <unistd.h>     /* getuid() */
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> /* IPPROTO_TCP & 
                           struct sockaddr_in */
#include <arpa/inet.h>  /* inet_ntoa(). */

/* Coloquei o buffer como global a esse módulo porque,
   declarado assim, ele será colocado no segmento BSS.
   Se o fizesse local à main() ele seria alocado na 
   pilha. */
#define MAX_BUFFER_SIZE 2048
static char buffer[MAX_BUFFER_SIZE + 1];

static void send_request_too_long_error(int);

/* Este teste apenas lê uma requisição vinda de um 
   browser. A idéia aqui é analizar o conteúdo de uma 
   requisição real e confrontá-la com o que a 
   especificação HTTP/1.1 nos diz.

   Lerei apenas uma requisição e a mostrarei a string 
   resultante já que o protocolo HTTP é textual, no que 
   se refere à requisição e aos headers. Não estou 
   preocupado com a implementação estrita do protocolo,
   por enquanto. */

/* Usarei apenas um argumento: A porta que o 'server' 
   ouvirá. O valor de retorno será de 0 até 9, dependendo
   do erro. */
int main(int argc, char *argv[])
{
  int port,
      port_min = 1, /* porta 0 é reservada! */
      listen_fd, conn_fd; /* socket descriptors */

  /* Se não forneceu a porta, reclama! */
  if (!*++argv)
  {
    fputs("\033[1mUsage\033[0m: http_test1 <port#>\n", 
        stderr);
    return 1;
  }

  /* Pega o UID do usuário que chamou esse processo.
     Isso é útil porque não podemos atribuir portas
     abaixo de 1024 se o processo não roda sob root. */
  if (getuid() != 0)
  {
    port_min = 1024;
    fputs("\033[1mWarning\033[0m: User is not root.\n"
          "         "
          "Only ephemeral ports allowed (>= 1024).\n", 
          stderr);
  }

  port = atoi(*argv);

  if (port < port_min || port > 65535)
  {
    fprintf(stderr, 
        "\033[1mError\033[0m: Invalid port number (%d)\n",
        port);
    return 1;
  }

  /* Cria o socket que vai "escutar" - 
     usando IPv4 e TCP */ 
  if ((listen_fd = socket(AF_INET, 
                         SOCK_STREAM, 
                         IPPROTO_TCP)) == -1)
  {
    perror("socket()");
    return 1;
  }

  /* Antes de ligarmos o socket recém-criado a um 
     endereço e uma porta, precisamos dizer à API que 
     queremos "reusar" o endereço, caso ele já esteja em
     uso. */
  int enable = 1;
  if (setsockopt(listen_fd, SOL_SOCKET, 
                           SO_REUSEADDR, 
                           &enable, sizeof(int)) == -1)
  {
    perror("setsockopt()");
    return 1;
  }

  /* Vamos ligar o socket "escutador" no endereço 0.0.0.0,
     porta 'port', usando IPv4. Um endereço zerado é um 
     "coringa" e nos diz que o socket será ligado ao
     dispositivo default. */
  struct sockaddr_in listen_addr = { 
    .sin_family = AF_INET,
    .sin_port = htons(port),
    .sin_addr.s_addr = INADDR_ANY
  };

  /* Ligando o socket ao endereço e porta colocados em 
     'listen_addr'. */
  if (bind(listen_fd, (struct sockaddr *)&listen_addr, 
                     sizeof(listen_addr)) == -1)
  {
    perror("bind()");
    return 1;
  }

  /* Colocando o socket para ouvir. Precisamos de apenas
     1 socket na fila. */
  if (listen(listen_fd, 1) == -1)
  {
    perror("listen()");
    return 1;
  }

  /* A função accept() bloqueará a thread do processo, 
     esperando por uma conexão. Ela aceitará a conexão 
     (3-way handshake) e retorna o socket conectado, 
     bem como informações sobre o requisitante. */
  fputs("Waiting for connection...", stdout);
  fflush(stdout);

  struct sockaddr_in conn_addr;
  socklen_t n;

  if ((conn_fd = accept(listen_fd, 
                           (struct sockaddr *)&conn_addr, 
                           &n)) == -1)
  {
    perror("accept()");
    return 1;
  }

  /* Mostra as informações do requisitante. */
  printf("\nAccepted connection from: "
         "\033[32m%s\033[0m:\033[32m%u\033[0m\n", 
    inet_ntoa(conn_addr.sin_addr), 
    conn_addr.sin_port);

  /* Lê a requisição inteira. */
  char *p, *q;
  ssize_t bytes, remind = MAX_BUFFER_SIZE;

  p = buffer;
  for (;;)
  {
    bytes = recv(conn_fd, p, (size_t)remind, 0);

    /* Quando recv retorna 0 isso pode significar
       duas coisas: Ou não há dados para leitura ou
       a conexão foi quebrada. Não há possibilidade
       de obtermos o primeiro caso, já que o socket
       está no modo 'bloqueável'. */       
    if (bytes == 0)
    {
      fputs("\033[1mError\033[0m: Connection lost!\n", 
          stderr);
      return 1;
    }

    /* Um erro ocorreu! */
    if (bytes == -1)
    {
      /* EINTR só acontece quando há uma interrupção
         (signal). O que não é o caso neste exemplo.
         Só coloquei isso como exemplo. */
      if (errno == EINTR)
        continue;

      perror("recv()");
      return 1;
    }

    /* Avança o ponteiro para o próximo ponto de 
       leitura do buffer, diminui o espaço disponível
       e coloca um '\0' no final, para testarmos pelo
       final da requisição. */
    p += bytes;
    remind -= bytes;
    *p = '\0'';

    /* A requisição não cabe no buffer?
       Diz isso pro requisitante! */
    if (remind <= 0) 
    { 
      send_request_too_long_error(conn_fd);
      fputs("\033[1mError\033[0m: Request too large.\n", 
        stderr); 
      return 1; 
    } 

    /* Temos, pelo menos, 4 bytes no buffer? */
    if ((size_t)(p - buffer) > 4)
    {
      /* Encontramos o final da requisição? */
      q = p - 4;
      if (!memcmp(q, "\r\n\r\n", 4))
        break;  /* Se sim, saimos do loop. */
    }
  }

  /* Mostra o bloco recebido. */
  buffer[bytes] = '\0';
  printf("Received %zd bytes:\n%s", bytes, buffer);

  /* Manda uma resposta (hardcoded). */
  const static char resp[] = "HTTP/1.1 200 OK\r\n"
                             "Content-Type: text/html\r\n"
                             "Content-Length: 43\r\n\r\n"
                             "<html>"
                               "<body>"
                                 "<h2>Success!</h2>"
                               "</body>"
                             "</html>";

  bytes = strlen(resp);
  printf("Sending OK response (%zu bytes)...\n", bytes);

  p = (char *)resp;
  for (;;)
  {
    remind = send(conn_fd, p, bytes, 0);

    if (remind == -1)
    {
      /* EINTR só acontece quando há uma interrupção
         (signal). O que não é o caso neste exemplo.
         Só coloquei isso como exemplo. */
      if (errno == EINTR)
        continue;

      perror("send()");
      return 1;
    }

    bytes -= remind;
    if (bytes <= 0)
      break;

    p += remind;
  }

  /* Fecha o socket da conexão e o listener. */
  close(conn_fd);
  close(listen_fd);

  return 0;
}

void send_request_too_long_error(int fd)
{
  const static char resp[] = 
    "HTTP/1.1 413 Request Entity Too Large\r\n\r\n";

  /* Não me preocupo com o tamanho do buffer aqui. */
  send(fd, resp, strlen(resp), 0);
}

 

Anúncios