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 lê
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 lê
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 lê 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
lê uma linha de entrada,
envia a linha através
da conexão TCP para o servidor
de ECHO, lê 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 lê
linhas de entrada, as
envia ao servidor, as lê
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 lê 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