2. Exemplos de software cliente.

 

2.1 Introdução

O Capítulo anterior discutiu os algoritmos básicos usados na construção de aplicações clientes, assim como técnicas específicas usadas para implementar esses algoritmos. Neste capítulo, serão apresentados exemplos completos de programas clientes que ilustram os conceitos em maiores detalhes. Mais importante, o capítulo mostra como um programador pode construir uma biblioteca de procedimentos que escondem os detalhes das chamadas à API sockets e tornam mais fácil construir clientes portáveis e fáceis de manter.

2.2 A Importância de Exemplos Simples

O TCP/IP define um grande número de serviços e de protocolos padrão para acessá-los. Estes serviços variam de triviais (p. ex.: geradores de caracteres usados para testar software de protocolos) a complexos (p. ex: serviço de transferência de arquivos usando identificação e proteção).

Projetar aplicações cliente-servidor de baixa complexidade no início do processo de aprendizagem da programação em rede, proporciona melhor compreensão do processo de desenvolvimento, enfatiza detalhes fundamentais dos algoritmos, ilustra como os programas utilizam as funções do sistema e desenvolve a intuição do aprendiz para projetos complexos (projetos multiprotocolo e multiserviço).

A abstração de funções (ou procedimentos) possibilita aos programadores a definição de operações de alto nível, o compartilhamento de código entre aplicações, e a redução de alterações no código provocadas por enganos durante o desenvolvimento.

2.3 Uma biblioteca de funções para a construção de programas clientes

Para relacionar-se com um servidor, o cliente deve escolher um protocolo (p.ex. TCP ou UDP), encontrar o endereço do hospedeiro remoto, encontrar e mapear o serviço desejado para um número de porta, alocar um soquete e conectá-lo. Abstrair esses passos em operações de mais alto nível contribuí para reduzir o tempo gasto pelo programador e para tornar o código produzido mais portável e fácil de manter. Por exemplo, as funções connectTCP(),e connectUDP(), podem se encarregar da alocação e conexão de um soquete usando a função connectsock(). Esta por sua vez pode usar a função errexit() para emitir mensagens de erro.

2.4. O Serviço Daytime

Os padrões TCP/IP definem um protocolo de aplicação que permite ao usuário obter a data e a hora corrente no formato inteligível ao ser humano. O nome oficial para o serviço é daytime. Para ter acesso ao serviço, o usuário invoca uma aplicação cliente. O cliente contata um servidor para obter a informação e a apresenta. O padrão não especifica a sintaxe exata, mas sugere vários formatos possíveis, como:

Dia da semana, dia do mês, ano hora-zona_de_tempo

Exemplo:

Friday, September 20, 2004 19:45:37-PST

 

O padrão especifica que o DAYTIME está disponível tanto para TCP como para UDP. Nos dois casos ele opera na porta 13. A versão TCP do DAYTIME usa a presença de uma conexão TCP para disparar a saída: quando uma nova conexão é aberta, o servidor forma uma cadeia de caracteres contendo a data e a hora atuais, envia a cadeia e fecha a conexão. O cliente não envia pedido algum. O padrão especifica que o servidor deve descartar qualquer dado enviado pelo cliente.

A versão UDP do DAYTIME requer que o cliente envie um pedido. O pedido consiste em um datagrama UDP arbitrário. Sempre que o servidor recebe um datagrama, ele formata a data e a hora corrente, coloca a string resultante em um datagrama e o envia ao cliente. O servidor descarta o datagrama recebido assim que tiver enviado a resposta.

2.4.1 Implementação de um cliente TCP para o DAYTIME.

O arquivo TCPdaytime.c contém o código para um cliente TCP que acessa o serviço DAYTIME. Observe que usar connectTCP() simplifica o código. Uma vez que a conexão foi estabelecida, DAYTIME simplesmente a entrada recebida pela  conexão e apresenta o resultado, iteragindo até encontrar uma condição de final de arquivo.

2.4.2 Lendo de uma conexão TCP

O exemplo DAYTIME ilustra uma idéia importante: o TCP oferece um serviço stream e não garante que o limite de um registro seja preservado. Na prática, o paradigma do stream significa que  o TCP desacopla as aplicações de envio e recepção.  Por exemplo, Suponha que uma aplicação envie 64 bytes de dados na uma primeira chamada write, seguida de 64 da segunda chamada write. A aplicação pode receber 128 bytes em uma única chamada read, ou pode receber 10 bytes na primeira chamada, 100 bytes na segunda chamada e 18 bytes na terceira chamada. O número de bytes retornados em uma chamada depende do tamanho dos datagramas na internet utilizada, do espaço disponível para buffers e dos atrasos encontrado quando o datagrama viaja pela internet.

Como o serviço stream do TCP não garante que os dados serão entregues nos mesmos blocos em que foram escritos, uma aplicação que recebe dados de uma conexão TCP não pode depender da entrega de todos os dados em uma única transferência; ela deve chamar recv (ou read) repetidas vezes até que todos os dados tenham sido obtidos.

2.5 O serviço Time

O TCP define um serviço que permite uma máquina obter a data e a hora atual de outra máquina. O nome oficial deste serviço é TIME e ele é bem simples: um programa cliente que está sendo executado em uma máquina envia uma requisição ao servidor que está em execução em outra máquina. Sempre que o servidor receber uma requisição, ele obtém a data e a hora corrente do seu sistema operacional local, codifica a informação em um formato padrão e a envia ao cliente.

Para evitar problemas decorrentes da localização de clientes e servidores em zonas de tempo (timezones) diferentes, o protocolo TIME especifica que toda a informação de hora e data deve ser representada em UCT (Universal Coordinated Time), ou apenas UT. Assim, o servidor converte sua hora local para a hora universal antes de enviar a resposta, e o cliente converte a hora universal para a hora local quando a resposta chega.

Ao contrário do serviço DAYTIME que se destina a usuários humanos, o serviço TIME é voltado para programas que armazenam ou manipulam informações de tempo. O protocolo TIME sempre especifica o tempo em um inteiro de 32 bits, representando o número de segundos a partir de uma data de referência (epoch date). O protocolo TIME  usa meia-noite, janeiro 1, 1900, como referência.

Usar a representação inteira permite aos computadores a transferência rápida de informações de horário, de uma máquina para outra, sem converter strings para inteiros e vice-versa. Assim, o serviço  TIME possibilita que um computador atualize o seu relógio usando o relógio de outro sistema.

2.5.1 Acesso ao serviço TIME

Clientes podem usar o UDP ou o TCP para acessar o serviço TIME na porta de protocolo 37 (tecnicamente, o padrão define dois serviços separados, um para UDP e outro para TCP). Um servidor TIME construído para TCP usa a presença de uma conexão para disparar a saída, de forma semelhante ao serviço DAYTIME. O cliente abre uma conexão TCP com o servidor TIME e espera a chegada de um inteiro. Quando o servidor detecta uma nova conexão, ele envia a hora corrente codificada como um inteiro, e então fecha a conexão. O cliente não envia dado algum porque o servidor nunca da conexão.

Clientes podem também ter acesso ao serviço TIME com o UDP. Para isto, uma cliente envia um pedido, que consiste em um datagrama qualquer. O servidor não processa o datagrama de entrada, exceto para extrair o endereço do remetente e o número da porta do protocolo para usá-los em sua resposta. O servidor codifica a instante de tempo corrente em um inteiro, coloca-o em um datagrama e o envia de volta ao cliente.

2.5.2 A precisão do serviço TIME e o atraso na rede.

Embora o serviço de tempo acomode diferenças entre as zonas de tempo, ele não manipula o problema da latência da rede. Se uma mensagem demora três segundos para viajar de um servidor para o cliente, o cliente receberá uma hora que estará três segundos atrás da hora do servidor. Outros protocolos mais complexos manipulam a sincronização de relógios. Porém o serviço TIME continua popular por três razões:

ù         ele é extremamente simples comparado ao serviço de sincronização de relógios;

ù         a maioria dos clientes contatam servidores em redes locais, onde a latência é de apenas alguns milisegundos;

ù         exceto quando são usados registros de tempo (timestamps) para controlar o processamento, as pessoas não ligam se os relógios de seus computadores diferem de pequenas quantidades de tempo.

No caso de maior precisão requerida é possível melhorar o serviço TIME ou usar protocolos alternativos. A forma mais fácil de melhorar a precisão do serviço TIME é calcular uma aproximação do retardo da rede entre o servidor e o cliente e adicioná-lo ao valor apresentado pelo servidor.

2.5.3 Um cliente UDP para o serviço TIME

O arquivo UDPtime.c contém o código de um cliente UDP para o serviço TIME. O código do exemplo  contata o serviço TIME enviando um datagrama. Ele então chama read para esperar pela resposta e extrair os dados dela. Após receber a hora, ela deve ser convertida para um formato apropriado para a máquina local. Primeiro deve ser usada a função ntohl para converter o valor de 32 bits (um long em C) do padrão de ordenação de bytes da rede para o padrão de ordenação de bytes do hospedeiro local. Depois deve ser feita a conversão para a representação de tempo da máquina local. O código apresentado é apropriado para o Linux. Como os protocolos da Internet, o Linux representa a hora em um inteiro de 32 bits e interpreta o inteiro como um montante de segundos. Ao contrário da Internet, entretanto, o Linux assume como data de referência o dia primeiro de janeiro de 1970. Assim, para converter da época do protocolo TIME para a época do Linux, o cliente deve subtrair o número de segundos entre primeiro de janeiro de 1900 e primeiro de janeiro de 1970 do valor recebido. O código do exemplo usa o valor de conversão 2208988800. Uma vez que este valor foi convertido para a representação compatível com o da máquina local, UDPtime poderá chamar a função de biblioteca ctime, que converte o valor em uma saída compreensível para humanos.

2.6 O serviço ECHO

O padrão TCP/IP especifica o serviço ECHO para os protocolos TCP e UDP. À primeira vista este protocolo parece não ter utilidade porque o servidor simplesmente retorna todos os dados que recebe de um cliente. Apesar de sua simplicidade, os serviços ECHO são importantes ferramentas que os gerentes de rede usam para testar a ‘alcançabilidade’ na rede, depurar software de protocolo e identificar problemas de roteamento.

O serviço de ECHO TCP especifica que o servidor deve aceitar uma conexão de entrada, ler os dados da conexão, e escrever os dados de volta, usando esta conexão, até que o cliente termine a transferência. Enquanto isso, o cliente envia entrada e as de volta.

2.6.1 O cliente TCP para o serviço ECHO

O arquivo TCPecho.c contém um cliente simplificado para o serviço ECHO. Após abrir uma conexão, TCPecho entra em um loop em que uma linha de entrada, envia a linha através da conexão TCP para o servidor de ECHO, a resposta e a apresenta. Após todas as linhas de entrada terem sido enviadas ao servidor, recebidas de volta e apresentadas com sucesso, o cliente termina sua execução.

2.6.2 O cliente UDP para o serviço ECHO

O arquivo UDPecho.c mostra como um cliente UDP usa o serviço ECHO. O cliente UDP ECHO do exemplo segue o mesmo algoritmo geral da versão TCP. Ele repetidamente linhas de entrada, as envia ao servidor, as de volta do servidor e as apresenta na saída padrão. Como o UDP é orientado a datagramas, o cliente trata linha de entrada com uma unidade e coloca cada uma em um datagrama. Assim, enquanto o cliente TCP a entrada de dados do servidor como um fluxo (stream) de bytes, o cliente UDP ou recebe uma linha inteira de volta do servidor ou não recebe nenhuma linha; cada chamada a read retorna uma linha inteira, a não ser que ocorra algum erro.

2.7 Resumo

Este capítulo apresentou um exemplo de biblioteca de funções que podem ser usadas para a construção de software cliente. Foram também apresentados programas em C que usam essas funções para implementar o lado cliente dos protocolos padrão DAYTIME, TIME  e ECHO.

 

Apêndice - Funções para manipular data e hora

ù         As funções de data e de hora permitem várias formas de acesso ao relógio e ao calendário do sistema. Todas estas funções requerem a inclusão do arquivo de cabeçalhos <time.h>. Este arquivo define uma macro e declara três definições de tipo. A macro é CLOCKS_PER_SEC que representa o número de segundos do valor retornado pela função clock( ).

Os tipos definidos são: clock_t,time_, e tm. A estrutura tm contém pelo menos os seguintes componentes (ela pode conter componentes adicionais):

int tm_sec;     /* segundos após o minuto          [0, 59] */

int tm_min;     /* minutos  após a hora            [0, 59] */

int tm_hour;    /* hora após a meia noite          [0, 23] */

int tm_mday;    /* dia do mês                      [1, 31] */

int tm_mon;     /* mês desde janeiro               [0, 11] */

int tm_year;    /* ano desde 1900                  [     ] */

int tm_wday;    /* dia desde domingo               [0,  6] */

int tm_yday;    /* dias desde janeiro              [0,365] */

int tm_isdst;   /* flag para indicar Horário de verão      */

O valor de tm_isdst é positivo para que o horário de verão tenha efeito,  zero se ele não tiver efeito e negativo se a informação não tiver disponível.

A função clock( )

 

#include <time.h>

clock_t clock(void);

 

A função clock() retorna o montante de tempo que o processador foi usado pelo programa. Para obter o valor em segundos, divida o valor retornado pela macro CLOCKS_PER_SEC. Se o tempo de processamento não estiver disponível a função clock() retorna o valor -1 redefinido em um tipo clock_t.

A função time( )

 

#include <time.h>

time_t time(time_t *timer);

 

A função time() retorna a melhor implementação de horário de calendário. A codificação do valor não é especificada. Se timer não é um ponteiro para null, o horário de calendário é também associado ao objeto apontado por ele.  Se este horário não estiver disponível a função time() retorna o valor -1.

A função mktime( )

 

#include <time.h>

time_t mktime(struct tm *timeptr);

 

A função mktime() converte a data armazenada em uma estrutura tm em um horário de calendário, da mesma forma que função time(). O valor de tm_wday e de tm_yday são ignorados. Os outros campos não são restritos aos campos descritos anteriormente para a estrutura tm. A função também atribui valores apropriados aos campos da estrutura apontada por timeptr. Ou seja, se os valores originais estiverem fora do intervalo, mktime() corrige estes valores. A função mktime() também  atribui valores apropriados aos campos tm_wday e tm_yday.

Se mktime() não puder calcular um valor retornável para a hora de calendário, ela retorna o valor -1.

Exemplo de uso da função mktime() para especificar um a execução de um loop por uma quantidade de minutos especificada.

 

#include <time.h>

void do_for_x_minutes(int x minutes)

{

struct tm when;

time_t now, deadline;

 

time(now);

when =* localtime(now);

when.tm_min += x_minutes;

deadline = mktime(when);

 

/* Do foo() for x_minutes */

 

while (difftime(time(0), deadline) > 0)

      foo();

}

 

Observe que a função mktime() irá funcionar mesmo se a expressão whe.tm_min += x_minutes for maior que 59.

A função asctime( )

 

#include <time.h>

char * asctime(const struct tm *timeptr);

 

A função asctime() converte o tempo representado por uma estrutura apontada pelo por timeptr em uma string na seguinte forma:

Sun Sep 16 01:03:52 1998\n\0

asctime() retorna um ponteiro para a string gerada. Chamadas subsequentes a asctime() ou ctime() devem sobrescrever a string.

A função ctime( )

 

#include <time.h>

char * ctime(const time_t *timer);

 

A função ctime() converte o horário de calendário apontada por timer do sistema local para um string de caracteres. Isto é equivalente à:

asctime( localtime (timer) )

A função difftime()

 

#include <time.h>

double difftime( time_t time1, time_t time0);

 

A função difftime() retorna a diferença ( time1 - time0 ), expressa em segundos.

A função gmtime()

 

#include <time.h>

struct tm *gmtime(const time_t *timer);

 

A função gmtime() converte um horário de calendário apontado por timer para uma hora expressa pelo Tempo Médio de Greenwich (GMT). A função gmtime() retorna um ponteiro para a estrutura contendo os componentes da hora. Se GMT não estiver disponível, gmtime() retorna um ponteiro para null.

A função localtime()

 

#include <time.h>

struct tm * localtime(const time_t *timer);

 

A função localtime() converte um horário de calendário apontado por timer em na hora local. A função localtime() retorna um ponteiro para uma estrutura contendo os componentes da hora.

A função strftime()

 

#include <time.h>

size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr);

 

A função strftime() permite construir uma string contendo informações de uma estrutura apontada por timeptr. O formato de strftime() é similar ao printf(), onde o primeiro argumento é um formato de string que pode conter um texto ou outro especificadores. Entretanto, o formato dos especificadores são trocados por dados particulares da estrutura timeptr. Não mais que max_size caracteres podem ser trocados pelos strings apontados por s, caso contrário o número de caracteres escritos é o número apresentado por s (sem incluir o caracter de terminação null). Se o conteúdo de s for indeterminado strftime() retorna zero. A tabela seguinte apresenta os especificadores de formato que podem ser usados.

Formato do especificador

Significado

%a

Nome do dia da semana abreviado

%A

Nome completo do dia da semana

%b

Nome do mês abreviado

%B

Nome completo do mês

%c

Representação apropriada da data e da hora

%d

O dia do mês como um número decimal (01 a 31)

%H

A hora (em um relógio de 24 horas) como um número decimal (0 a 23).

%I

A hora (em um relógio de 12 horas) como um número decimal (01 a 12)

%j

O dia do ano como um decimal (001 a 368)

%m

O mês como um número decimal

%M

Os minutos como um número decimal (00 a 59)

%p

Se o horário é AM ou PM (ou o equivalente na linguagem local)

%S

Os segundos como um número decimal (00-59)

%U

O número de semanas do ano (Domingo é o primeiro dia da semana) como um decimal (00-52)

%w

O dia da semana como um número decimal (0 a 6). Domingo é 0.

%W

O número de semanas do ano (onde Segunda-feira é o primeiro dia da semana) como um número decimal (00-52)

%x

Uma representação apropriada para data

%X

Uma representação apropriada para a hora

%y

O ano (os dois últimos dígitos) como um número decimal (00-99)

%Y

O ano (todos os quatro dígitos) como um número decimal

%Z

A zona de tempo, ou nenhum caracter se não existir zona de tempo

%%

 

 

Bibliografia:     

COMER, D. E., STEVENS, D. L., Internetworking With TCP/IP Volume III: Client-Server Programming and Applications, Linux/POSIX Socket Version, Prentice-Hall International 2001, Capítulo 7.

STEVENS,W.R.; "UNIX NETWORK PROGRAMMING - Networking APIs: Sockets and XTI" - Volume 1 - Second Edition - Prentice Hall - 1998

 

Obs. Texto preparado pela Profª Magda Patrícia Caldeira Arantes e revisado por Juan Manuel Adán Coello.