Programação UDP e TCP sobre "Sockets de Berkeley"


Nota: estão disponíveis alguns exemplos no final deste documento.

Os "sockets" de Berkeley são uma API genérica para programação sobre protocolos de comunicação. A implementação das system-calls desta interface é "standard" em todos os sistemas operativos UNIX e estende-se também a muitas outras plataformas. No final deste documento pode encontrar algumas notas sobre portabilidade para Visual C++ / Sockets Windows.

Dentro do possível é mantida a semântica associada aos descritores de ficheiros (io.h), os descritores são números inteiros usados para facultar o acesso ao controlo das comunicações embebido no núcleo e são aqui conhecidos por "sockets".

De um modo geral, quando ocorre um erro as "system-call" devolvem o valor -1, as aplicações devem verificar sempre a ocorrência de erros, para simplificar o código dos exemplos aqui apresentados este aspecto é muitas vezes omitido.

Como esta API foi concebida para suportar diversos protocolos terá de suportar diversos formatos de dados, tais como os endereços. Estes são armazenados numa estrutura especial genérica:

struct sockaddr
      {
      u_short	sa_family;
      char	sa_data[14];
      };

Para cada família de protocolos existem depois estruturas mais especificas e adequadas, por exemplo para o protocolo IP, que nos interessa neste momento, estão definidas as seguintes estruturas:

struct in_addr
    {
    u_long s_addr; /* endereço IP (4 bytes) */
    };
struct sockaddr_in
    {
    short sin_family;
    u_short sin_port; /* porta (2 bytes) */
    struct in_addr sin_addr;
    char sin_zero[8];
    };

Como se pode verificar a estrutura sockaddr e sockaddr_in têm exactamente o mesmo tamanho e o campo que designa a família de protocolos (sa_family e sin_family) têm posição coincidente.

Abertura de Sockets

Para ter acesso aos protocolos de comunicação começa-se por abrir um socket, para o efeito utiliza- se a "system-call" "socket" que devolve um descritor necessário em todas as operações subsequentes.

Como já foi referido, esta API permite a utilização de diversos protocolos, assim esta "system-call" envolve vários parâmetros que descrevem o protocolo a utilizar:

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

A tabela 1 mostra algumas combinações possíveis das constantes a usar para cada um dos parâmetros, para a família AF_INET que de momento nos interessa.

Tabela 1: Protocolos da Família AF_INET
familytype protocolProtocolo Usado
AF_INETSOCK_DGRAMIPPROTO_UDPUDP
AF_INETSOCK_STREAMIPPROTO_TCPTCP
AF_INETSOCK_RAWIPPROTO_ICMPICMP
AF_INETSOCK_RAWIPPROTO_RAWIP

Quando se utiliza os protocolos UDP e TCP o parâmetro protocol não é necessário, pois, fica implícito pelo parâmetro type, devendo então ser utilizado o valor zero. Em sistemas operativos protegidos como o UNIX, os "sockets" do tipo SOCK_ROW não são permitidos para utilizadores correntes.

Quando um descritor já não é necessário, tal como acontece com os ficheiros, deverá ser encerrado, para tal é usada a "system-call" close.

Associação de Endereços a "Sockets"

Antes de se poder receber ou enviar dados através de um descritor aberto, é necessário definir a porta que vai ser usada por esse descritor. Para este efeito deve ser utilizada a "system-call" bind:

int bind(int sock, struct sockaddr *myAddress, int addrLen);

O parâmetro sock é o descritor devolvido anteriormente pela função socket, o segundo parâmetro é um apontados para a estrutura que contém o endereço e o último parâmetro é o tamanho dessa estrutura.

Se o bind falha (devolve -1), a causa mais provável é que a porta definida em myAddress esteja já a ser usada.

A seguir apresenta-se um exemplo de utilização:

struct sockaddr_in myAddr;
int sock=socket(AF_INET,SOCK_DGRAM,0);
bzero((char *)&myAddr,sizeof(myAddr));
myAddr.sin_family=AF_INET;
myAddr.sin_addr.s_addr=htonl(INADDR_ANY);
myAddr.sin_port=htons(6520);
if(-1==bind(sock, (struct sockaddr *)&myAddr, sizeof(myAddr)))
    {
    puts("Porta ocupada");
    close(sock); exit(1);
    }

O valor INADDR_ANY representa o endereço IP da máquina onde a aplicação é executada. A utilização desta constante é vantajosa, se a máquina possui mais do que um endereço IP ("router") permite a recepção de dados em qualquer dos endereços da máquina.

No exemplo utiliza-se a porta 6520, se o "socket" for usado para recepção de dados o emissor terá de os enviar para esta porta.

Normalmente quando uma porta se destina a emissão não há necessidade de ter um valor preestabelecido, nestes casos pode usar-se o valor 0 que força o sistema a atribuir uma porta livre.

A função bzero é usada para colocar zeros na estrutura myAddr.

As funções htonl e htons permitem a conversão de números inteiros longos e curtos do formato interno da máquina ("host") para o formato usado na rede ("net"). As funções ntohl e ntohs, não usadas neste exemplo, realizam a operação inversa.

Emissão e Recepção de "datagramas" UDP

As "system-call" mais importantes para a emissão e recepção de "datagramas" UDP são sendto e recvfrom:

int sendto(int sock, char *buffer, int buffSize, int flags, struct sockaddr *to, int addrLen);
int recvfrom(int sock, char *buffer, int buffSize, int flags, struct sockaddr *from, int *addrLen);

O parâmetro sock é o descritor a ser usado para o envio ou recepção dos datagramas, buffer e buffSize definem onde estão os dados a enviar, ou onde devem ser colocados os dados a receber. O parâmetro flags permite usar algumas opções que alteram alguns aspectos do modo de funcionamento destas "system-call", normalmente terá o valor zero.

A estrutura to contém o endereço de destino para o "datagrama" a ser emitido, a estrutura from é usada para guardar o endereço de proveniência de um "datagrama" recebido. Note-se que o último parâmetro é passado de forma diferente em cada uma das "system- call".

Os exemplos simples apresentados a seguir ilustram a utilização destas "system-call", o primeiro envia "datagramas" UDP contendo uma linha de texto para a porta 8450 da máquina com endereço 193.136.62.4:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void main(void)
{
struct sockaddr_in me, target;
int sock=socket(AF_INET,SOCK_DGRAM,0);
char linha[81];
bzero((char *)&me,sizeof(me));
me.sin_family=AF_INET;
me.sin_addr.s_addr=htonl(INADDR_ANY); /* endereço IP local */
me.sin_port=htons(0); /* porta local (0=auto assign) */
bind(sock,(struct sockaddr *)&me,sizeof(me));
bzero((char *)&target,sizeof(target));
target.sin_family=AF_INET;
/* endereço IP de destino */
target.sin_addr.s_addr=inet_addr("193.136.62.4");
target.sin_port=htons(8450); /* porta de destino */
do
    {
    gets(linha);
    sendto(sock,linha,81,0,(struct sockaddr *)&target, sizeof(target));
    }
while(strcmp(linha,"exit"));
close(sock);
}

O segundo exemplo recebe "datagramas" na porta 8450 e deverá ser usado na máquina com endereço 193.136.62.4 para recepção dos "datagramas" emitidos pelo programa anterior:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void main(void)
{
struct sockaddr_in from, me;
int sock, addrlen=sizeof(me);
char linha[81];
sock=socket(AF_INET,SOCK_DGRAM,0);
bzero((char *)&me,addrlen);
me.sin_family=AF_INET;
me.sin_addr.s_addr=htonl(INADDR_ANY);
me.sin_port=htons(8450);
bind(sock,(struct sockaddr *)&me,addrlen);
do
    {
    recvfrom(sock,linha,81,0,(struct sockaddr *)&from, &addrlen);
    puts(linha);
    }
while(strcmp(linha,"exit"));
close(sock);
}

Enquanto a aplicação que recebe "datagramas" tem de usar uma porta bem definida, aplicação que os emite utiliza uma porta atribuída dinamicamente pelo sistema.

Note-se que embora ambos os exemplos usem duas estruturas para guardar endereços (local e remoto), apenas no caso do emissor é necessário definir previamente o endereço remoto. Neste ultimo exemplo, após a recepção de uma datagrama, o endereço de proveniência está disponível na estrutura from.

Para apresentar no receptor a porta de proveniência e endereço IP basta converter os dados da estrutura from para alfanumérico. A função inet_ntoa converte um endereço IP guardado dentro numa estrutura in_addr em texto legível:

char *inet_ntoa(struct in_addr inaddr);

No exemplo anterior, para apresentar a porta e endereço de proveniência do "datagrama", antes da linha de texto, bastaria acrescentar as seguintes linhas.

printf("%i :", ntohs(from.sin_port));
puts(inet_ntoa(from.sin_addr));

Quando se usam "datagramas" para diálogo entre duas aplicações a verificação de endereços de proveniência é importante porque não existe conexão. Para o receptor responder ao emissor tem usar o endereço de proveniência, por outro lado os "datagramas" podem ser originários de terceiros.

Servidores UDP elementares

O modelo cliente-servidor está muitas vezes associado ao conceito de sessão que implica a manutenção pelo servidor de contextos separados para cada um dos clientes activos. Neste caso diz-se que o servidor é "statefull", e a resposta a um pedido depende do diálogo privado anterior entre o cliente e o servidor.

O protocolo UDP não é orientado à conexão, pelo que impossibilita uma implementação directa de sessões entre clientes e servidores. Como o servidor não tem estado os pedidos devem limitar-se a um datagrama, o mesmo se passando com as respostas.

Isto não quer dizer que seja impossível a implementação de sessões sobre UDP, mas como não existe suporte de conexões o esforço da aplicação será substancialmente maior já que terá de ser esta a realizar a distinção entre "datagramas" de diferentes origens e processar os mesmos em diferentes contextos.

Por outro lado o UDP não é fiável, para implementar sessões sobre ele será necessário que as aplicações cliente e servidor procedam elas próprias à detecção e correcção dos diversos tipos de erros a que os "datagramas" estão sujeitos.

Por estas razões o UDP é mais adequado para servidores sem estado em que os pedidos e respostas se limitam a um único datagrama. Quando se pretende estabelecer uma sessão cliente-servidor é aconselhável utilizar uma conexão TCP.

Um servidor UDP é uma aplicação que escuta constantemente a chegada de "datagramas" contendo pedidos, processa os pedidos e envia um "datagrama" de resposta ao emissor do mesmo.

Como é o cliente que toma a iniciativa de contactar o servidor, este ultimo tem de escutar os pedidos numa porta preestabelecida entre os dois. O cliente pode usar uma porta qualquer atribuída dinamicamente pelo sistema. Para enviar a resposta ao cliente, o servidor pode consultar o endereço de proveniência do pedido.

Exercício

Implementar um servidor UDP que receba um "datagrama" com uma linha de texto e a devolva ao emissor convertida para maiúsculas. Implemente também o cliente, o endereço IP da máquina servidora deverá ser passado ao cliente como parâmetro na linha de comando.

Sockets não bloqueantes

Durante os testes sobre os programas desenvolvidos no exercício anterior poderá ter notado uma grave deficiência: se um "datagrama" se perde o cliente bloqueia.

Este bloqueio deve-se ao facto de o cliente, após o envio do "datagrama" ao servidor, invocar recvfrom para receber a resposta. Se o "datagrama" não chega ao servidor, se o servidor não está operacional ou se o "datagrama" de resposta se perde, o cliente fica indefinidamente bloqueado nesta system-call.

Para tornar um "socket" não bloqueante pode ser usada a "system-call" ioctl para activar a característica FIONBIO ("File I/O NonBlocking I/O").

A "system-call" ioctl permite configurar vários aspectos de um descritor aberto. Tem três parâmetros: o descritor, um identificador da característica a alterar e finalmente um apontador para o novo valor dessa característica.

int ioctl( int socket, unsigned long request, char *arg);

Como habitualmente, em caso de erro devolve - 1.

Os identificadores de características (parâmetro "request") estão definidos no ficheiro sys/ioctl.h, pelo que a sua utilização é muito simples. É no entanto necessário atender a que dependendo de request, o tipo de dado para o qual arg aponta varia.

No caso de FIONBIO arg deve apontar para um inteiro, se este tem valor zero esta característica é desactivada, se tem valor diferente de zero será activada.

Usando esta "system-call", pode obter-se facilmente um "socket" não bloqueante tal como é exemplificado na seguinte sequência de código:

...
int sock, value=1;
sock=socket(AF_INET,SOCK_DGRAM,0);
ioctl(sock, FIONBIO, &value);
...

Ao tornar o "socket" não bloqueante é necessário atender a que a emissão de um "datagrama" pode falhar. Se o sistema está ocupado e um "datagrama" é emitido num "socket" bloqueante, a operação é suspensa até que o sistema esteja livre. Se o "socket" é não bloqueante a operação falha e a "system-call" retorna -1.

Sob o ponto de vista de recepção, a respectiva "system-call" terá de ser invocada ciclicamente ("polling") para verificar se chegou algum datagrama.

Exercício

Introduza as modificações necessárias no cliente anterior para que a situação de bloqueio seja eliminada. Se após um tempo de espera não chega a resposta o cliente volta a enviar a linha de texto ao servidor, este procedimento será repetido várias vezes. Mais tarde serão abordados outros mecanismos que permitem a recepção de "datagramas" de um modo totalmente assíncrono.

"Broadcast" de "datagramas"

Um "datagrama" pode ser enviado em "broadcast", para o efeito é usado o endereço de destino 255.255.255.255. Um "datagrama" enviado em "broadcast" será recebido por todas as máquinas que estão directamente ligadas à mesma rede IP onde a operação é realizada.

A principal utilidade do "broadcast" é permitir contactar uma aplicação que sabemos que está à escuta numa dada porta, sem necessidade de saber em que máquina se encontra.

A emissão em "broadcast" também pode ser usada por aplicações que desejam ser contactadas por outras. Neste caso a aplicação envia periodicamente um "datagrama" que funciona como anuncio.

Os procedimentos descritos são comuns na maioria das redes locais com partilha de recursos "peer-to- peer" ou com servidores dedicados, tais como MicroSoft, IBM e Novell.

Na comunidade "internet", a utilização de "broadcast" para estas finalidades não é tão popular partindo-se do pressuposto que a localização dos servidores é conhecida.

A utilização da emissão em "broadcast" tem dois aspectos negativos:

  • Só pode ser usada dentro de uma mesma rede IP. Isto torna a rede menos transparente, sendo necessário conhecer previamente a sua estrutura.
  • O tráfego gerado chega a todos os pontos da rede IP onde é emitido. Isto é negativo porque os dispositivos de comutação que se baseiam em endereços de nível 2 não podem actuar.

Para que um "socket" possa ser usado para envio em "broadcast" tem de ser previamente definida essa opção. Existem duas "system-call" que permitem manusear as opções de funcionamento de um "socket":

int getsockopt(int sock, int lev, int opt, char *arg, int *len);
int setsockopt(int sock, int lev, int opt, char *arg, int len);

As opções estão divididas em níveis (lev), cada nível possui diversas opções (opt). Tal como para a "system-call" ioctl, cada tipo de opção é definida usando um tipo especifico, para o qual o parâmetro arg deve apontar. O último parâmetro indica o comprimento do argumento.

Relativamente ao envio em "broadcast" o nível está definido na constante SOL_SOCKET e a opção é SO_BROADCAST. O valor adequado para esta opção é um inteiro que deverá ter um valor zero para desactivar e diferente de zero para activar.

O extracto seguinte apresenta a utilização da "system-call" setsockopt de modo a permitir o envio de "datagramas" em "broadcast":

...
int sock, v=1;
sock=socket(AF_INET,SOCK_DGRAM,0);
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &v, sizeof(v));
...

Filtragem de "datagramas"

Embora o protocolo UDP não suporte conexões, é possível definir previamente um "parceiro" para um "socket". Sem existir conexão define-se contudo uma associação entre o "socket" local e o endereço e porta remota.

Para obter este efeito usa-se a "system-call" connect, os parâmetros são exactamente iguais aos do bind, mas como endereço devemos utilizar o endereço de destino.

Como o endereço de destino é predefinido, podem ser usadas as "system-call" write ou send, esta última é uma variante de sendto que omite o endereço de destino. No contexto do exemplo emissor anteriormente apresentado seria:

...
target.sin_family=AF_INET;
/* endereço IP de destino */
target.sin_addr.s_addr=inet_addr("193.136.62.4");
target.sin_port=htons(8450); /* porta de destino */
connect(sock, (struct sockaddr *)&target, sizeof(target));
do { gets(linha); send(sock,linha,81,0 );}
while(strcmp(linha,"exit"));
close(sock);
}

As consequências da utilização da "system-call" connect, sob o ponto de vista de recepção são extremamente interessantes pois é realizada uma filtragem dos "datagramas" recebidos. Apenas são passados à aplicação aqueles que têm como origem o endereço/porta indicados. Igualmente aqui podem ser utilizadas as "system-call" read e recv que não necessitam de uma estrutura para guardar o endereço de proveniência.

Estabelecimento de Conexões TCP

O estabelecimento da conexão garante a existência de um canal bidireccional dedicado de transferência de "bytes".

O protocolo TCP proporciona serviços orientados à conexão. Antes de ser possível enviar ou receber dados há necessidade de se estabelecer a conexão.

O estabelecimento da conexão exige a colaboração entre duas aplicações, uma aplicação escuta pedidos de conexão numa dada porta, enquanto a outra emite um pedido de conexão para essa porta.

Para este feito, as "system-call" a usar são as seguintes:

int listen(int sock, int backlog);

Esta "system-call" permite colocar um "socket" em escuta de pedidos de conexões numa dada porta. A porta deverá ter sido previamente definida com a "system-call" bind.

O valor backlog define o número de pedidos de conexão que podem ser mantidos em espera sem serem aceites pela "system-call" accept.

int accept(int sock, struct sockaddr *from, int *addrLen);

A "system-call" accept permite aceitar um pedido de conexão, devolve um novo "socket" já ligado ao emissor do pedido e o "socket" original mantém-se em escuta. A estrutura from é usada para guardar o endereço de proveniência do pedido de conexão.

A "system-call" accept é bloqueante, quando é invocada o processo fica suspenso até que chegue um pedido de conexão, a menos que um pedido já tenha sido recebido desde a invocação de listen.

Note-se que o conceito de conexão implica que o "socket" devolvido por accept é totalmente independente do "socket" original. Enquanto o "socket" original continua à escuta de pedidos de conexão de qualquer proveniência o novo "socket" está associado a uma conexão entre duas aplicações e portanto permite a circulação de dados exclusivamente entre essas duas aplicações.

A aplicação que toma a iniciativa de estabelecer uma conexão utiliza para o efeito a "system-call" connect:

int connect(int sock,struct sockaddr *address, int addressLen)

O "socket" sock deverá ser do tipo apropriado ("SOCK_STREAM") e não necessita de ter atribuída uma porta ("bind"). Esta "system-call" encarrega-se de a definir dinamicamente.

A estrutura address deverá conter o endereço de destino no qual uma aplicação deverá estar à escuta de conexões. Se no endereço indicado isso não se verifica esta "system-call" devolve -1.

Recepção/Envio de dados sobre conexões TCP

Uma vez estabelecida a conexão passa a existir um canal dedicado para comunicação entre os dois intervenientes que não está acessível a terceiros.

O endereço de destino está definido por natureza e podem ser utilizadas as "system-call" read e write, respectivamente para receber e enviar dados:

int read(int sock, char *buffer, int len);
int write(int sock, char *buffer, int len);

A "system-call" read recebe len bytes, do "socket" sock, colocando-os no buffer. A "system-call" write envia len" bytes "do buffer pelo "socket" sock.

A utilização destas "system-call" deve ser cuidadosa: ambas devolvem o número de" bytes "recebidos ou emitidos que podem não coincidir com o parâmetro len. Isto é particularmente verdade se o "socket" é não bloqueante. O programador deve preocupar-se com este aspecto, invocando novamente as "system-call" para emitir ou receber os" bytes "em falta.

Por outro lado, tal como recvfrom, a "system-call" read bloqueia até que sejam recebidos os dados, isto quer dizer que se a invocarmos para receber 1000" bytes "de uma conexão, podemos ter de esperar até que essa quantidade de informação chegue. Note-se que a "system-call" recvfrom desbloqueava quando chegasse um "datagrama" UDP de qualquer tamanho.

O extracto seguinte estabelece de uma conexão TCP com a porta 8451 da máquina 193.136.62.4, seguida do envio de linhas de texto.

...
struct sockaddr_in target;
int sock;
char linha[81];
sock=socket(AF_INET,SOCK_STREAM,0);
bzero((char *)&target,sizeof(me));
target.sin_family=AF_INET;
target.sin_addr.s_addr=inet_addr("193.136.62.4");
target.sin_port=htons(8451);
connect(sock,(struct sockaddr *)&target,sizeof(target));
do
    {
    gets(linha);
    write(sock,linha,81);
    }
while(strcmp(linha,"exit"));
close(sock);
...

Note-se a ausência da invocação da "system-call" bind, a "system-call" connect encarrega-se de definir a porta local.

O extracto seguinte recebe uma conexão TCP na porta 8450, e de seguida lê linhas de texto da conexão estabelecida.

...
struct sockaddr_in from, me;
int newSock,sock, addrlen=sizeof(from);
char linha[81];
sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
bzero((char *)&me,addrlen);
me.sin_family=AF_INET;
me.sin_addr.s_addr=htonl(INADDR_ANY);
me.sin_port=htons(8451);
bind(sock,(struct sockaddr *)&me,addrlen);
listen(sock,5);
newSock=accept(sock,(struct sockaddr *)&from,&addrlen);
close(sock);
do
{
read(newSock,linha,81);
puts(linha);
}
while(strcmp(linha,"exit"));
close(newSock);
...

Note-se que o "socket" usado para receber o pedido de conexão não serve para troca de dados e neste exemplo é fechado após o estabelecimento da conexão. O novo "socket", associado à conexão é depois usado para recepção de dados.

Quando um "socket" TCP é definido como não bloqueante, as consequências são as seguintes:

  • Sempre que uma operação falha por não existir a possibilidade de execução sem bloqueio a "system- call" devolve -1 e a variável global errno é colocada com o valor EWOULDBLOCK.
  • connect: ao contrário do que acontece com "sockets" UDP, sobre "sockets" TCP, esta "system-call" bloqueia a execução até que a conexão esteja estabelecida. Se o "socket" é não bloqueante devolve imediatamente o valor -1, e define o valor da variável global errno com o valor EINPROGRESS. indicando que a operação não falhou.

  • accept: se existe algum pedido devolve o novo "socket", caso contrário gera um erro.

  • read: efectua leituras parciais, o total de" bytes "lidos é devolvido pela "system-call"

  • write: efectua escritas parciais, tal como para a anterior devolve o número de" bytes "processados.

Servidores TCP

Uma característica fundamental num servidor é que enquanto está a responder a um cliente deve continuar disponível para atender outros clientes.

Usando datagramas isto obriga a que um único processo atenda todos os pedidos dirigidos para a porta, dificultando a manutenção de contextos independentes para cada cliente.

Com a existência de conexões tudo fica muito simplificado, mesmo que esteja a ser usada uma única porta, cada conexão é independente das restantes. Pode por isso existir um processo independente para lidar com cada cliente.

Num ambiente multi-processo como o "UNIX" , isto é relativamente simples de implementar:

  1. Tipicamente um servidor TCP aguarda conexões estabelecidas por clientes (system-calls listen e accept).
  2. Após o estabelecimento da conexão (saída da "system-call" accept), o servidor deve dividir-se em dois processos (fork):
    • O processo pai fecha o novo "socket" e invoca novamente a "system-call" accept para aceitar o pedido de conexão seguinte.
    • O processo filho fecha o "socket" original e presta os serviços comunicando com o cliente através da conexão associada ao novo "socket".

Assim, a estrutura tipo de um servidor "TCP" pode ter a seguinte forma:

...
bind(sock, .....);
listen(sock,5);
for(;;)
    {
    newSock=accept(sock,....,..);
    if(fork())
        {
        /* processo pai */
        close(newSock);
        }
    else
        {
        /* processo filho */
        close(sock);
        ...
        processamento do pedido com comunicação
        TCP sobre o newSock
        ...
        close(newSock);
        exit(0);
        }
    }

Exercício

Implementar um servidor TCP recebe nomes de ficheiros de texto e envia o seu conteúdo.

Resolução de Nomes de Máquinas

Nos exemplos anteriores é necessário fornecer ao cliente o endereço IP do servidor, contudo é muito mais cómodo usar nomes de máquinas.

As associações entre nomes de máquinas e endereços IP podem estar definidas localmente, mas a solução geral é a utilização de servidores de nomes que são inquiridos pelos interessados. A obtenção do endereço IP a partir do nome de uma máquina é normalmente conhecida por resolução do nome.

Graças a uma hierarquia de domínios de nomes sustentada por um conjunto de servidores interligados é possivel resolver o nome de qualquer máquina ligada à "internet". Cada máquina utiliza um servidor de nomes correspondente ao seu domínio.

Para resolver o nome de uma máquina pode ser directamente utilizada a função gethostbyname:

hostent *gethostbyname(char *hostName);

Esta função devolve um apontador para uma estrutura do tipo hostent, o campo h_addr_list, é um apontador para um vector que contem os vários endereços IP da máquina cujo nome foi passado como parâmetro. Na prática, a menos que se trate de um "router", cada máquina apenas tem um endereço IP pelo que se usa o primeiro elemento do vector.

Os endereços IP guardados no vector estão sob a forma de inteiros longos, já em formato de rede pelo que podem ser directamente copiados para o campo sin_addr das estruturas sockaddr_in.

O exemplo seguinte ilustra a utilização desta função, supondo-se que o nome da máquina é fornecido como primeiro parâmetro da linha de comando:

...
struct sockaddr_in target;
struct hostent *host;
...
host=gethostbyname(argv[1]);
target.sin_addr=*(struct in_addr *) *host->h_addr_list;
target.sin_port=htons(......);
...

Melhorar a utilização da rede

Nos exemplos elementares apresentados verifica-se que a quantidade de octetos enviada é muitas vezes bastante maior do que a informação útil.

Mais especificamente para enviar linhas de texto que podem conter apenas alguns caracteres está a ser enviado sempre todo o "buffer" com 81 caracteres.

No caso do UDP este problema pode ser resolvido de uma maneira muito simples. Basta indicar à "system-call" sendto a quantidade exacta de octetos a enviar:

sendto(sock,linha,1+strlen(linha),0,(struct sockaddr *)&target, ...);

Note-se que a função strlen devolve o comprimento do "string" (número de caracteres), mas o zero que indica o seu final também deve ser enviado, daí a adição de uma unidade.

Se para o UDP a solução é simples, para o TCP torna-se algo complicada. O TCP estabelece conexões, numa conexão os dados são transmitidos em continuo, não existindo qualquer tipo de delimitador de blocos de dados.

O problema coloca-se sob o ponto de vista do receptor que, não sabendo à partida a quantidade de dados que vai receber não pode invocar directamente a "system-call" read para os ler na totalidade.

Se a quantidade de dados a ler indicada à "system- call" read é menor do que os que foram enviados então a operação será incompleta, se ocorre a situação contrária o processo fica bloqueado.

A solução corrente consiste em definir previamente entre as entidades qual será o separador utilizado, a invocação da "system-call" read será depois realizada caractere a caractere sendo interrompida quando o separador é lido.

Por exemplo para o caso do envio de linhas de texto esta ideia pode ser implementada com os seguintes excertos de código:

/* EMISSOR */
...
write(sock,linha,1+strlen(linha));
...
/* RECEPTOR */
...
aux=linha-1;
do
    {
    aux++;
    read(sock,aux,1);
    }
while(*aux);
...

Recepção assíncrona

Por recepção assíncrona entende-se aqui recepção de dados por uma aplicação sem bloqueio da mesma. Independentemente das chegadas de dados a aplicação deve continuar a funcionar normalmente.

A recepção assíncrona é necessária se a aplicação recebe dados em diferentes portas, ou mesmo que receba apenas numa porta, se necessita de executar outras tarefas e não pode ficar suspensa à espera que os dados cheguem.

Um dos métodos possíveis já referido é definir o "socket" como não bloqueante, nesse caso a aplicação deverá invocar periodicamente as "system-call" recvfrom ou read para verificar se chegaram dados ("polling").

Existem contudo outros métodos que podem exigir um menor esforço para a aplicação:

  • Utilização de sinais - o sistema operativo pode ser instruído para enviar ao processo um sinal sempre que chegam dados. Os sinais são processados por funções definidas na aplicação. Quando o sinal chega ao processo a sua execução é suspensa enquanto a referida função é executada. A função de processamento do sinal pode receber directamente os dados ou simplesmente alterar uma variável global que assinala a chegada de dados.
  • Mini-processos - os mini-processos ou "Light Weight Processes" (LWP), também conhecidos por "Threads", são unidades de execução sequencial que são executadas concorrentemente dentro do mesmo processo. Como correm dentro do mesmo processo partilham o espaço de endereçamento. É por isso simples colocar um "thread" à espera da chegada de dados enquanto os restantes continuam a sua execução normal.
  • "System-call" select - esta é uma solução parcial, permite escutar dados em mais do que uma porta, o processo fica bloqueado, mas quando chegam dados a uma das portas desbloqueia.

Não será aqui detalhada a solução "threads", não existe qualquer dificuldade na sua implementação e existem grandes variações quanto às "system-calls" usadas para trabalhar com "threads".

O "thread" que recebe dados invoca recvfrom ou read e fica bloqueado até à chegada de dados, mas os restantes "threads" do processo continuam a execução normal.

A utilização de sinais envolve alguns passos bem definidos, é necessário definir uma função que vai ser invocada quando o sinal SIGIO é recebido.

O PID tem de ser associado ao "socket" para que quando os dados cheguem, o sinal seja enviado ao processo correcto. Finalmente o "socket" tem de ser preparado para recepção assíncrona.

O extracto seguinte exemplifica o procedimento referido:

char linha[81], dataReady=0;
struct sockaddr_in from;
int len=sizeof(from);
int sock=socket(AF_INET,SOCK_DGRAM,0);
int sigio_handler(void)
{
dataReady=1;
recvfrom(sock, linha, 81, 0, (struct sockaddr *)&from,&len);
}
void main(void)
{
signal(SIGIO,sigio_handler);
fcntl(sock, F_SETOWN, getpid());
fcntl(sock, F_SETFL, FASYNC);
sigblock(SIGIO);
for(;;)
    {
    if(dataReady)
        {
        puts(linha);
        dataReady=0;
        sigsetmask(0);
        sigblock(SIGIO);
        }
/* outras actividades de processamento da aplicação */
    }
close(sock);
}

É claro que o processamento dos dados recebidos poderia se realizado directamente na função sigio_handler. Embora esta solução liberte totalmente a aplicação principal da recepção de dados, não soluciona o problema da recepção em várias portas. O sinal SIGIO indica que existem dados prontos, mas se existem vários descritores a ser usados para recepção, não indica em qual deles se encontram os dados.

Para resolver este problema pode ser usada a "system-call" select:

int select(int maxFd, fd_set *rd, fd_set *wr, fd_set *ex, struct timeval *timeout);

A estrutura apontada por timeout contém o tempo de máximo de bloqueio desta "system- call":

struct timeval
    {
    long tv_sec;
    long tv_usec;
    }

Esta estrutura pode conter os valores zero e nesse caso não existe bloqueio ("poll").

O apontador passado à "system-call" pode ser NULL, nesse caso o bloqueio verifica-se até à chegada de dados.

O objectivo desta "system-call" é detectar condições especificas num conjunto de "sockets", essas condições são:

  • pronto para leitura (verificado para os "sockets" rd)
  • pronto para escrita (verificado para os "sockets" wr)
  • condição de excepção (verificado para os "sockets" ex)
O parâmetro maxFd indica o número de descritores controlar. Este número de descritores refere-se ao seu valor. Não esquecer que os descritores são números inteiros, aqui designados por "sockets", o descritor 0 e 1 são usados para o stdin e stdout.

Devido ao modo como são armazenados, os conjuntos de "sockets" a monitorizar são passados à "system-call" de um modo algo rebuscado, para o efeito é definido o tipo fd_set e 4 macros para lidar com este tipo:

FD_ZERO(fd_set *fds);
FD_SET(int fd, fd_set *fds);
FD_CLR(int fd, fd_set *fds);
FD_ISSET(int fd, fd_set *fds);

A macro FD_ZERO deve ser sempre usada para inicializar o tipo fd_set.

As macros FD_SET e FD_CLR, respectivamente incluem ou retiram os "socket" fd da variável fds.

A macro FD_ISSET permite testar se um "socket" está incluído num conjunto do tipo fd_set.

A "system-call" select retorna o número de condições detectadas, para saber a qual dos "sockets" diz respeito é necessário utilizar a macro FD_ISSET para analisar o conjunto que interessa.

A "system-call" select pode também ser usada para receber conexões (accept), esta situação é tratada como se fosse uma chegada de dados.


Bibliografia

Stevens W. R. 1990, "UNIX Network Programming", Prentice Hall, Englewood Cliffs, New Jersey, 1990.

Sun Microsystems 1990, "Network Programming Guide", Sun Microsystems, Inc., 1990.


Programas exemplo

Todos estes exemplos estão a usar a interface de "loopback" (127.0.0.1): pode modificar os endereços de destino de modo a coincidirem com o "host" onde se encontra o receptor ou servidor.

01. UDP_SND.CEmissor de "datagramas" UDP
02. UDP_RCV.CReceptor de "datagramas" UDP
03. UDP_CLI.CCliente UDP (conversao para maiusculas)
04. UDP_SRV.CServidor UDP (conversao para maiusculas)
05. UDP_SRV1.CServidor UDP (04.), com apresentação do endereço de origem.
06. UDP_CLI1.CCliente UDP (03.), com detecção de falhas, usando um "socket" não bloqueante.
07. UDP_CLI2.CCliente UDP (03.), com emissão em "broadcast".
08. TCP_SND.CEmissor de conexão TCP.
09. TCP_RCV.CReceptor de conexão TCP.
10. TCP_CLI.CCliente TCP (ficheiros de texto), com resolução do nome do servidor.
11. TCP_SRV.CServidor TCP (ficheiros de texto).
12. TCP_CLI_B.CVersão B do cliente TCP com leitura byte a byte.
13. TCP_SRV_B.CVersão B do servidor TCP com leitura byte a byte.
14. UDP_CLI3.CCliente UDP (03.), com detecção de falhas, usando a "system-call" select.
15. XTCP_CLI.CCliente TCP (10.), com interface X, usando a "libsx" ("The Simple X Library").


Notas sobre a portabilidade para Visual C++

Os exemplos apresentados podem ser compilados em ambiente VC++ sem qualquer modificação importante, há contudo algumas particularidades a considerar:

  • É necessário usar o cabeçalho "Winsock2.h" (#include "winsock2.h").
  • Nas definições do projecto incluir a biblioteca "Ws2_32.lib".
  • É necessário inicializar a biblioteca dinâmica WS2_32.DLL, usando para esse efeito a função WSAStartup(). Isto deve ser feito antes de abrir descritores de rede.
  • Antes de terminar a aplicação, depois de ter fechado os descritores de rede, deve ser invocada a função WSACleanup() para libertar os recursos usados pela WS2_32.DLL.
  • Existe uma função especifica para fechar descritores de rede, a função closesocket(), que deve por isso ser usada em lugar da função close().

O protótipo da função WSAStartup() é:

int WSAStartup(WORD version, WSADATA *data);

O primeiro argumento é a versão "Winsock" pretendida, a mais actual é a versão 2.2 que se pode definir usando a função MAKEWORD(2,2), o octeto mais significativo representa a subversão, logo para se usar a versão 2.0 usa-se MAKEWORD(0,2).

O segundo argumento é um apontador para uma variável do tipo WSADATA que tem de ser localmente declarada, esta variável é usada para guardar informação relativa aos sockets windows.

A função WSAStartup() devolve zero em caso de sucesso.

A função WSACleanup() não tem argumentos e devolve um inteiro que terá o valor zero em caso de sucesso.

No exemplo seguinte apresenta-se o código referente ao exemplo 01 (udp_snd.c) com as alterações necessárias para ser compilado em VC++:

#include "winsock2.h"

int main(int argc, char* argv[])
{
struct sockaddr_in me, target;
int sock;
char linha[81];
WSADATA wsaData;

WSAStartup(MAKEWORD(2,2), &wsaData);
/***** ATENÇÃO: só pode abrir-se o socket depois de invocar WSAStartup ******/
sock=socket(AF_INET,SOCK_DGRAM,0);
me.sin_family=AF_INET;
me.sin_addr.s_addr=htonl(INADDR_ANY); /* endereco IP local */
me.sin_port=htons(0); /* porta local (0=auto assign) */
bind(sock,(struct sockaddr *)&me,sizeof(me));
target.sin_family=AF_INET;
/* endereco IP de destino */
target.sin_addr.s_addr=inet_addr("193.136.62.7");
target.sin_port=htons(8888); /* porta de destino */
do
       {
       gets(linha);
       sendto(sock,linha,81,0,(struct sockaddr *)&target,sizeof(target));
       }
while(strcmp(linha,"exit"));
closesocket(sock);
WSACleanup();
return 0;
}