Sockets são ótimas ferramentas para qualquer programador, independentemente da linguagem. Eles basicamente permitem que programas em diferentes computadores troquem informações. Aqui vou mostrar o caminho que percorri para aprender a programar usando sockets.
Minha abordagem será em C e a API que vou mostrar é baseada em UNIX. Para quem programa com outras linguagens ou usa Windows, existem vários tutoriais ótimos na internet. Ótimas referências são Python, Java e a API winsock.
Aprender sockets não é tão trivial. São necessários conceitos de redes protocolos, processos, input não-bloqueante, file descriptors, UNIX... Portanto, recomendo tentar aprender com objetivos sólidos em mente. Por exemplo, criar um programa de chat, servidor web ou um jogo online.
Além da teoria, vou definir dois "deveres de casa". São dois programas que você deve tentar fazer para provar que aprendeu a usar sockets. No final, vocẽ deve ter um conhecimento sólido e prático de como usar sockets nos seus programas.
Introdução
Aqui vou passar superficialmente sobre alguns conceitos básicos. A explicação é simples e rápida porque os detalhes estarão nos materiais de referência.
Uma analogia que é repetida desde sempre é a idéia do telefone. Sockets são como telefones: apenas pontos de comunicação. Eles ligam dois computadores e permitem que se troque dados. 'Abrir' um socket significa ligar para alguem. Para isso você precisa do número de telefone (no caso dos computadores, do endereço IP - Internet Protocol).
Quando falamos no telefone, nos comunicamos através de uma língua que ambos entendem. Para os computadores enviarem e receberem dados, é definido um protocolo. Assim eles sabem de onde veio a informação, qual o tamanho da informação, data de envio e assim vai. O protocolo mais usado na internet é o HTTP (Hypertext Transfer Protocol).
Quando você fala no telefone, não precisa se preocupar sobre como sua voz é convertida para sinais eletrônicos ou como ela vai chegar até o telefone da pessoa que você está ligando. Isso tudo é feito pelas empresas telefônicas. Do mesmo jeito, a API de sockets cuida de todos esses problemas de baixo nível por você. Tudo o que você precisa fazer é usar uma sequência de funções e fornecer os dados para a comunicação acontecer.
Aqui vai uma lista das principais chamadas que são necessárias na socket API, junto com um equivalente na analogia do telefone:
gethostbyname()- Procura o endereço IP a partir de um host name (procurar o telefone a partir do nome da pessoa)
socket()- Criar um socket (levantar o gancho do telefone)
connect()- Conectar a um endereço IP (discar o número de telefone)
bind()- Define uma porta de comunicação na sua máquina (não funciona muito bem com a analogia do telefone, mas basicamente define uma porta para fazer essa ligação, já que uma mesma máquina pode fazer várias ligações ao mesmo tempo)
listen()- Avisa a socket API que você quer receber conexões (ficar esperando por ligações)
accept()- Aceita uma conexão (atender o telefone)
send()- Enviar dados através de um socket (falar no telefone)
recv()- Receber dados através de um socket (ouvir no telefone)
close()- Fechar um socket (desligar o telefone)
Quando uma comunicação é feita entre dois computadores, existem dois papéis que podem ser assumidos: cliente e servidor. Cliente é quem telefona e servidor é aquele que está pronto para receber a ligação, falar e desligar.
Um servidor oferece um serviço que os clientes desejam usufruir. Então ele faz o papel de estar pronto para receber conexões e entregar o que for solicitado - seja hostear um jogo, devolver uma página HTML ou entregar um arquivo para o cliente fazer download.
O cliente precisa ter conhecimento do que o servidor oferece para poder solicitar. Ele que começa a comunicação, pedindo alguma coisa. Assim que recebe o que pediu, fecha a conexão e vai embora. Ele pode ser um navegador de internet, cliente ftp, jogador de MMORPG e assim vai.
Apesar disso, note que um programa pode ser tanto cliente quanto servidor. Aqui segue um exemplo de sessão, com as funções chamadas pelo cliente e pelo servidor:
| Servidor | Cliente |
|---|---|
| socket() | |
| bind() | |
| listen() | |
| gethostbyname() | |
| socket() | |
| accept() | connect() |
| send() & recv() | send() & recv() |
| close() | close() |
Quando for criar clientes e servidores, o programador deve saber as diferenças entre cada um. Para um mesmo programa, os servidores e clientes são bastante distintos e são desenvolvidos de maneira bem diferente. Os "deveres de casa" propostos cobrem exatamente isso - criar um cliente e um servidor web.
Materiais
Todos esses materiais são referências que encontrei e usei para estudar
esse assunto. Porém, precisei de incontáveis pesquisas no Google e sites
que não foram listados aqui. Tambem aproveite as manpages. Quando tiver
dúvida sobre alguma função, abra um terminal e digite man <nome_da_função>.
Não se limite apenas às minhas referências e seja auto-didata.
A leitura obrigatória é o Beej's Guide to Network Programming. O livro é gratuito e pode ser baixado em diversos formatos. Apenas com ele já se aprende praticamente tudo dessa área. Mas como tem pontos que são melhor explicados em outros lugares, esse livro deve ser lido ao mesmo tempo que as outras referências. A ordem de aprendizado pode ser o índice desse livro.
Qualquer dúvida que surgir, visite o UNIX Socket FAQ. Ele pode ser acessado aqui ou aqui. Ele contém literalmente perguntas e respostas - nada de explicações teóricas.
Esse Unix Network Socket Tutorial explica bem rápido e pode até ser um resumão.
Sobre protocolos, aprenda sobre HTTP aqui (HTTP Made Really Easy). Ele explica apenas as partes necessárias para esse curso e de forma bem rápida.
Um grande tópico a ser estudado é a arquitetura cliente/servidor. Qualquer pesquisa no Google pode encontrar definições ótimas sobre esse assunto: aqui e aqui, por exemplo. Essa introdução é ótima. Note que o Beej's Guide tambem cobre isso.
O Spencer's Socket Site tem recursos interessantes, com links para códigos-fonte de exemplo.
Outro assunto muito importante é input bloqueante e não-bloqueante. Para entender isso, você já deve ter noções de programação em sockets e treinado um pouco (fazendo o primeiro exercício, por exemplo). Esses dois tutoriais devem ser lidos simultaneamente: Introduction no non-blocking I/0 e Non-Blocking Network Socket Tutorial.
Dentro de input não-bloqueante, esse tutorial foca mais em
poll() e select().
Unix Daemon Server Programming faz jus ao nome. Um passo-a-passo ensinando como fazer um programa virar um daemon.
Objetivos
Esses são os exercícios que devem ser feitos para por em prática o estudo.
Recuperador de Páginas WEB
Esse programa vai ser um clone simples do wget - vai baixar a página/arquivo especificado. Ele deve baixar tanto páginas HTML quanto arquivos, mas vou me referir apenas a 'página' de agora em diante.
Ele recebe dois argumentos: a URI da página e o nome de arquivo onde será salvo. Assim, ele vai conectar ao servidor que tem a página, baixá-la e salvar no arquivo especificado. O programa deve implementar o protocolo HTTP/1.0 pra se comunicar com o servidor remoto.
Para fins didáticos, apenas isso é necessário. Mas existem coisas que melhoram a funcionalidade do programa e devem ser levadas em questão:
- Checar linha de comando incompleta ou parâmetros mal-formados.
- Lidar com diferentes erros de rede (servidor não existe, não responde, etc).
- Lidar com diferentes erros de arquivo (arquivo já existe, nao pode ser acessado, etc).
E fatores opcionais são:
- Flags para personalizar o programa (forçar substituição do arquivo caso exista, verbose, etc).
- Claridade na leitura do código-fonte (nomes de variáveis, comentários, identação, etc).
- Modularidade (diminuir o acoplamento e aumentar a coesao).
- Aderência completa ao protocolo HTTP.
Uma boa forma de treinar requests HTTP é usar o telnet. Veja essa pagina
para exemplos.
O código-fonte do meu programa (que apelidei de getw) pode ser acessado aqui. Não o leve muito a sério, serve mais como uma consulta e não como um programa "de verdade".
Servidor WEB
Esse programa pretende ser um clone simples de servidores como o Apache. Ele vai ficar bem parecido com servidores pequenos, como o lighthttpd. Basicamente vai receber pedidos em HTTP e agir de acordo.
Ele recebe dois argumentos: a porta na qual ele vai servir e a pasta raíz. Dessa forma, ele vai abrir porta, esperar conexões e servir todos os arquivos que estiverem na pasta especificada.
O servidor deve esperar até que um cliente se comunique com ele. Então, vai receber a request em HTTP, fazer o parsing da mensagem (para determinar o que o cliente quer) e retornar o arquivo solicitado ou mensagem de erro.
Para fins de simplicidade, a única request que deve ser servida é a GET.
Outros serviços como HEAD, POST e PUT ficam sendo opcionais. A versão
do HTTP a ser usada deve ser a 1.0.
Preste atenção para as seguintes questões:
- Caso o cliente solicite algo inválido (arquivo não existente, etc) o servidor deve retornar uma mensagem de erro em uma página HTML. Teste com o
telnetpara ver o que os servidores costumam fazer. - O servidor deve recusar tentativas de sair da pasta raíz (pedidos como
../../../etc/passwd).
Uma forma interessante de teste seria usar o recuperador de páginas WEB criado por você para solicitar páginas desse servidor. Use tambem o seu browser preferido, colocando como endereço o seu hostname (ou IP local).
Existem muitas referências para servidores web. Um ótimo exemplo (com código em C) pode ser visto aqui. Essa página da IBM é mais complexa, mas é bem detalhada.
O meu código-fonte (que chamei de servw) pode ser acessado aqui. Esse código foi muito mais corrido e mal-feito. Por isso, me desculpe se ele for ilegível. Tentei comentar, mas admito que não dá pra entender o que ele faz só olhando. Se você realmente quiser entender, vai ter que quebrar a cabeça um pouco.
Bônus: Melhorando o Servidor
Esse é um exercício extra que serve para aprofundar os conhecimentos de servidores web. Com isso seu programa vai ficar muito mais robusto e próximo de servidores "reais".
Eis a checklist:
- Fazer o servidor servir vários clientes ao mesmo tempo.
- Transformar o servidor num daemon.
- Criar controle de velocidade.
- Otimizar o uso de memória.
Para servir vários clientes ao mesmo tempo, seu servidor terá que usar sockets não-bloqueantes. Nenhum cliente pode ficar "esperando" para ser servido. Para isso, o loop principal do programa não pode parar em lugar algum. Tambem, se não conseguir mandar nada pro cliente agora, deixa pra lá e mande depois.
Para criar controle de velocidade, você vai limitar a banda de cada usuário. Por exemplo, cada usuário pode baixar no máximo a 300Kb/s. Isso é mais complexo e deixo à cargo de você pesquisar como implementar.
Por fim, para evitar que seu servidor gaste muita memória, você deve
fazer ele "descansar" sempre que possível. Em vez de ficar testando
o tempo todo se você pode fazer alguma ação (o que gasta processamento),
deixe seu servidor sempre "dormindo" e só o acorde quando puder fazer
algo. A função select() é sua melhor amiga.
No meu servidor fiz tudo isso, então qualquer coisa olhe lá.
1 comment
Renatosantos
October 24, 2012 at 02:10 (UTC -2)
Cara ainda não tive tempo de ler esse seu texto, mais salvei aqui pra ler quanto tiver tempo, to querendo aprender sockets e toda informação é bem vinda. Valeu.