Aplicação sobre Sockets

Transcrição

Aplicação sobre Sockets
!
"
#
$
"
%
&
$
'
(
)
&
*
&
+
"
#
,
"
)
-
.
$
#
'
$
/
.
&
,
.
"
Sistemas de Telecomunicações
2003/2004
1º - Trabalho Prático:
Aplicação sobre Sockets
Licenciatura de Engenharia Electrotécnica e de Computadores
Paulo da Fonseca Pinto
2
0
1
2
3
4
5
6
7
8
9
:
Familiarização com o uso de sockets para comunicação entre máquinas e
com o funcionamento da interface de programação sockets do DELPHI. O
trabalho consiste no desenvolvimento de um cliente e de um servidor que conjuntamente
definem um serviço de prestação de horas e de transferência de ficheiros.
Sugestões: Em certas partes do enunciado aparece algum texto formatado de
um modo diferente que começa com a palavra “Sugestões”. Não é obrigatório
seguir o que lá está escrito (a não ser numa delas), mas pode ser importante para os
alunos ou grupos onde ainda não haja um à-vontade muito grande com
programação, estruturas de dados e algoritmia.
;
<
=
>
?
@
A
B
C
B
A
D
E
F
@
>
Pretende-se desenvolver um serviço de prestação de horas e de transferência de ficheiros.
Tanto o cliente como o servidor vão ser processos únicos em que internamente se faz a distinção
com módulos para os dois objectivos (horas e ficheiros). Assim, para o servidor vão existir os
seguintes módulos:
•
uma "central de acessos”,
•
um “servidor de horas”,
•
um “servidor de ficheiros”.
Os módulos do cliente são os seguintes:
•
pedido de contacto inicial,
•
tratamento das horas,
•
tratamento das transferências de ficheiros.
O modo de funcionamento em linhas gerais é o seguinte: o cliente mostra o seu interesse em
usar um dos servidores disponíveis contactando a central de acessos. A central de acessos
contacta o cliente para saber qual o serviço exacto que este pretende. O cliente responde e
depois, volta a contactar o cliente a dar-lhe o serviço (as horas, ou o servidor de transferência de
ficheiros).
O processo servidor está sempre a correr no sistema (se achar útil pode ter um “Memo” para
escrever mensagens de ajuda ao desenvolvimento). Quando o processo cliente arranca, o
utilizador parametriza o endereço conhecido da central de servidores, escolhe o serviço que
pretende e dá os parâmetros necessários (no caso das horas não há nenhum, e no caso do ficheiro
é preciso dois nomes – leitura e escrita). A sequência de mensagens trocadas está mostrada na
figura da página seguinte e consiste em:
(1) – O cliente envia uma mensagem à central de acessos com a indicação do endereço de
socket para o qual o servidor irá perguntar que serviço pretende;
(2) – Escolha de serviço
(2a) – O servidor envia a pergunta ao cliente, no socket por este indicado, para saber
que serviço quer;
(2b) – O cliente responde com a indicação do serviço;
(3) – No caso de serem as horas, o servidor indica as horas ao cliente
(4) – No caso de ser transferência de ficheiros
(4a) – O servidor envia a informação que pode atender o cliente;
(4b) – O cliente envia uma mensagem com o nome do ficheiro a receber;
(4c-4n-1) – O servidor envia o tamanho do ficheiro, seguido dos octetos do ficheiro;
(4n) – O servidor desliga a ligação.
3
A comunicação com a "central de acessos" deve ser feita através de sockets "datagram". A
comunicação da “central de acessos” com o cliente para saber o serviço deve também ser feita
através de sockets "datagram" para um socket diferente do cliente. A comunicação do "servidor
de horas" para indicar as horas deve ser feita através de sockets "datagram" para o mesmo socket
onde se perguntou o serviço. A comunicação com o "servidor de ficheiros" deve ser feita através
de sockets orientados à conexão.
Os passos 4c a 4n-1 correspondem ao envio do comprimento e dos dados do ficheiro. Não se
sabe ao certo quantos pacotes serão pois o socket orientado à conexão é um feixe de octetos e
não um feixe de pacotes (pretende-se, aliás, que o servidor escreva cerca de 20 octetos de cada
vez e que o cliente leia, por exemplo, 25 octetos de cada vez).
Central de
Acesso
Cliente
Socket
UDP1
Socket
TCP servidor
1
Socket
UDP0
Socket
UDP3
2a
2b
3
4b
Servidor de
Horas
Servidor de
Ficheiros
Socket
UDP4
Socket
TCP cliente
4a
…
4c
4n
Fig 1 – Sequência de mensagens do trabalho
G
H
I
H
J
K
L
M
N
O
M
K
O
K
P
N
J
Q
L
M
R
N
S
T
Q
U
O
Q
V
V
K
V
O acesso à "central de acessos" utiliza sockets datagram e obedece ao protocolo mostrado
na figura (em que estão representados os dados):
ola
Mensagem ola: {sequência contígua de }
Cod_oper
: Word;
{ deve ser sempre 1 }
Porto
: Word;
{ do socket UDP1
}
Esta mensagem tem mais do que um parâmetro. Os parâmetros devem estar seguidos uns
aos outros. Não deve usar Records para o envio dos parâmetros pois o compilador procede a
alinhamentos dos campos para endereço múltiplos de quatro octetos, estragando a sequência
contígua. Assim, o modo mais simples é escrever (ler) parâmetro a parâmetro para o (do) buffer.
Esta mensagem enviada através do socket UDP0 e serve para dar a conhecer ao servidor a
identificação do socket UDP1.
O socket da central de acessos é o único conhecido à priori (o endereço IP é o da máquina
respectiva e o que realmente é conhecido é o porto). Todos os outros sockets podem ter portos
livres pois são fornecidos pelos vários componentes. O porto do socket conhecido tem um valor
diferente consoante o grupo. A fórmula a aplicar é a seguinte:
PServ = (nº Grupo) * 10 + 20000
4
Numa primeira fase pode admitir que só existe um cliente activo em cada instante (se isso
lhe parecer que simplifica a programação). Caso, no fim do trabalho, tenha algum tempo, pode
pensar em suportar vários clientes em paralelo. Alguma complicação pode existir no socket
UDP3 que poderá ter “olas” de clientes misturados com informação de serviços de outros
clientes.
Sugestões: O cliente tem de saber logo que arranca, qual o endereço e portos
do socket datagrama da escolha do serviço e das horas e do socket stream dos
ficheiros, para poder responder logo ao servidor.
Sugestões: Um problema com este sistema é que o servidor tem de definir os
objectos TCPClient antes de arrancar. Isso vai limitar o número de clientes que
pode atender. Um modo de contornar este problema é o servidor definir 10 objectos
TCPClient, dando um máximo de atendimento simultâneo de 10 clientes. Para
complicar ainda mais as coisas, a mensagem ola não especifica se um cliente veio
para as horas, ou para transferir ficheiros Quando aparece o 11º (isto é, o servidor
está a servir no momento 10 clientes de transferência de ficheiros), pelo sim pelo
não, o servidor deve não aceitar mais pedidos e responder logo ao cliente a sua
indisponibilidade (ver próximo ponto).
W
X
W
X
Y
Z
[
\
]
^
_
`
a
Z
a
b
c
d
e
\
Z
A escolha de serviços utiliza os sockets datagram UDP3 e UDP1. Obedece ao protocolo
mostrado na figura (em que estão representados os dados):
Mensagem que_servico:{sequência contígua de}
Cod_oper
: Word;
{ deve ser sempre 2 }
Porto
: Word;
{ do socket UDP3
}
aceito
: char;
{ resposta ao cliente
}
{ ‘s’
- o cliente pode prosseguir }
{ ‘n’
- o cliente deve desistir }
Id_cliente : Word;
{ identificação do cliente }
que_servico
Mensagem servico: {sequência contígua de
Cod_oper
: Word;
{ deve ser sempre
Id_cliente : Word;
{ identificação do
Tipo_serv
: Word; ;
{ código serviço
{ 4
- serviço de horas }
{ 5
- serviço de ficheiros }
Porto_serv : Word; { porto do socket do
servico
}
3 }
cliente }
}
serviço }
Quando o servidor pergunta ao cliente que serviço ele quer, indica-lhe uma identificação
para que o cliente a use na resposta. Isso vai permitir distinguir os clientes no caso de haver
vários clientes ao mesmo tempo. No caso de o cliente escolher um serviço não existente, o
servidor deve esquecer o pedido completamente (o cliente terá de iniciar o contacto outra vez
com um “ola”). Na altura da pergunta, o servidor indica também se pode atender o pedido do
cliente, ou não, através da variável aceito. No caso de não aceitar, o Id_cliente vai a zero
e o cliente não deve enviar mais nada.
O cliente envia o porto do socket (datagrama ou stream) no campo porto_serv.
Dependendo do serviço, o servidor sabe que tipo de socket se trata (UDP1 ou TCP).
f
g
h
g
i
j
k
l
m
n
o
p
j
q
o
k
r
s
O serviço de horas obedece ao protocolo:
5
são_horas : {sequência contígua de }
Cod_oper : Word;
{ deve ser sempre 4 }
horas
: TdateTime;
{ data e hora }
sao_horas
O servidor envia uma mensagem com as horas para o socket datagrama UDP1.
t
u
v
u
w
x
y
z
{
|
}
~
x

y
€

‚
ƒ
x
y
„

…
{
€
~
x
†
{
…
‡
x
{
y
}
‚
O serviço de transferência de ficheiros deve utilizar sockets orientados à conexão e obedecer
ao protocolo:
que_ficheiro : {sequência contígua de }
nome_len : Word; { nº bytes }
nome
: String;{ nome do servidor }
que_ficheiro
file_req
file
file_req : { sequência contígua de }
file_name_len : Word; { nº bytes }
file_name
: String;{nome do ficheiro}
file : {sequência contígua de }
len
: LongInt;
{ -1
- ficheiro inexistente }
{ 0
- ficheiro vazio }
{ senão – tamanho ficheiro }
contents : String {sequência de caracteres
com conteúdo do ficheiro }
O servidor liga-se ao socket do cliente e envia a sua identificação para começar o serviço
(cada grupo pode inventar a identificação que quiser). O cliente responde com o nome do
ficheiro que quer. O servidor começa então a enviar o ficheiro. Antes do ficheiro vai uma
indicação a notificar qualquer erro na abertura do ficheiro, ou o comprimento de bytes que o
ficheiro tem, se tudo estiver bem. No final da transmissão o servidor termina a ligação e ambos
fecham os ficheiros. Para tornar as coisas mais reais e interessantes, o servidor envia, em cada
ciclo de envio, apenas 20 octetos do ficheiro.
Caso decida tentar suportar vários clientes em paralelo, lembre-se que necessita de ter um
número de variáveis (objectos) igual ao número de ligações, o que obriga a criar e destruir
variáveis deste tipo. Deixe a realização desta funcionalidade para o fim, caso tenha tempo.
Sugestão obrigatória: Aprenda a programar por eventos como deve de ser.
Assim, não vai ser permitido que a rotina de envio do ficheiro nunca saia, devido a um
ciclo contínuo de leitura de ficheiro e escrita no socket. Uma programação deste
género “prende” o sistema nessa rotina podendo impedir que outras respondam a
eventos. A rotina de leitura e envio deve ser chamada por eventos: relógio,
disponibilidade de espaço no socket depois de ter estado cheio devido a uma escrita
intensa, etc. (no segundo caso é muito importante ter em atenção os valores de retorno
das funções de sistema). Cada vez que é chamada envia mais um pacote e sai (se for o
último fica a saber que nunca mais é chamada). No cliente deve proceder-se de modo
semelhante: cada vez que existe informação para ler no socket o cliente deve lê-la,
eventualmente mais do que uma vez, mas quando numa dada altura se esgotam os
dados do socket a rotina deve acabar, libertando o sistema. Quando voltar a haver
dados para ler, a rotina é novamente chamada.
6
ˆ
‰
Š
‰
‹
Œ

Ž


‘
’
“
”

Ž
•
–
—
•
A aplicação cliente deve obrigatoriamente ter o aspecto da figura em baixo. Os campos já
devem estar preenchidos com valores por omissão para tornar os testes mais rápidos (o IP deve
ser o da máquina onde se está). Para a escolha dos serviços deve usar botões rádio (apenas um
pode estar ligado de cada vez). O menu Main contém três opções: Clear Memo para limpar a
janela de teste; Reset para colocar o programa com os seus valores de início; e Sair para
terminar o programa. No servidor também deve existir um menu idêntico. Os outros campos
são autoexplicativos pelo que não se descrevem as suas funcionalidades aqui. Qualquer dúvida
será esclarecida pelo corpo docente. Depois de terminar a operação, o cliente deve escrever no
espaço de resultados um relatório: no caso das horas deve escrever as horas num formato legível;
no caso dos ficheiros deve escrever o servidor utilizado, o tamanho ficheiro e o número de bytes
recebidos. A outra janela de mensagens deve servir para os alunos escreverem mensagens que os
ajudem no desenvolvimento do programa.
O cliente deve ficar pronto para executar outra operação mal a primeira esteja terminada.
˜
™
š
›
œ

ž
œ
›
Ÿ
¡
œ
¢
›
£
¤
›
œ
Não se devem tratar situações muito excepcionais. Por exemplo, se o cliente, depois de ter
pedido um serviço for abaixo, o servidor pode ficar bloqueado para sempre. Isso não tem
importância para este trabalho. Se algum pacote UDP se perder pode também não se efectuar o
serviço. Também não tem importância. Se se receberem códigos de operação (Cod_oper)
incorrectos, deve-se considerar que não se recebeu qualquer pacote. Etc.
¥
¦
§
¨
©
ª
«
¬
¦
§
«
­
©
®
¦
§
Cada grupo deve ter em consideração o seguinte:
•
Não perca tempo com a estética de entrada e saída de dados
7
•
Programe de acordo com os princípios gerais de uma boa codificação (utilização de
indentação, apresentação de comentários, uso de variáveis com nomes conformes às
suas funções...) e
Proceda de modo a que o trabalho a fazer fique equitativamente distribuído pelos
membros do grupo.
•
¯
°
±
°
²
³
´
µ
´
¶
·
A parte laboratorial é composta por um trabalho de aprendizagem (sem avaliação) e dois
trabalhos de avaliação. A duração prevista para cada um deles é de 3 semanas para o trabalho de
aprendizagem, de 4 semanas para o primeiro trabalho e de 6 semanas para o segundo trabalho de
avaliação. Tem sido hábito que o primeiro trabalho exige mais dos alunos por ser o
primeiro embate a sério com os sockets. Tenha isso em consideração para não se desleixar
nos primeiros tempos de execução do trabalho.
As datas limites para entregas dos trabalhos de avaliação são 10 de Novembro e 22 de
Dezembro, às 14h00. Não são permitidas entregas posteriores, sendo a nota desse trabalho
de zero valores. Tenha especial atenção à data de 22 de Dezembro por causa da pressão
dos testes e outros trabalhos. Cumpra as várias etapas propostas pelo corpo docente (em
baixo).
O quadro seguinte mostra as datas de entrega dos trabalhos (L1 e L2).
Setembro
14
0
21
1
Outubro
28
2
5
3
12
4
19
5
15
16
17
18
19
20
22
23
24
25
26
27
29
30
1
2
3
4
6
7
8
9
10
11
13
14
15
16
17
18
20
21
22
23
24
25
28
29
30
31
1
4
5
6
7
8
11
12
13
14
15
18
19
20
21
22
25
26
27
28
29
Dezembro
26
27
6
Novembro
2
3
7
9
8
L1
16
17
9
23
24
10
11
12
13
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
L2
23
24
25
26
27
Para permitir um desenvolvimento suave do trabalho os alunos devem cumprir as seguintes
etapas:
1ª aula – Fazer as partes datagrama do cliente e do servidor (usando os executáveis
fornecidos pelo corpo docente para fazer uma das partes de cada vez).
2ª aula – Fazer a parte orientada à ligação (serviço de ficheiros) do servidor (idem).
3ª aula – Fazer a parte orientada à ligação do cliente (idem).
4ª aula – Melhorar o trabalho adicionando a possibilidade de haver mais do que um cliente
simultaneamente.
8
Apêndice A
Tipos de Ficheiros e Tratamento de Caracteres
Introdução
Nos primeiros tempos, anos, um computador servia principalmente para fazer cálculos! Quase
nunca para tratar textos. É assim natural que muitas linguagens de programação nunca tivessem
dado importância ao tipo texto. Este tipo sempre apareceu como sendo um “acrescento” aos
verdadeiros tipos como os inteiros, os reais, etc. Isto torna o seu manuseamento em programas
uma tarefa com um grau de dificuldade elevado. Com o passar dos anos, o texto tem tido uma
importância cada vez maior e é um assunto muito importante. Por estas razões, os trabalhos de
Sistemas de Telecomunicações têm sempre alguma coisa relacionada com texto para iniciar os
alunos neste tipo de dificuldades.
Este documento é uma breve introdução aos tipos de ficheiros existentes no Delphi e ao
manuseamento de caracteres. Para ficar curto, apenas contém uma introdução aos aspectos mais
relevantes. O aprofundar dos temas deve ser procurado no manual de ajuda do Delphi.
Tipos de Ficheiros
Existem três tipos de ficheiros no Delphi: Estilo antigo de ficheiros de Pascal (Old style Pascal
Files); “Windows file handles”; e “file stream objects”.
1. Old style Pascal files – São os tipos usados nas antigas variáveis de ficheiros, normalmente
com o formato "F: Text: ou "F: File". Como é mais comum a outros
sistemas para além do Delphi e do Windows, é aconselhável aos alunos
usarem este tipo. Existem três classes destes ficheiros: typed, text, e untyped
e um grande número de rotinas do Delphi usam-nas: por exemplo AssignPrn
e writeln.
Ficheiros typed (em que Text ou TextFile são casos particulares para texto): Um
ficheiro é um conjunto ordenado de elementos do mesmo tipo. As
rotinas standard de I/O usam os tipos predefinidos TextFile ou
Text, os quais representam um ficheiro contendo caracteres
organizados em linhas. Para declarar um ficheiro typed deve-se
usar:
type fileTypeName = file of type
em que fileTypeName é um identificador válido qualquer, e type
é um tipo de tamanho fixo. Tipos Pointer — quer implícitos ou
explícitos — não são permitidos. Assim, um ficheiro não pode ter
vectores dinâmicos, long strings, classes, objectos, pointers,
variants, outros ficheiros, ou tipos estruturados que contenham
quaisquer destes tipos.
Por exemplo,
type
9
PhoneEntry = record
FirstName, LastName: string[20];
PhoneNumber: string[15];
Listed: Boolean;
end;
PhoneList = file of PhoneEntry;
Declara um ficheiro typed para guardar nomes e número de
telefone. Pode-se também usar um ficheiro de … construindo
directamente o tipo na declaração da variável. Por exemplo,
var List1: file of PhoneEntry;
A palavra file por ela mesmo, indica um ficheiro untyped:
var DataFile: file;
Para o caso particular de ficheiros do tipo Text ou TextFile,
apresenta-se o seguinte exemplo de utilização onde se copia o
conteúdo do ficheiro F1 para o ficheiro F2:
var
F1, F2: TextFile;
Ch: Char;
begin
if OpenDialog1.Execute then begin
AssignFile(F1, OpenDialog1.Filename);
Reset(F1);
if SaveDialog1.Execute then begin
AssignFile(F2, SaveDialog1.Filename);
Rewrite(F2);
while not Eof(F1) do
begin
Read(F1, Ch);
Write(F2, Ch);
end;
CloseFile(F2);
end;
CloseFile(F1);
end;
end;
Ficheiros untyped: são usados para acesso directo aos ficheiros de disco malgrado o
tipo ou a estrutura do ficheiro. Um ficheiro untyped é declarado
com a palavra file e nada mais. Por exemplo,
var DataFile: file;
Para estes ficheiros os procedimentos Reset e Rewrite permitem
um parâmetro extra para especificar o comprimento do record
10
usado na transferência. O valor por omissão é de 128 bytes. Um
comprimento de 1 byte é o único valor que reflecte correctamente
o comprimento exacto de qualquer ficheiro (Não existem records
parciais quando o comprimento é 1).
Excepto para Read e Write, todos os procedimentos para
ficheiros typed são permitidos em ficheiros untyped. Em vez de
Read e Write, utilizam-se dois procedimentos chamados de
BlockRead e BlockWrite.
REPARE QUE: Dependendo do tipo de ficheiros que se usa, assim os procedimentos
para escrever e ler (por exemplo) podem ser diferentes.
2. Windows file handles – O objecto “file handles” encapsula este tipo de ficheiros do
Windows. As funções de sistema do Delphi são funções que acedem
directamente à API do Windows. Por exemplo, as chamadas a FileRead
chama a função de Windows ReadFile. O uso destas rotinas é trivial (ver
Manual).
3. File streams – São instâncias da classe VCL TFileStream usada para aceder à informação de
ficheiros. São portáveis entre sistemas e formam uma aproximação de altonível a ficheiros. TFileStream tem uma propriedade chamada de Handle que
dá acesso ao Windows file handle.
Tratamento de Caracteres
Existem muitas hipóteses de trabalhar com texto, ou caracteres, no Delphi. Vão-se descrever
neste ponto algumas das mais usadas.
Os caracteres podem ser trabalhados como um vector (array) de char. Neste caso tem de se
saber qual a dimensão máxima que alguma vez se vai usar para dimensionar a memória. A partir
daí pode-se aceder a qualquer posição, usando o modo normal em vectores (isto é, parêntesis
rectos. Por exemplo a posição dois é referenciada com o valor [2], se o vector foi definido como
começando em 1).
Às vezes não sabemos, nem queremos saber qual a dimensão máxima, e deixamos isso ao
cuidado do sistema. Para esses casos existe um tipo chamado String, que é definido como uma
sequência de caracteres. Na realidade existem três tipos: ShortString; AnsiString e WideString. O
primeiro tem um limite de 255 caracteres, e os outros dois muito mais. Dependendo de uma
variável do compilador, a palavra String corresponde a ShortString ou a AnsiString.
O tipo String tem algumas coisas boas. Primeiro quando se declara, não existe memória para
ele. Se tentarmos aceder ao seu valor dá um erro de violação, o que simplifica logo o teste do
programa – ele rebenta! Então como é que conseguimos memória para ele? Das duas uma: ou o
obrigamos a ter um comprimento inicial com a função SetLength, ou simplesmente copiamos
um texto de outra variável para ele. Felizmente, se usarmos o primeiro caso, o sistema inicializa
o String automaticamente e os esquecidos não trabalharão com lixo...
Fazer operações com String são facílimas. Podemos usar o ‘+’ para concatenar texto, etc.
11
Mesmo assim ainda há programadores que sentem que querem ter mais ajuda do sistema. Por
exemplo, não ter de controlar o tamanho exacto num dado momento, retirar partes do texto
sucessivamente a partir do início e não ter de controlar onde se está na sequência, etc.
Para esses programadores existe uma classe abstracta (que define as operações da classe mas
outras classes concretas terão as implementações) chamada de TStream. De um ponto de vista
abstracto esta classe serve para se poder ler e escrever para vários tipos de meios de
armazenamento como ficheiros, memória, etc. No nosso caso particular estamos interessados em
memória e a classe concreta chama-se TMemoryStream. Mas falando ainda da classe abstracta,
ela vai ter, como todas as classes, propriedades e métodos. As propriedades são Position e Size
(é óbvio o seu significado). Entre os métodos existem Read, Write, Create, Free, Seek ou SetSize
(ver Manual).
A classe TMemoryStream guarda os seus dados em memória dinâmica. É boa para objectos
intermediários onde se necessite de ler e escrever partes. Existem também funções de sistema
que aceitam objectos destes (Ver documento de Sistemas de Telecomunicações intitulado
“Introdução aos Sockets no Ambiente Delphi4” para ver exemplos de funções sobre sockets
que usam o TStream). A nível de acesso a ficheiros também é diferente usar-se um String ou
um TMemoryStream, evidentemente.
Como qualquer objecto, tem de se criar com a operação Create, e deve-se eliminar, quando não
for mais necessário, com a operação Free. De outro modo fica um bocado de memória por
libertar.
Como qualquer objecto, também, os seus dados são um problema interno dele e não se deve
tentar aceder directamente à memória (com os parêntesis rectos [2], por exemplo) à procura do
que quer que seja. Deve-se sempre usar os métodos do objecto.
No trabalho 0 de Sistemas de Telecomunicações estão muitos exemplos do uso de
TMemoryStream.
Agora que já teve uma introdução a estes temas, só tem mesmo de escolher os tipos com que
quer trabalhar, e divertir-se a fazer os trabalhos!...
12