Aplicações para rede são muito mais simples do que a maioria das pessoas acredita. Elas se baseiam em uma arquitetura cliente/servidor, mais do que explorada, ou seja, todos os grandes problemas das aplicações mais comuns já foram explorados e estudados. O grande problema dessas aplicações é o “debug”, já que em 99% dos casos os servidores irão rodar em máquinas externas, fica extremamente complicado a depuração de um código cliente/servidor, mas com muita calma, conseguimos também “debugar”, talvez em uma forma um pouco mais primitiva, mas conseguimos.
Cliente/Servidor
Mas afinal, o que é uma arquitetura cliente/servidor? Uma arquitetura cliente/servidor nada mais é do que dois programas rodando em máquinas diferentes se comunicando via uma interface chamada socket. Um servidor é o programa que fica normalmente em uma potente máquina, ligada 24 horas por dia, 7 dias por semana, apenas ouvindo e esperando algum cliente encontrá-la, e então começar uma inúmera troca de mensagens. Funciona mais ou menos assim:
Cliente Servidor
Ola, tem alguém ai?
Sim, eu estou aqui e pronto para falar com você
Muito bem, será que
você tem o conteúdo X?
Tenho sim, estou te enviando agora
<Envia conteúdo>
Muito obrigado, até mais.
Até mais.
Sockets
Uma aplicação assim, quando está em uma rede de computadores, precisa de uma interface mais complexa do que arquivos ou memória compartilhada para a comunicação. Ela utiliza sockets. Sockets são interfaces para a comunicação via rede, quando se cria um socket, se está criando um canal de comunicação entre o cliente/servidor, que podemos utilizar como se fosse um arquivo, afinal, tudo em UNIX é um arquivo não?
Para entender como utilizar sockets, precisamos ter apenas uma visão maior de como é uma estruturada uma rede e como realmente ocorre a comunicação, porém para debugar um programa de rede, já precisamos ter um entendimento muito maior do que apenas o básico. Como não é objetivo ensinar a debugar, mas sim a programar, vamos dar apenas uma pincelada.
A rede é dividida em camadas, isso é feito para permitir a modularização das aplicações, dessa forma cada camada precisa saber apenas sobre ela mesma, e o que enviar para a próxima camada, ou o que receber da camada. No modelo ISO/OSI são 7 camadas no total:
7)Aplicação
6)Apresentação
5)Sessão
4)Transporte
3)Rede
2)Acesso
1)Física
Nós estamos na camada 7, a da aplicação, os sockets que serão criados são a interface que faz a nossa aplicação passar camada por camada até chegar na camada física e enviar o dado via a rede para o servidor. Durante o caminho ele passará por vários roteadores que irão ler até a camada de Rede, desmontando o “pacote” e remontando-o com o destino do próximo roteador, até chegar no destino. De forma rápida e sucinta, é assim que é uma rede.
3-Way handshake
Quando estamos programando em sockets, mais especificamente socket de stream (TCP), precisamos ter uma conexão segura e constante com o servidor, ou seja, para cada pacote que enviarmos, teremos um pacote de retorno falando que o servidor recebeu o pacote, ou não, nesse caso enviamos novamente. Inicialmente precisamos inicializar uma conexão com o servidor, isso é chamado de 3-way handshake. Sem escovar muito os bits, é assim que funciona:
Cliente Servidor
Ola
Ola, você quer falar comigo?
Gostaria muito
<inicio da transmissão de dados>
Escovando um pouco os bits, o que acontece é que quando vamos iniciar uma conexão a um servidor temos várias flags, as importantes para o 3-way handshake são SYN e ACK. SYN vem de Syncronize, e ACK de Acknowledge. O cliente envia inicialmente um pacote ao servidor com a flag SYN setada como verdadeira, o servidor retorna então um pacote com as flags SYN e ACK setadas, o cliente retorna um ACK e logo em seguida a comunicação pode ser feita. Existe muito mais do que apenas isso, como por exemplo tamanho da janela, tamanho da mensagem, tempo de vida da mesma, número de seqüência tudo isso é inicializado nessa hora, para aplicações básicas, não precisamos nos preocupar com nada disso, a interface socket irá cuidar de tudo isso automáticamente.
O programa servidor
Como é de praxe, ou melhor, como fizemos com as threads, vou parar de falar e mostrar logo a parte do servidor, com o código comentado. A única grande diferença entre o servidor e o cliente em questão de programação, é que o servidor precisa escolher uma porta e endereço para ficar residente esperando por conexões, quando o cliente apenas precisa saber isso e, enviar a requisição de uma nova conexão.
Vamos ao código.
Apesar de um pouco mais complexo que um simples servidor, é fácil perceber o que é sobre socket e o que é sobre sistema ou adaptações.
//Um servidor TCP demo feito em C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define MYPORT 3490
#define BACKLOG 10 //Quantas conexões ao mesmo tempo iremos ter
void sigchld_handler(int s){
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[]){
int sockfd, new_fd; //Ouve em sockfd e conecta em new_fd
struct sockaddr_in my_addr; //informação sobre mim
struct sockaddr_in their_addr; //informação do cliente
socklen_t sin_size;
struct sigaction sa;
int yes = 1;
char *buff = NULL;
unsigned int i = 0;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR, &yes, sizeof(int))==-1){
perror("setsockopt");
return errno;
}
my_addr.sin_family = AF_INET; // tipo, em formato host byte
my_addr.sin_port = htons(MYPORT); // a porta que queremos ouvir, em formato network short
my_addr.sin_addr.s_addr = INADDR_ANY; // Meu ip, automáticamente
memset(my_addr.sin_zero,'\0',sizeof(my_addr.sin_zero));
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr))==-1){
perror("bind");
return errno;
}
if(listen(sockfd,BACKLOG) == -1){
perror("listen");
return errno;
}
// Alocando memória para mandar mais do que o window size 40000 bytes
if((buff=(char*)malloc(sizeof(char)*400000))==NULL){
perror("malloc");
return errno;
}
memset(buff,'\0',sizeof(char)*400000);
for(i=0;i<400000;i++) buff[i]='a';
buff[399999]='b';
sa.sa_handler = sigchld_handler; //limpa todos os processos antigos
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if(sigaction(SIGCHLD, &sa, NULL) == -1){
perror("sigaction");
return errno;
}
while(1){ //loop principal de aceitação
sin_size = sizeof(their_addr);
if((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size))==-1){
perror("accept");
continue;
}
printf("server: conexao de %s\n",inet_ntoa(their_addr.sin_addr));
if(!fork()){ //This is the child process
close(sockfd); //Child don't need to listen
if(send(new_fd, buff, 400000, 0) == -1){
perror("send");
}
close(new_fd); //Fechamos a conexão do cliente
exit(0);
}
close(new_fd); //O pai não preciso disso
}
free(buff);
return 0;
}
Para compilar:
gcc -o server server.c
Pode parecer complicado, mas não é, temos uma seqüência básica de funções a chamar em um servidor:
1)socket(), para criarmos o nosso socket TCP que irá receber conexões
2)Setamos as opções do socket (fazemos toda a parte de “escovar bits” aqui, podemos em 90% dos casos lidar com o default apresentado aqui
3)bind(), para associarmos as opções ao socket
4)listen(), para ficarmos ouvindo por conexões
5)accept(), aceitarmos a conexão
6)read() e send(), para fazermos a comunicação com o cliente
Todo o resto é para fazermos o servidor aceitar mais de uma conexão ao mesmo tempo, e enviar uma mensagem pré configurada e alocada em memória.
O programa cliente
O programa cliente é o mais simples possível, no nosso caso ele se conecta e fica esperando a mensagem do servidor, quando recebe ele gentilmente fecha a conexão e mostra a mensagem na tela, vamos ver:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MAXSIZE 255
#define PORT 3490
void usage(char *pname){
fprintf(stderr,"Uso:%s <IP>\n",pname);
exit(1);
}
int main(int argc, char *argv[]){
if(argc!=2)
usage(argv[0]);
int sockfd;
int n;
struct sockaddr_in end;
char buff[MAXSIZE];
memset(buff,'\0',sizeof(char)*MAXSIZE);
/* Creating the socket */
if( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0 ){
perror("socket");
return errno;
}
/* Creating the structure */
end.sin_family=AF_INET;
end.sin_port=htons(PORT);
if( inet_pton(AF_INET,argv[1],&end.sin_addr) < 0){
perror("inet_pton");
return errno;
}
memset(end.sin_zero,'\0',8);
/* Connecting */
if(connect(sockfd,(struct sockaddr*)&end,sizeof(struct sockaddr)) < 0){
perror("connect");
return errno;
}
while( (n=read(sockfd,buff,MAXSIZE)) > 0){
fprintf(stdout,"%s",buff);
}
/* closing */
close(sockfd);
return 0;
}
Compilar com:
gcc -o client client.c
Podemos ver que ele é muito parecido com o servidor:
1)socket(), cria o socket
2)Seta os dados do socket
3)connect() conecta o socket ao servidor
4)Utiliza read() e send() para se comunicar com o servidor
5)close() para finalizar a conexão.
Conclusões finais
Obvio que aplicações reais são bem mais complexas do que isso, porém com isso podemos ter uma idéia de como programar coisas básicas para rede. Espero que tenha ajudado.