sábado, 24 de maio de 2014

Programação de Sockets para Linux

Introdução


Atualmente muitos softwares precisam se conectar à internet para realizar alguma tarefa específica. Conectividade é um assunto muito importante hoje em dia, ela se tornou praticamente obrigatoria no desevolvimento de novos aplicativos. Muitos negócios, em hoje em dia, dependem da internet para funcionarem, isso tem se tornado em algo gigantesco que gera muito dinheiro para diversas empresas. Pode-se citar, por exemplo, video games online que faturam mais que filmes de Hollywood, como o World of Warcraft que já faturou mais de $10 bilhões. Mas isso não se limita apenas à jogos, obviamente. Para muitas aplicações a conectividade é muito importante.

O que torna o conexão à internet possível, no caso dos softwares, são os sockets. Em termos simples, socket é um arquivo que representa uma conexão entre dois processos distintos, que podem estar no mesmo sistema ou não, através de um canal de transmissão. Para que a comunicação entre sockets funcione existem uma série de padrões e protocolos de comunicação. A internet que usamos no dia a dia usa a arquitetura TCP/IP. Essa arquitetura é dividida basicamente em cinco camadas. Os sockets estão no 4 nível do TCP/IP, especificamente na camada de transporte.

A arquitetura TCP/IP




A arquitetura TCP/IP apresenta cinco camadas, enquanto que o modelo de referência OSI usa 7 camadas. Cada camada tem um função especifica e bem definida. Abaixo é dado de forma resumida a função de cada camada no modelo OSI.

Nivel de Aplicação


Possui um conjunto de protocolos diversificado e orientado à aplicações bem definido. Exemplos: FTP, SNMP, DNS, HTTP, etc.

Nivel de Apresentação


Representação dos dados. Realiza transformações adequadas nos dados (compressão de textos, criptografia, etc). Seleção de sintaxes dos dados. Deve-se conhecer a sintaxe do sistema local e do sistema de transferência.

Nivel de Sessão


Permite a comunicação com sucesso entre as aplicações. Controle de diálogo ( em caso de interrupção, a rede continua de onde parou). Gerenciamento de atividades (prioridades). Sicronização.

Nivel de Transporte


Isola a parte de tranmissão da rede dos níveis superiores. Controle de sequência fim-a-fim. Controle de congestionamento. Detectação e recuperação de erros fim-a-fim

Nivel de Rede


Unidade de dados/pacotes. Roteamento de pacotes. Fragmentação e remontagem de pacotes. Conversão de endereços lógicos em endereços físicos. Não garante a entrega dos pacotes ou a sequência correta entre eles.

Enlace de Dados


Detecta e corrige erros ocorridos no nível físico. Converte um canal de transmissão não confiável em um canal confiável. Enquadramento de cadeias de bits. Redundância para detecção de erros (bits de controle e retransmissão). Controle de acesso.

Fisica


Fornece características mecânicas, elétricas e funcionais para ativar, manter e desativar conexões fisicas para tranmissão de bits. Define o padrão de sinalização com o meio físico. Não trata erros de tranmissão! Em resumo, converte quadros em sinais elétricos para serem transmitidos através do cabo de rede ou outro de meio de transmissão qualquer.

O que são Sockets


O estabelecimento de conexão entre processos diferentes é feito através de um socket. Através dele é possível acessar protocolos da camada de transporte, como TCP e UDP. Sockets garantem a intercomunicação bidirecional entre processos, executados localmente ou em máquinas conectadas através de uma LAN/WAN.
A comunicação entre sockets é feita por meio de um endereçamento. A formação dos endereços vai depender do dominio. Alguns exemplos de dominios de sockets:
  1. AF_UNIX - endereço é composto por um caminho dentro do sistema de arquivos. O domínio está restrito a árvore de diretórios acessível pelo processo que criou o socket. Utilizado quando os processos rodam em uma mesma máquina;
  2. AF_INET - O endereço é composto pelo endereço de rede da máquina (IP) e o número de identificação da porta sendo utilizada pelo processo. Este é o mais comumente usado

Tipos de sockets:
  1. SOCK_STREAM - indica que os dados irão trafegar pelo socket na forma de um stream de caracteres.
  2. SOCK_DGRAM - indica que os dados irão trafegar pelo socket na forma de datagramas.
  3. SOCK_RAW - um socket raw é um socket que permite o envio direto e recebimento de pacotes de Protocolo de Internet sem qualquer formatação específica da camada de transporte. Ele é muito usado por hackers para implementação de algumas técnicas de ataque.

API para desenvolvimento de sockets


Todo sistema operacional atual tem sua implementação do padrão TCP/IP, tanto para uso interno, como para o uso em software através de uma API. A API vai nos permitir criar um socket, envia dados pela rede, receber dados do mundo externo, "escutar" em uma porta à espera de conexões, etc. Cada sistema operacional que suporta rede tem algum tipo de Stack de rede. Linux não é exceção. A pilha de rede é o que permite que os aplicativos sejam capazes de acessar a rede por meio de um dispositivo de rede física. Dispositivos de rede pode ser modems, cable modems, ISDN, dispositivos Wi-Fi, placas Ethernet, placas Token Ring, etc. Em adição a isso, o Linux já suporta o protocolo IPv6.

Para que a conexão seja estabelecida, algum processo no sistema operacional precisa "tomar iniciativa" e iniciar o processo de conexão. Dessa forma, será haverá o processo cliente e o processo servidor. O cliente é processo responsavel por abrir uma conexão, ou socket, em uma determinada porta. O servidor é o processo responsável por esperar uma conexão em uma determinada porta, associada a um certo serviço. Existem várias formas e abordagens de programar um software cliente e um software servidor. Vamos abordar a implementação através de threads.

A API de sockets disponibilizada pelo Linux, ou o Windows, tem funções que seguem o padrão de sockets BSD, também conhecido como Berkeley sockets. O API de sockets BSD é escrito na linguagem de programação C. A maioria das outras linguagens de programação oferecem interfaces semelhantes, normalmente escritos como uma biblioteca com base na API C.

A interface de socket Berkeley é definido em vários arquivos de cabeçalho. Os nomes e conteúdo desses arquivos diferem ligeiramente entre implementações. De um modo geral, eles incluem:
<sys/socket.h>
Funções e estruturas de dados principais do núcleo BSD.
<netinet/in.h>
Família de endereços AF e AF INET inet6 e seus protocolos correspondentes PF_INET e PF_INET6. Amplamente utilizado na Internet, que incluem endereços IP e números de porta TCP e UDP.
<sys/un.h>
Família de endereços PF_UNIX/PF_LOCAL. Usado para comunicação local entre os programas em execução no mesmo computador. Não é usado em redes.
<arpa/inet.h>
Funções para manipulação de endereços IP numéricos.
<netdb.h>
Funções para traduzir nomes de protocolos e nomes de host em endereços numéricos. Pesquisas de dados locais, bem como DNS.
As funções mais comumente usadas em sockets e que são providas pela API Berkeley sockets: socket(), bind(), listen(), connect(), accept(), send(), recv().

Qualquer sistema operacional moderno tem suporte a estas funções, como o Linux, o MAC OS e o Windows.

Exemplos de sockets cliente e servidor

Cliente

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    char recvBuff[1024];
    struct sockaddr_in serv_addr; 

    if(argc != 2)
    {
        printf("\n Usage: %s  \n",argv[0]);
        return 1;
    } 

    memset(recvBuff, '0',sizeof(recvBuff));
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    } 

    memset(&serv_addr, '0', sizeof(serv_addr)); 

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5000); 

    if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    } 

    if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    } 

    while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0)
    {
        recvBuff[n] = 0;
        if(fputs(recvBuff, stdout) == EOF)
        {
            printf("\n Error : Fputs error\n");
        }
    } 

    if(n < 0)
    {
        printf("\n Read error \n");
    } 

    return 0;
}

Servidor


/*
    C socket server example, handles multiple clients using threads
    Compile
    gcc server.c -lpthread -o server
*/

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <pthread.h> //para threading, ligado através de lpthread 

 
//the thread function
void *connection_handler(void *);
 
int main(int argc , char *argv[])
{
    int socket_desc , client_sock , c;
    struct sockaddr_in server , client;
     
    //Create socket
    socket_desc = socket(AF_INET , SOCK_STREAM , 0);
    if (socket_desc == -1)
    {
        printf("Could not create socket");
    }
    puts("Socket created");
     
    //Prepare the sockaddr_in structure
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( 8888 );
     
    //Bind
    if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
    {
        //print the error message
        perror("bind failed. Error");
        return 1;
    }
    puts("bind done");
     
    //Listen
    listen(socket_desc , 3);
     
    //Accept and incoming connection
    puts("Waiting for incoming connections...");
    c = sizeof(struct sockaddr_in);
     
     
    //Accept and incoming connection
    puts("Waiting for incoming connections...");
    c = sizeof(struct sockaddr_in);
 pthread_t thread_id;
 
    while( (client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c)) )
    {
        puts("Connection accepted");
         
        if( pthread_create( &thread_id , NULL ,  connection_handler , (void*) &client_sock) < 0)
        {
            perror("could not create thread");
            return 1;
        }
         
        //Now join the thread , so that we dont terminate before the thread
        //pthread_join( thread_id , NULL);
        puts("Handler assigned");
    }
     
    if (client_sock < 0)
    {
        perror("accept failed");
        return 1;
    }
     
    return 0;
}
 
/*
 * This will handle connection for each client
 * */
void *connection_handler(void *socket_desc)
{
    //Get the socket descriptor
    int sock = *(int*)socket_desc;
    int read_size;
    char *message , client_message[2000];
     
    //Send some messages to the client
    message = "Greetings! I am your connection handler\n";
    write(sock , message , strlen(message));
     
    message = "Now type something and i shall repeat what you type \n";
    write(sock , message , strlen(message));
     
    //Receive a message from client
    while( (read_size = recv(sock , client_message , 2000 , 0)) > 0 )
    {
        //end of string marker
  client_message[read_size] = '\0';
  
  //Send the message back to client
        write(sock , client_message , strlen(client_message));
  
  //clear the message buffer
  memset(client_message, 0, 2000);
    }
     
    if(read_size == 0)
    {
        puts("Client disconnected");
        fflush(stdout);
    }
    else if(read_size == -1)
    {
        perror("recv failed");
    }
         
    return 0;
} 

Nenhum comentário:

Postar um comentário