Ir para o conteúdo

Sockets de Berkeley

Neste artigo vamos fazer uma breve introdução aos Sockets de Berkeley em linguagem C para ambientes UNIX e Linux. Basicamente os Sockets de Berkeley são uma API, isto é, um conjunto de bibliotecas de funções para a programação sobre protocolos de comunicação.

Modelo Cliente – Servidor

O modelo Cliente – Servidor é composto por um conjunto de dois programas em execução que comunicam entre si. O servidor está sempre à espera de pedidos efectuados pelos clientes, mas desconhece a sua localização. O cliente tem de conhecer obrigatoriamente a localização do servidor (IP) para poder ligar-se e comunicar com ele. O código do cliente e do servidor não têm de ser necessariamente diferentes, facilmente se desenvolve uma aplicação que tanto pode funcionar como cliente como para servidor. A grande diferença está no seu comportamento, ou seja, o servidor inicia a execução e aguarda uma ligação, o cliente inicia a execução e a ligação ao servidor.

Para dar um exemplo prático de como funcionam os sockets, vamos fazer um simples par de aplicações em que o cliente envia uma palavra com letras minúsculas para o servidor. O servidor trata de converter as letras em maiúsculas devolvendo a palavra ao cliente. Vamos utilizar o modo TCP do protocolo TCP/IP e o conjunto de aplicações são fechadas quando o cliente digita a palavra “exit”.

Cliente

O cliente tem o seguinte código. De seguida vamos analisá-lo passo a passo para melhor compreensão do mesmo.

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

int main() {
    struct sockaddr_in target;
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    char palavra[50], palavra2[50];
    int ad1 = sizeof(target);

    bzero((char *)&target, ad1);

    target.sin_family = AF_INET;
    target.sin_addr.s_addr = inet_addr("127.0.0.1");
    target.sin_port = htons(8450);

    if(connect(sock, (struct sockaddr *)&target, ad1) == -1)
    {
        close(sock);
        puts("Conexao falhada!");
        exit(0);
    }

    do {
        scanf("%s", palavra);
        write(sock, palavra, 50);
        if(strcmp(palavra, "exit") != 0){
            read(sock, palavra2, 50);
            printf("%s\n", palavra2);
        }
    }while(strcmp(palavra, "exit") != 0);
    close(sock);
    return 0;
}

Bibliotecas necessárias

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <unistd.h>

Definir e inicializar a estrutura de destino (servidor) e as variáveis

// definição das estruturas do cliente e do servidor
struct sockaddr_in me, from;
// criação dos sockets.o “sock” é declarado identicamente ao cliente
int newSock, sock = socket(AF_INET, SOCK_STREAM, 0);
// declaração das variáveis
int tam = 0, i = 0;
int ad1 = sizeof(me);
char palavra[50], palavra2[50];

// inicializa a estrutura do cliente
bzero((char *)&target, ad1);

Definir as propriedades do servidor ao qual nos vamos ligar

// indica a família do protocolo
target.sin_family = AF_INET;
// especifica o endereço (IP) do servidor
target.sin_addr.s_addr = inet_addr("127.0.0.1");
// porta que o programa vai usar
target.sin_port = htons(8450);

Estabelecer a ligação ao servidor

// efectua a ligação ao servidor. Se falhar (por exemplo o servidor estar em baixo) o programa termina
if(connect(sock, (struct sockaddr *)&target, ad1) == -1)
{
    close(sock);
    puts("Conexao falhou!");
    exit(0);
}

Ler dados do teclado

Vamos usar um método de leitura de daods muito simples. Lê uma palavra do teclado enquanto esta for diferente de exit. Em seguida envia-a para o servidor e recebe-a com letras maiúsculas

do {
    scanf("%s", palavra);
    // envia para o servidor os dados contidos na variável “palavra”
    write(sock, palavra, 50);
    if(strcmp(palavra, "exit") != 0){
        // recebe do servidor os dados e guarda-os na variável “palavra2”
        read(sock, palavra2, 50);
        printf("%s\n", palavra2);
    }
}while(strcmp(palavra, "exit") != 0);

Fechar o socket

close(sock);

Servidor

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

int main() {
    struct sockaddr_in me, from;
    int newSock, sock = socket(AF_INET, SOCK_STREAM, 0);
    int tam = 0, i = 0;
    int ad1 = sizeof(me);
    char palavra[50], palavra2[50];

    bzero((char *)&me, ad1);

    me.sin_family = AF_INET;
    me.sin_addr.s_addr = htonl(INADDR_ANY);
    me.sin_port = htons(8450);

    if(bind(sock, (struct sockaddr *)&me, ad1) == -1)
    {
        close(sock);
        puts("Porta Ocupada!"); 
        exit(0);
    }

    listen(sock, 5);
    newSock = accept(sock, (struct sockaddr *)&from, (void *)&ad1);
    close(sock);

    for(;;){
        read(newSock, palavra, 50);
        if(strcmp(palavra, "exit") != 0){
            tam = strlen(palavra);
            for(i=0;i<tam;i++){
                if((palavra[i]>='a') && (palavra[i]<='z')){
                    palavra2[i] = toupper(palavra[i]);
                }
            }
            palavra2[i] = '\0';
            write(newSock, palavra2, 50);
        }
        else{
            close(newSock);
            exit(0);
        }
    }
    return 0;
}

Segue a análise do código passo a passo

Definir a estrutura do cliente, do servidor e as variáveis

// definição das estruturas do cliente e do servidor
struct sockaddr_in me, from;
// criação dos sockets.o “sock” é declarado identicamente ao cliente
int newSock, sock = socket(AF_INET, SOCK_STREAM, 0);
// declaração das variáveis
int tam = 0, i = 0;
int ad1 = sizeof(me);
char palavra[50], palavra2[50];

// inicializa a estrutura do servidor
bzero((char *)&me, ad1);

Definir as propriedades do servidor

// indica a família do protocolo
me.sin_family = AF_INET;
// fica associado a todos os endereços IP do host local
me.sin_addr.s_addr = htonl(INADDR_ANY);
// porta em que o servidor vai estar à escuta
me.sin_port = htons(8450);

Alocar a porta

Se estiver ocupada é fechado o servidor:

// se a porta estiver ocupada o servidor não pode correr e é terminado
if(bind(sock, (struct sockaddr *)&me, ad1) == -1)
{
    close(sock);
    puts("Porta Ocupada!"); 
    exit(0);
}

Esperar por pedidos

É criado um novo socket para tratar apenas deste pedido, enquanto que o outro socket continua à espera de pedidos:

GeSHi (c):
// coloca o socket à escuta. Podem ser mantidos em espera 5 pedidos de ligação
listen(sock, 5);
// gera um novo socket específico para essa ligação
newSock = accept(sock, (struct sockaddr *)&from, (void *)&ad1);
// fechar o socket antigo
close(sock);

Ciclo infinito que trata dos pedidos do cliente

for(;;){
    // recebe os dados do cliente e guarda-os na variável “palavra”
    read(newSock, palavra, 50);
    if(strcmp(palavra, "exit") != 0){
        tam = strlen(palavra);
        for(i=0;i<tam;i++){
            if((palavra[i]>='a') && (palavra[i]<='z')){
                palavra2[i] = toupper(palavra[i]);
            }
        }
        palavra2[i] = '\0';
        // envia os dados que estão na variável “palavra2” para o cliente
        write(newSock, palavra2, 50);
    }
    else{
        close(newSock);
        exit(0);
    }
}