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