• Nenhum resultado encontrado

Projeto e Desenvolvimento de Aplicações Cliente / Servidoras Para a INTERNET

N/A
N/A
Protected

Academic year: 2021

Share "Projeto e Desenvolvimento de Aplicações Cliente / Servidoras Para a INTERNET"

Copied!
83
0
0

Texto

(1)

Projeto e Desenvolvimento de

Aplicações Cliente / Servidoras

Para a INTERNET

João Carlos Gluz

(2)

Sumário

SUMÁRIO ... 2

CAPÍTULO I - O MODELO CLIENTE / SERVIDOR... 4

1.1. MOTIVAÇÃO... 4 1.2. TERMINOLOGIA... 4 1.3. COMPLEXIDADE RELATIVA... 5 1.4. PADRONIZAÇÃO... 6 1.5. PROTOCOLOS DE COMUNICAÇÃO... 6 1.6. FORMATOS DE INFORMAÇÕES... 6 1.7. PARAMETRIZAÇÃO DE CLIENTES... 7 1.8. USO DE CONEXÕES... 7 1.9. INFORMAÇÕES DE ESTADO... 8

CAPÍTULO II - INTERFACE DE PROGRAMAÇÃO PARA APLICAÇÕES DE REDE ... 9

2.1. CONCORRÊNCIA... 9

2.2. PROCESSOS E PROGRAMAS... 10

2.3. COMUNICAÇÃO ENTRE PROCESSOS... 11

2.4. SINCRONISMO... 11

2.5. INTERFACES DE PROGRAMAÇÃO... 12

2.6. INTRODUÇÃO A INTERFACE SOCKET... 13

CAPÍTULO III - INTERFACE SOCKETS ... 16

3.1. CRIANDO E ELIMINANDO SOCKETS... 16

3.2. ESPECIFICANDO ENDEREÇOS... 17

3.3. ESTABELECENDO CONEXÕES... 18

3.4. FORÇANDO A AMARRAÇÃO DE ENDEREÇOS... 19

3.5. RECEBENDO CONEXÕES... 20

3.6. TRANSFERÊNCIA DE DADOS... 21

CAPÍTULO IV - ARQUITETURA GENÉRICA DE CLIENTES ... 23

4.1. INTRODUÇÃO... 23

4.2. CARACTERÍSTICAS DE UMA APLICAÇÃO CLIENTE... 23

4.3. ALGORITMO BÁSICO DE UM CLIENTE... 23

4.4. LOCALIZAÇÃO DO SERVIDOR... 24

4.4. IDENTIFICAÇÃO DO SERVIÇO... 26

4.5. PREENCHIMENTO DA ESTRUTURA DE ENDEREÇOS... 28

4.6. CRIAÇÃO DO SOCKET... 28

4.7. ESTABELECIMENTO DA CONEXÃO COM O SERVIDOR... 29

4.8. TRANSFERÊNCIA DOS DADOS... 30

(3)

4.11. EXEMPLO DE UM CLIENTE HTTP... 35

CAPÍTULO V - ARQUITETURA DE APLICAÇÕES SERVIDORAS... 39

5.1. ALGORITMO BÁSICO... 39

5.2. CONCORRÊNCIA VERSUS ITERATIVIDADE NO TRATAMENTO DAS REQUISIÇÕES... 40

5.3. O USO DE CONEXÕES NO TRATAMENTO DAS REQUISIÇÕES... 40

5.4. QUANDO USAR CADA TIPO DE SERVIDOR... 42

5.5. EXEMPLO DE UM SERVIDOR HTTP ... 42

CAPÍTULO VI – O PROTOCOLO HTTP... 57

6.1. OPERAÇÃO GERAL DO PROTOCOLO... 57

6.2. NOTAÇÃO USADA PARA APRESENTAR O FORMATO DAS MENSAGENS... 58

6.2.1. Definição de um Formato... 58

6.2.2. Opções de Formatos... 58

6.2.3. Caracteres / Símbolos Especiais ... 60

6.2.4. Elementos Básicos... 60

6.3. FORMATO BÁSICO DAS MENSAGENS... 60

6.4. MENSAGENS DE REQUISIÇÃO... 63

6.4.1. Métodos das Requisições... 63

6.4.2. Identificador de Recurso da Requisição... 64

6.4.3. Cabeçalhos de Requisição... 65

6.5. MENSAGENS DE RESPOSTA... 66

6.5.1. Linha de Estado da Resposta ... 66

6.5.2. Códigos de Estados e sua Descrições ... 66

6.5.3. Cabeçalhos de Resposta... 68

6.6. ENTIDADE... 68

6.7. DESCRIÇÃO DOS MÉTODOS... 69

6.7.1. GET ... 69

6.7.2. HEAD... 69

6.7.3. POST ... 69

6.8. CÓDIGOS DE ESTADO... 70

6.9. CAMPOS DOS CABEÇALHOS... 72

6.9.1. Cabeçalho Geral ... 72

6.9.2. Cabeçalho das Requisições ... 73

6.9.3. Cabeçalho das Respostas ... 74

6.9.4. Cabeçalho das Entidades ... 75

6.10. AUTENTICAÇÃO DE ACESSO... 77

CAPÍTULO VII - REQUISIÇÕES HTTP COM FORMUIÁRIOS CODIFICADOS ... 79

7.1. MÉTODOS APLICÁVEIS... 79

7.2. ALGORITMO DE CODIFICAÇÃO... 79

7.3. EXEMPLO... 80

7.4. SUGESTÕES DE TRABALHOS... 80

7.4.1. Implementação do Tratamento de Formulários e CGI no WebServ ... 80

(4)

Capítulo I - O Modelo Cliente / Servidor

A pilha de protocolos TCP/IP provê um serviço genérico fim-a-fim de comunicação entre pares

(peer-to-peer)de aplicações ou processos, mas não define como esta comunicação será organizada entre os

pares de aplicações.

O modelo ou paradigma dominante (quase que único) para organizar a comunicação entre as aplicações é o modelo denominado:

CLIENTE / SERVIDOR

1.1. Motivação

A principal motivação por trás do uso do modelo cliente/servidor tem a ver com o problema de sincronização da comunicação entre as aplicações tentando interagir, principalmente se se levar em conta a velocidade com que são executadas as aplicações e também são atendidas as solicitações de comunicação pela rede.

O modelo divide as aplicações em duas grandes classes ou categorias:

Clientes: sempre iniciam o processo de comunicação

Servidores: sempre esperam que uma (ou mais) aplicação inicie a comunicação

Além disso este modelo resolve uma questão importante relacionada ao funcionamento do TCP/IP: A pilha TCP/IP não fornece nenhum tipo de mecanismo para ativação (indicação na terminologia OSI/ISO) de aplicações quando da chegada de alguma mensagem particular, portanto deve sempre haver uma aplicação já executando esperando pelas mensagens que chegam da rede. Pela própria maneira como se organizam as aplicações no modelo cliente/servidor este problema fica naturalmente resolvido.

1.2. Terminologia

Clientes e servidores são aplicações, programas ou processos do sistema operacional, isto é, são

softwares. Ocasionalmente, entretanto, a terminologia é aplicada, em particular o termo servidor,

diretamente para as máquinas que estão executando alguma aplicação servidora. Para não haver confusão estas máquinas serão denominadas no texto de máquinas servidoras e não de servidores. O termo servidor será reservado apenas para as aplicações.

(5)

Os clientes serão sempre as aplicações que começam o processo de comunicação, enviando usualmente uma ou mais requisições para um servidor remoto.

Por sua vez os servidores serão os programas que esperarão por estas requisições, recebidas (tipicamente) através da rede de comunição que os liga com os clientes remotos.

Implicitamente se assumirá que as aplicações cliente/servidoras serão remotas umas das outras, embora isto não seja mandatório (por exemplo em caso de testes pode-se executar tanto o cliente quanto o servidor na mesma máquina).

A troca destas informações entre o par cliente / servidor se dará através de um protocolo de aplicação que é apenas um protocolo de comunicação definido especificamente para tratar dos problemas e questões envolvidas na interação do cliente com o servidor. No caso da Internet a identificação do protocolo de aplicação (e do serviço correspondente) será feita através de portas “lógicas” disponibilizadas pelo protocolo TCP (ou UDP).

Os servidores irão implementar um serviços que serão acessados remotamente pelos clientes através dos protocolos de aplicação. Normalmente os protocolos de aplicação, em aplicações cliente / servidoras, são organizadas em dois tipos de mensagens:

mensagens de requisição ou solicitação de serviços, que são enviadas dos clientes aos servidores

mensagens de resposta, que são enviadas dos servidores aos clientes, em resposta as requisições. As requisições enviadas pelos clientes solicitam aos servidores que executem algum tipo de tarefa. Um servidor ao receber uma requisição de um cliente deverá executar uma ação correspondente e enviar a mensagem de resposta indicando que ação foi tomada. Por exemplo num dado instante o servidor pode realmente executar uma tarefa que atenda a solicitação feita pelo cliente, porém num outro instante de tempo, pelo fato de já estar sobrecarregado de tarefas, o servidor pode não fazer nada e apenas avisar isto ao cliente.

Uma outra questão importante relacionada aos protocolos de aplicação tem a ver com os formatos das mensagens trocadas entre o cliente e o servidor. Em geral, além de ser necessário especificar os formatos de requisições, respostas, etc., pode também ser necessário especificar o formato dos dados ou informações que serao trocados dentro destas mensagens, principalmente quando as questões de formato de informações forem realmente importantes para a aplicação. Neste caso será necessário definir formatos de representação de informações apropriados e que sejam independentes de arquiteturas de máquinas ou sistemas operacionais onde os clientes e servidores estão implementados.

1.3. Complexidade Relativa

Os servidores serão normalmente as aplicações mais complexas de se implementar neste par de aplicações por várias razões:

Servidores usualmente deverão executar com privilégios especiais dentro do sistema operacional não só porque terão que interagir intimamente com o subsistema de rede do S.O. (por ter que esperar

(6)

requisições) mas também porque muitas vezes para atender as requisições remotas terão de a necessidade de acessar informações privilegiadas do S.O. e ocasionalmente ter que executar computações laboriosas. Porém ao executar operações em modo privilegiado os servidores deverão implementar código para executar as seguintes tarefas:

Autenticação através da verificação da identidade dos usuários (clientes remotos)

Autorização ou não das requisições de acordo com o perfil do usuário

Garantir a segurança no acesso aos dados dos usuários

Garantir a privacidade das informações fornecidas pelos usuários

Proteçao aos recursos do S.O.

Além disso quando for necessário a execução de computações laboriosas ou extensas o para atender uma dada requisição será necessário utilizar recursos de processamento concorrente do S.O. complicando ainda mais a implementação dos servidores.

1.4. Padronização

As aplicações cliente/servidoras podem ainda ser classificadas em relação ao fato de serem baseadas sobre serviços ou protocolos padronizados (well-known ports definidas através de RFC/STD específica) ou serem baseadas sobre serviços não padrão de uso local a uma organização ou de propriedade de alguma corporação.

1.5. Protocolos de Comunicação

Os protocolos de comunicação entre os pares de clientes e servidores definem formalmente como será a troca de informações entre eles. Estes protocolos podem ser padronizados ou não com visto anteriormente, mas também podem ser genéricos ou específicos, no sentido de que poderão ser usados apenas para um dado serviço (protocolo mais específico) ou poderão ser usados para suportar uma ampla variedade de serviços distintos (mais genérico).

Por exemplo o protocolo TELNET pode ser usado para suportar praticamente qualquer tipo de serviço cuja interação com os usuários esteja baseada no paradigma de terminal de texto remoto.

Por outro lado um protocolo como o FTP tem um uso bem mais restrito, sendo utilizado essencialmente (como o próprio nome indica) apenas para a transferência de arquivos.

1.6. Formatos de Informações

Geralmente são tratadas de forma independente as questões envolvendo a especificação dos protocolos usados na comunicação cliente/servidor e as questões envolvendo o formato das informações trocadas entre estas aplicações. Não que estas questões sejam totalmente independentes, mas apenas que um tratamento diferenciado destes dois tipos de questões facilita a generalização do uso do protocolo, uma

(7)

vez que a falta de geralidade de uso de um protocolo está mais intimamente ligada aos formatos de informação que podem ser intercambiados dentro do protocolo do que com a as mensagens de controle usadas pelo mesmo.

Sendo assim atualmente se define um formato padrão único para o “envelope” destas mensagens e se define um formato extensível para o conteúdo destas mensagens.

Exemplos de pares de protocolo / formato: TELNET / VT-100 HTTP / HTML SMTP / MIME SNMP / MIB-II

1.7. Parametrização de Clientes

Quando uma aplicação clientes é definida, normalmente vale a pena permitir que a mesma possa ser parametrizada para uma ampla gama de aplicações. Pelo menos a especificação do endereço e porta a ser usada no servidor remoto devem poder ser parametrizados.

A combinação de um cliente flexível bastante parametrizável com protocolos e formatos de mensagens genéricos permite a criação de verdadeiras aplicações clientes multifuncionais. Por exemplo existem inúmeros tipos de serviços disponibilizados por máquinas servidoras que caem dentro do paradigma de terminal de texto interativo remoto (desde o login remoto do shell até interfaces orientadas a menus ou formulários de aplicações específicas), sendo que se pode usar o cliente TELNET para o acesso a estes diferentes serviços, desde que se possa parametrizar não só o endereço da máquina servidora, mas também a porta TCP e provavelmente o tipo de emulação de terminal requerido.

Um exemplo mais recente de aplicação cliente multifuncional é o dos clientes HTTP/HTML (NetscapeTM, IExplorerTM, etc.) que hoje servem como front-end para um sem número de aplicações e serviços (na verdade quase que substituindo o antigo paradigma de terminal de texto).

1.8. Uso de Conexões

O termo conexão dentro de redes de computadores tem uma semântica bem específica relacionado a criação e uso de um canal de transporte fim-a-fim robusto, confiável e ordenado (uma espécie de “tubo” -

pipe) para a troca de informações entre o par de aplicações. Serviços orientados a conexão são então

serviços que faze uso deste tipo de canal de comunicação robusto e confiável. Na arquitetura da Internet este é o serviço baseado no protocolo TCP.

Por outro lado as redes de comunicação geralmente também disponibilizam um tipo de serviço de transporte mais segmentado, orientado a mensagens individuais que normalmente não garante nem a confiabilidade na entrega e nem o ordenamento correto de seqüência de mensagens, apesar de ser

(8)

baseado no conceito de melhor esforço possível de entrega (best-effort delivery). Na arquitetura da Internet este serviço é fornecido através do protocolo UDP.

Um dos pontos mais importantes que deve ser decidido quando uma aplicação cliente/servidora está sendo especificada é que tipo de serviço de transporte se irá usar: com suporte a conexões ou sem suporte a conexões.

Apesar do fato do uso de conexões implicar no uso de maiores recursos de processamento e memória por parte do sistema operacional, tem-se como regra geral para implementação de aplicações cliente/servidoras que se deve sempre usar o serviço com suporte a conexões (também chamado de serviço orientado a conexão) exceto nos seguintes casos:

1. O protocolo de troca de informações entre o cliente e o servidor já foi especificado (numa RFC por exemplo) e exige o uso de UDP (trata por sua própria conta os problemas de confiabilidade e ordenação).

2. Este tipo de aplicação requer o uso de difusão (broadcast) de mensagens.

3. O custo, em termos de recursos de processamento e memória, necessário para se criar e manter as conexões é realmente inaceitável para a aplicação.

1.9. Informações de Estado

As informações mantidas por um servidor sobre como estão as interações com os seus clientes remotos é denominada de informações de estado. Entretanto a manutenção e armazenamento deste tipo de informação não são obrigatórios, ou seja, existem servidores que não precisam manter informações sobre o estado das suas diversas interações.

A necessidade ou não de um servidor de manter o estado das interações é, na verdade, uma questão de especificação de protocolos: se uma dada mensagem de protocolo somente puder ser corretamente interpretada em função de mensagens recebidas anteriormente então é praticamente impossível construir um servidor que não mantenha informações de estado.

Por outro lado o armazenamento das informações pode reduzir em muito as necessidades de comunicação entre o cliente e o servidor, por permitir que as mensagens façam referência a um contexto de comunicação prévio. Caso contrário seria necessário que cada mensagem encapsula-se todas as informações necessárias para o atendimento de uma dada requisição (operações idempotentes).

Contudo, o uso de informações de estado pode ocasionar sérios problemas se houverem problemas de comunicação entre as duas aplicações. Se mensagens forem perdidas ou duplicadas, as informações de contexto guardadas no servidor poderão se tornar inválidas sem que ele necessariamente o reconheça. Além disso se o cliente sofrer uma pane, for reinicializado e a comunicação com o servidor for restabelecida sem que o servidor tenha sido informado da pane anterior então as informações de estado armazenadas no servidor serão totalmente errôneas.

(9)

Capítulo II - Interface de Programação para

Aplicações de Rede

2.1. Concorrência

Genericamente o termo concorrência se refere a execução de um conjunto de computações que parecem estar sendo executadas de forma simultânea. Estas computações podem estar realmente sendo executadas simultaneamente (ou seja sendo executadas em paralelo), ou a sua simultaneidade pode estar sendo simulada através de algum mecanismo de multiplexação de uso da CPU (simulação de paralelismo através da execução em tempo-compartilhado ou time-sharing). Entretanto, para os usuários e programadores , a diferença entre paralelismo simulado e o real é praticamente nula.

Numa rede, as aplicações rodando em diferentes máquinas estão efetivamente executando em paralelo uma em relação as outras. A rede fornece o mecanismo de intercomunicação destas aplicações e deve garantir, entre outras, coisas que a comunicação entre um dado par de aplicações não interfira na comunicação de outro par. Por exemplo supondo a existência de 4 aplicações: A, B, C e D rodando em quatro diferentes máquinas MA, MB , MC e MD , de acordo com a figura abaixo:

A B C D M A M B M C M C

Neste caso as aplicações estão realmente executando em paralelo e a troca de informações entre elas está sendo feita de forma realmente simultânea.

Dentro de uma máquina ou computador também pode existir concorrência (tanto simulada quanto real). Por exemplo numa dada estação de trabalho, um usuário pode ter diferentes clientes sendo executados concorrentemente (um ou mais acessos FTP, um acesso TELNET, várias páginas HTML abertas e em processo de transferência, etc.). Atualmente os Sistemas Operacionais (SO) modernos, disponíveis nas nossas estações de trabalhos e computadores pessoais já disponibilizam o processamento concorrente de forma natural para as aplicações clientes.

(10)

No caso das aplicações e máquinas servidoras a concorrência é ainda mais crucial para garantir um certo nível de eficiência e desempenho. Por exemplo numa dada máquina servidora de arquivos certamente que a concorrência é importante para garantir que um cliente remoto não fique esperando que os outros (p. ex.) 100 usuários anteriores terminem as transferências dos seus arquivos para, somente então, ser atendido.

Porém em relação às aplicações servidoras, a concorrência geralmente não é naturalmente criada pelo S.O. devendo ser implementada na própria aplicação servidora pelo programador. Isto incorpora um grande grau de complexidade no projeto e implementação de aplicações servidoras. Contudo este tipo de cuidado na implementação é realmente de crucial importância, podendo fazer toda a diferença entre um servidor bem aceito pelo mercado e com excelentes características e um servidor que seja um virtual fracasso em termos de mercado.

2.2. Processos e Programas

Em sistemas concorrentes o termo (ou conceito) de processo define a unidade fundamental de computação. Diferente do conceito de programa o conceito de processo evoca um significado mais dinâmico e ativo: um processo é um programa que está realmente em estado de execução numa dada máquina num dado intervalo de tempo.

Entretanto a relação entre processos e programas é bastante próxima, uma vez que um programa pode ser entendido com a especificação precisa de como um dado processo deve executar. Por exemplo um programa contém código e dados tipicamente armazenados um dado arquivo. Quando um usuário quer que o SO execute um dado programa, na verdade, o que o SO faz em resposta a esta solicitação é criar um processo com as informações fornecidas pelo arquivo de programa. Entre outras tarefas menores o SO criará este novo processo pelos seguintes atividades:

• uma área de código será alocada na memória e o código carregado nela, se porventura uma outra instância ou processo deste programa já não estiver em execução;

• uma área de memória será alocada especificamente para este processo;

• uma estrutura de dados contendo uma descrição do estado do processo (um um descritor de processo) será criada no núcleo do SO para manter o controle de estado da execução do processo.

Finalmente se não houver nenhum outro processo mais prioritário em execução o novo processo será ativado.

Nota: atualmente está se tornando muito comum tanto o termo quanto o uso de threads ou linhas de execução, como unidade de execução concorrente mínima. Na verdade o termo ou conceito de threads está mais relacionado às características que alguns SOs de uso geral, como o Linux e o Windows NT, apresentam quando da criação dos seus processos, do que a alguma diferença fundamental entre este conceito e o conceito de processo. Nestes SOs as threads nada mais são do que apenas “processos leves” (que, aliás, é um termo usado como sinônimo de linha de execução ou thread) que executam no mesmo espaço de dados que o processo “completo” original e que, portanto, são mais “leves” para criar e não exigem tantos recursos do SO para a sua execução.

(11)

2.3. Comunicação entre Processos

Existem três grandes formas de processos diferentes se comunicarem entre si. Dependendo tanto do SO subjacente da arquitetura da máquina em que estão em execução, a comunicação entre os processo poderá ser feita através de:

1) Uso de memória compartilhada.

2) Troca de mensagens independentes de informação. 3) Estabelecimento de conexões de comunicação.

A primeira forma, memória compartilhada, geralmente é usada apenas no caso de processos em execução numa arquitetura paralela. No caso de aplicações distribuídas numa rede, são mais usadas as duas últimas formas de comunicação.

O TCP/IP permite o uso de qualquer uma das duas:

• a troca de mensagens pode ser facilmente implementada tanto através do UDP quando as exigências de confiabilidade e sincronismo não são tão grandes. Caso sejam pode-se implementar a troca confiável e síncrona através do TCP,

• a função mais importente desempenhada pelo TCP é justamente a criação e estabelecimento de conexões de comunicação confiáveis e robustas entre pares de aplicações.

2.4. Sincronismo

Tanto no caso da comunicação por mensagens, mas principalmente na comunicação por conexões, existe um requisito básico de “acerto” ou “acordo” temporal entre os pares de aplicações, que é, na verdade, um problema relacionado a como garantir a sincronização entre estes processos, ou seja, como garantir que eles estejam tanto executando quando se comunicando simultaneamente.

Uma forma razoável de resolver este problema seria prevendo nos diversos tipos de serviço de rede a serem providos pelo SO, um mecanismo de sinalização ou de “ativação automática” de um processo / programa quando do recebimento de algum determinado tipo de mensagem ou conexão da rede. Porém, por várias razões (inclusive por problemas de segurança) o TCP/IP não prevê nenhuma forma de sinalização ou ativação automática de processos. Sendo assim somente as mensagens ou conexões que já tenham algum processo ativo esperando por elas é que serão efetivamente tratadas. As demais mensagens serão descartadas e as conexões recusadas.

O modelo cliente/servidor já prevê uma solução apropriada para este problema de sincronismo com a divisão do universo de aplicações em clientes que sempre começam a comunicação e servidoras que estarão sempre esperando pelo estabelecimento de comunicação de algum cliente remoto.

Para que seja possível implementar isto na programação em TCP/IP, o recurso que o SO deverá disponibilizar é algum mecanismo de registro que permita que um processo avise para o SO quais tipos de conexões ou mensagens (portas no caso do TCP) estará disposto a receber.

(12)

2.5. Interfaces de Programação

O TCP é como o próprio nome indica apenas um protocolo e não uma interface de programação de aplicações. O TCP normalmente é implementado através de um conjunto de processos ou rotinas do SO, que constituem o módulo ou entidade de controle da camada de transporte de rede.

O protocolo TCP serve justamente para definir precisamente como será forma de comunicação entre as entidades de transporte de um par de máquinas remotas, mas não foi feito para definir como estas entidades de controle de transporte irão se comunicar com os processos de aplicação.

A definição de como este tipo de comunicação entre o módulo de controle do TCP e os diversos processos de aplicação deve ser feita através de uma Interface de Programação de Aplicação (sigla em inglês API - Applications Programming Interface) específica que disponibiliza para os programadores os serviços do módulo TCP.

Reduzida as suas características mínimas uma API é pouco mais do que uma lista que especifica quais são as rotinas e funções de acesso a estes serviços. Quando necessário também é parte constituinte de uma API a especificação dos tipos de dados adicionais necessários para a implementação destes serviços. Do ponto de vista mais moderno uma API é na verdade a porção visível ao programador de um Tipo Abstrato de Dados ou de uma Classe de Objetos (Programação Orientada a Objetos).

Quando o TCP/IP foi incorporado originalmente aos sistemas UNIX, não foi criada imediatamente uma nova API para uso dos serviços destes protocolos. Na época se assumiu que os serviços de rede disponibilizados pelo TCP (notadamente o estabelecimento de conexões) seriam, grosso modo, equivalentes aos serviços fornecidos pelo sistema de arquivos (paradigma open-read-write-close de manipulação arquivos). Aplic. A Aplic. B Arquivo de A para B Arquivo de B para A

Isto não é tão disparatado quanto possa parecer, porque a comunicação através de uma conexão entre duas aplicações pares pode realmente ser idealizada como sendo feita através de operações de entrada e saída padrão sobre um par de arquivos: um para saída da aplicação A e entrada na B e o outro para o caminho inverso.

Porém este tipo de abstração, embora de bastante valia e ainda presente como um subconjunto da interface atual de desenvolvimento de aplicações TCP/IP, apresenta sérias limitações no que tange a sincronização entre os pares de aplicações.

(13)

Foi justamente para resolver este problema que a Universidade da Califórnia em Berkeley criou a interface sockets de programação e uso dos serviços da camada de transporte de rede. A interface sockets se adapta muito bem ao uso do protocolo TCP/IP para comunicação com máquinas remotas, mas nada impede que outros protocolos de transporte sejam usados (ISO TP4 ou Novell IPX/SPX por exemplo), ou que o serviço não possa ser implementado como um mecanismo local de intercomunicação dos processos internos a uma máquina.

2.6. Introdução a Interface Socket

Tanto o TCP quanto o UDP, que são os dois protocolos de transporte usados na Internet, implementam uma série de “portas” de comunicação dentro de um dado computador ou máquina. Estas portas TCP/UDP são, na verdade, pontos internos de atendimentos dos serviços de transporte.

O TCP/UDP suporta até 65.535 portas distintas porque é usado um campo de 16 bits para identificação de porta nestes protocolos. Na estrutura de intercomunicação prevista pelo TCP/IP os serviços e protocolos criados pelas aplicações de rede deverão obrigatoriamente ser mapeados nestas portas. Os serviços de aplicação padronizados e de uso público são identificados através de números de portas TCP ou UDP reconhecidas publicamente (em inglês well known ports). Tanto as portas quanto os serviços reconhecidos publicamente são definidos através de documentos específicos para isto: as RFCs (Request For Comments) publicados pela IETF (Internet Engineering Task Force) no seu próprio site (www.ietf.org).

Nada impede, entretanto, que se possa definir e usar uma porta qualquer para um serviço proprietário, exceto se esta porta já estiver sendo usada nos computadores em questão para um outro serviço.

Um outro detalhe interessante relacionado ao uso das portas é que, embora não haja nenhuma obrigação para que os serviços fornecidos pelo TCP usem as mesmas portas que os fornecidos através do UDP, tem-se mantido, pelo menos nos serviços públicos que são fornecidos pelos dois protocolos, a compatibilidade entre os números de porta.

O paradigma básico de trabalho no sockets é que a troca de informações entre um par de aplicações deve ser feita por uma conexão de comunicação. Uma vez que esta conexão seja estabelecida todos os dados enviados por uma aplicação são recebidos pela outra, na mesma ordem em que foram enviados. O serviço de sockets não prevê nenhum tipo de estruturação dos dados além do nível de caractere, ou seja, o

sockets não faz nenhuma pressuposição sobre como será o formato de mensagens ou registros enviados

pela conexão. Se houver algum tipo de estruturação nos dados além do nível de caractere então ela deverá ser tratada pelas aplicações que estarão usando os sockets.

(14)

A E C D B Cada um é um socket distinto

São previstas na interface sockets mecanismos para estabelecer (abrir) conexões e encerrar (fechar) conexões. Também são previstos mecanismos que permitem a um processo de aplicação se registrar junto ao SO indicando qual ou quais portas este processo estará usando tanto para o recebimento de mensagens quanto ao envio. Por último também se pode indicar se o uso destas portas será feito de forma ativa pelo processo, isto é, o processo começará a usá-las abrindo conexões com uma máquina remota (caso típico de um cliente) ou então será empregado passivamente através da espera da chegada de conexões (caso típico de um servidor).

A identificação completa de uma conexão socket de uma máquina A para uma máquina B, dentro do universo de todas as conexões sockets possíveis numa Internet, é formada por quatro números ou endereços distintos:

IP-A: Endereço IP da máquina A

Porta-A: Nro. de porta TCP ou UDP da máquina A IP-B: Endereço IP da máquina B

Porta-B: Nro. de porta TCP ou UDP da máquina B.

Na figura a seguir pode-se ver a relação entre portas internas e endereço IP. Também fica claro que um

socket é idenficado de forma única somente pelos endereços de IP e porta das máquinas de origem e

(15)

P1 P2 IP1 P3 P4 P5 P10 P11 IP2 P12 P1 P2 IP3 São 5 sockets distintos:

IP1-P1-IP2-P10 IP1-P2-IP2-P10 IP1-P3-IP2-P12 IP3-P1-IP2-P12 IP1-P5-IP3-P2

(16)

Capítulo III - Interface Sockets

3.1. Criando e eliminando sockets

A primitiva usada para criar um socket é a seguinte:

sock_descr = socket( protocol_family, type, protocol )

onde os parâmetros protocol_family e protocol definem, respectivamente, a família de protocolos e, caso existam vários protocolos nesta família, qual o protocolo específico. Para o caso da arquitetura TCP/IP basta usar PF_INET como valor de protocol_family e 0 como valor de protocol. O parâmetro type define o tipo de serviço que se pretende empregar no socket sendo criado. Para o uso na Internet são possíveis dois tipos de serviço:

SOCK_STREAM serviço orientado a conexão baseado no protocolo de transporte TCP

SOCK_DGRAM serviço orientado a datagrama, baseado no protocolo de transporte UDP O parâmetro retornado sock_descr irá conter o identificador do descritor do novo socket sendo criado, caso não tenha ocorrido nenhum erro na criação do socket. Entretanto se o socket não puder ser criado então a primitiva socket irá retornar um valor negativo.

Quando um socket não necessitar mais ser usado deve-se chamar a primitiva close1: close( sock_descr )

para informar ao S.O. que o socket não está mais em uso.

// Tenta criar um socket TCP

sock_descr = socket( PF_INET, SOCK_STREAM, 0 ). if (sock_descr < 0)

{

printf(“Erro: socket nao pode ser criado”);

exit(0);

}

1

(17)

3.2. Especificando endereços

A primitiva socket cria um socket novo, ou seja, aloca os recursos necessários do S.O. para uso de um

socket e também avisa ao S.O. que se vai passar a usar comunicação por sockets neste processo. Esta

primitiva, entretanto, não estabelece nenhuma conexão com uma máquina remota.

Para estabelecer conexões (ou enviar mensagens no caso do serviço orientado a datagramas) é necessário especificar o endereço IP da máquina remota, bem como o identificador de porta desta máquina remota. Isto é feito através das estruturas: sockaddr e sockaddr_in. A primeira destas (sockaddr) descreve o formato genérico dos endereços que podem ser usados na interface sockets e a segunda (sockaddr_in) descreve o formato de endereços específico para a Internet. Como neste texto o enfoque principal é em programação sockets sobre a Internet, somente o formato sockaddr_in será apresentado: struct in_addr { uint32_t s_addr; }; struct sockaddr_in {

unsigned char sin_len; /* tamanho total */

unsigned short sin_family; /* tipo do endereco, deve

ser AF_INET */

unsigned short sin_port; /* porta TCP ou UDP */

struct in_addr sin_addr; /* endereco IP */

char sin_zero[8]; /* nao usado (zerado) */

};

O campo sin_len define o tamanho completo do endereço (somente é usado em familias de protocolos com endereços de tamanho variável, pode ser deixado em 0 no caso da Internet).

O campo sin_family define a família de protocolos do endereço (para Internet deve ser AF_INET). O campo sin_port define qual porta TCP ou UDP será usada e o campo sin_addr define o endereço IP do

host a ser contatado.

Importante:

Tanto a porta quanto o endereço devem estar em formato de dados da rede. Portanto caso se esteja alterando o valor destes campos a partir de variáveis internas do tipo short ou long, deve-se obrigatoriamente usar as primitivas de conversão: htons e htonl, que convertem, respectivamente, os tipos short e long do formato de dados de host para o formato de dados da rede.

(18)

...

sra.sin_port = htons( 80 ); /* vai usar a porta 80 - HTTP */ sra.sin_addr.s_addr = inet_addr(“200.238.29.30”);

...

O contrario também é verdadeiro, para se armazenar estes campos em variáveis internas que depois poderão ser impressas (corretamente) deve-se usar as primitivas de conversão: ntohs e ntohl, que convertem, respectivamente, os tipos short e long do formato de dados de rede para o formato de dados do host.

A rotina inet_addr usada no exemplo converte uma string contendo um endereço IP em notação ponto-decimal para o endereço de binário correspondente, já no formato long da Internet.

3.3. Estabelecendo conexões

De posse do novo socket e do endereço da máquina e porta remota que se pretende contatar (devidamente armazenados numa estrutura sockaddr_in) é possível então estabelecer uma conexão (socket) com a máquina remota se se optou pelo serviço orientado a conexões (baseado no TCP).

É importante notar que o ato de estabelecer ativamente uma conexão com uma máquina remota, caracteriza a aplicação como sendo cliente (somente clientes começam a interação, servidores ficam esperando pela chegada das solicitações). Sendo assim, somente clientes irão tentar estabelecer a conexão inicial.

A primitiva para se estabelecer conexões no sockets é a seguinte:

result = connect( sock_descr, ptr_rem_addr, addrlen )

onde sock_descr contém o descritor de um socket previamente criado, ptr_rem_addr é um apontador para uma estrutura sockaddr_in que contém o endereço IP e a porta da máquina remota e addrlen é fixo devendo ser preenchido com sizeof( sockaddr_in ).

Esta primitiva tentará estabelecer uma conexão TCP com a máquina remota. Se for bem sucedida deverá retornar 0, caso contrário retornará -1. O endereço IP e porta da máquina remota são fornecidos por ptr_rem_addr, porém para o endereço do lado local do socket, se nada mais for especificado, o S.O. usará o endereço IP local do host como endereço IP do lado local. Quanto a porta, também se nada mais for especificado, o S. O. usará a primeira porta TCP que não está sendo utilizada algum outro processo e disponibiliza esta porta para estabelecer a conexão.

(19)

...

res = connect( sock_descr, &sra, sizeof( sockaddr_in )); if (res<0)

{

printf(“Erro: nao conseguiu estabelecer conexao!”);

exit(0);

}

...

3.4. Forçando a amarração de endereços

Do ponto de vista de redes, do lado do cliente pouca coisa é necessário fazer além de criar um socket, estabelecer uma conexão com um servidor remoto, transferir dados de/para o servidor e encerrar o socket. Porém do lado do servidor as coisas são um pouco mais complicadas.

Em primeiro lugar, como o servidor irá ficar esperando passivamente por conexões (ou mensagens datagramas) em determinadas portas é necessário especificar quais serão estas portas, ou seja, não se pode deixar a cargo, como no caso do cliente, do S.O. a definição de que porta usar.

Além disso servidores são, normalmente, máquinas muito poderosas que muitas vezes estão conectadas fisicamente mais de uma rede IP distintas. Neste caso o servidor irá ter um endereço IP diferente para cada uma destas redes, podendo atender a alguns serviços em uma delas mas não necessariamente nas outras (pense num firewall ou num proxy-server que obviamente prestam serviços distintos dependendo de as requisições virem da Intranet ou da Internet).

No caso de servidores ligados a múltiplas redes, também deve-se especificar que serviço (porta) o servidor irá atender para cada endereço IP.

Para resolver estes problemas é usada a primitiva bind:

result = bind( sock_descr, ptr_local_addr, addrlen )

onde sock_descr contém o descritor de um socket previamente criado, ptr_local_addr é um apontador para uma estrutura sockaddr_in que contém o endereço IP e a porta da máquina local e addrlen é fixo devendo ser preenchido com sizeof( sockaddr_in ).

Caso não se queira especificar um endereço IP específico (com o efeito colateral, no caso de servidores com múltiplas portas, de se amarrar a porta a este socket para todos os diferentes endereços IP) pode-se preencher o campo sin_addr da estrutura apontada por ptr_local_addr com o valor

INADDR_ANY, indicando então qualquer endereço IP existente na máquina.

Se não houve nenhum problema na amarração do socket então a primitiva retornará 0, caso ocorra algum problema (por exemplo o endereço IP solicitado não existe ou a porta especificada já está em uso) o valor retornado será -1.

(20)

...

sla.sin_addr = INADDR_ANY; sla.sin_port = htons( 80 );

res = bind( sock_descr, &sla, sizeof( sockaddr_in ) ); ...

3.5. Recebendo conexões

O próximo passo para um servidor é avisar ao S.O. que as conexões remotas poderão ser aceitas. Isto é feito pela primitiva listen:

listen( sock_descr, queue_size )

Esta primitiva não apenas avisa que o socket sock_descr irá ficar num estado passivo de espera de conexões mas também serve para informar o S.O. qual será o tamanho máximo da fila de requisições associada ao socket, através do parâmetro queue_size.

Contudo esta primitiva não espera pelas conexões - ela apenas avisa ao S.O. para que este coloque o

socket em modo passivo. Para atender as conexões entrantes será usada a primitiva accept: new_sock_descr = accept( sock_descr, ptr_rem_addr, ptr_addrlen ) onde sock_descr contém o descritor de um socket previamente criado, que já foi amarrado a um endereço através de bind e que já foi posto em modo passivo através de listen, ptr_rem_addr é um apontador para uma estrutura sockaddr_in que conterá o endereço IP e a porta da máquina remota quanto a conexão for estabelecida e ptr_addrlen é um apontador para um inteiro que (na prática em redes Internet) será preenchido com o valor sizeof( sockaddr_in ).

A primitiva accept é bloqueadora, isto é, a execução da aplicação fica “travada” até que uma nova conexão seja recebida. Somente após a recepção desta conexão é que a aplicação voltará a executar. Além disso os dados desta nova conexão poderão ser acessados através do novo socket sendo criado (new_sock_descr). O socket original sock_descr somente serve para especificar qual o endereço IP e porta a ser usados no lado local (serve também como um ponto de referência para a fila de requisições que o S.O. cria para o socket), mas não deverá ser usado para a troca de dados!

...

listen( sock_descr, 15 );

new_sd = accept( sock_descr, ptr_rem_addr, ptr_addrlen ); ...

(21)

3.6. Transferência de Dados

Uma vez que a conexão tenha sido estabelecida a troca de dados por meio da interface sockets é uma atividade bastante simples. Existem dois conjuntos de primitivas para esta troca de dados:

as primitivas de escrita / leitura derivadas da API do sistema de arquivos (read / write)

novas primitivas de envio / recepção específicas da interface sockets (send / recv / sendto / recvfrom)

A seguir é apresentada a lista destas primitivas, com uma breve descrição da operação das mesmas: res = read( sock_descr, ptr_buf, len )

res = recv( sock_descr, ptr_buf, len, flags )

Estas duas primitivas recebem dados de um socket definido pelo parâmetro sock_descr. Os dados recebidos na área apontada por ptr_buf, com um tamanho máximo de len bytes. É importante salientar que tanto read quanto recv somente retornam, quando len bytes forem lidos ou em caso contrário a conexão terá sido desfeita e o número de bytes finais que se conseguiu ler sem erro é dado por res. O parâmetro flags serve para solicitar algumas opções especiais da interface sockets (entre estes a opção de se fazer um recv não bloqueante por exemplo).

res = write( sock_descr, ptr_buf, len ) res = send( sock_descr, ptr_buf, len, flags )

Estas duas primitivas escrevem ou enviam dados para o socket definido pelo parâmetro sock_descr. A área apontada por ptr_buf deverá conter os dados (bytes) a serem transferidos e o parâmetro len define o tamanho desta área. Da mesma forma que no caso das primitivas de recepção se o valor retornado pelas primitivas (res) for diferente de len então a conexão foi desfeita e res terá o número dos últimos bytes que se conseguiu efetivamente enviar. O parâmetro flags também serve para solicitar algumas opções especiais da interface socket.

sendto( sock_descr, ptr_buf, len, flags, ptr_dest_addr, addrlen )

recvfrom( sock_descr, ptr_buf, len, flags, ptr_orig_addr, ptr_addrlen ) As primitivas e procedimentos vistos até aqui se adaptam a transferência de dados usando conexões TCP. Mas no caso de se usar o serviço orientado a datagrama do UDP, o processo todo é na verdade muito mais simples:

do lado do cliente, em vez de estabelecer uma conexão (connect) e depois transferir dados, basta informar qual mensagem e qual a máquina e porta destino. Esta é a função da primitiva sendto, que envia dados para uma máquina e porta remotas.

do lado do servidor a seqüência de passos ainda implica em se usar bind e listen, porém o uso do accept é desnecessário, pois basta usar a primitiva recvfrom para receber dados de uma máquina e porta remota.

(22)

Os parâmetros sock_descr, ptr_buf e len têm os mesmos significados vistos anteriormente. Da mesma forma que no connect o parâmetro ptr_dest_addr aponta para uma estrututura sockaddr_in que contém o endereço da máquina e porta remota que se deseja enviar a mensagem. Também da mesma forma que no accept o parâmetro ptr_orig_addr irá conter o endereço IP e porta da máquina que enviou a mensagem. Os parâmetros addrlen e ptr_addrlen têm significado igual que os parâmetros de mesmo nome em connect e accept.

Nota:

(23)

Capítulo IV - Arquitetura Genérica de Clientes

4.1. Introdução

As primitivas sockets de programação vistas até agora permitem a construção de um sem-número de aplicações de rede. Porém, a verdade é que apenas entender quais são as primitivas de comunicação disponibilizadas por uma determinada arquitetura de rede (seja ela TCP/IP, ou Novell  IPX/SPX ou Microsoft  / IBM  NetBIOS  ou qualquer outra arquitetura) não é suficiente para se começar a projetar e implementar aplicações de rede úteis, eficientes e muitos menos otimizadas.

Para tanto é necessário também se ater as características e conceitos envolvidos na criação de aplicações para operar em redes. Dito de outra forma: embora um bom conhecimento das capacidades das primitivas de uma dada interface de programação para redes seja necessário ele não é suficiente, tendo que ser complementado pelo conhecimento de como se pode estruturar a comunicação entre as aplicações e programas.

Dando seguimento a este processo agora se passará a analisar as características e conceitos envolvidos em programação de aplicações cliente/servidoras, começando pela arquitetura genérica usada nas aplicações clientes.

4.2. Características de uma aplicação cliente

De maneira geral as aplicações que agem como clientes são conceitualmente mais simples do que as aplicações servidoras:

• Em primeiro lugar, geralmente não é necessário que as aplicações clientes seja capazes ou tenham que tratar concorrentemente as suas conexões com o servidor (ou servidores).

• Usualmente também não é necessário que uma aplicação cliente tenha privilégios especiais de S.O. (como, p.ex., os necessários para se obter acesso ao cadastro de usuários do sistema).

• Por último, em conseqüência disso também não é normalmente necessário que a aplicação cliente tenha que reforçar ou garantir critérios de segurança de acesso (em geral se usa clientes para se ter acesso a sistemas remotos, portanto são os servidores remotos que terão que controlar este acesso).

4.3. Algoritmo básico de um cliente

As aplicações clientes tem uma estrutura básica bastante simples seguindo o algoritmo apresentado no diagrama visto a seguir:

(24)

(1) Localização do servidor (3) Preenchimento da estrutura de endereço (4) Criação do socket Conexão ou mensagem ? (5.a) Estabelecimento da conexão com o servidor

(6.a) Transferência dos dados

(7.a) Finalização da conexão com o servidor

(5.b) Envio das mensagens (6.b) Recepção das mensagens (2) Identificação do serviço Conexão Mensagem

Os primeiros passos (1 a 4), que são comuns aos clientes que usam streams ou datagramas como forma de comunicação, servem basicamente para preparar o início desta comunicação. Depois disso, será necessário adequar os passos executados (e as primitivas sockets chamadas) para o tipo de comunicação escolhido inicialmente.

4.4. Localização do servidor

Existem vários métodos que podem ser usados para localizar o endereço IP do servidor remoto: (1) definir este endereço fixamente no programa, como constante de compilação

(2) obter o nome do servidor a partir da linha de comando (3) solicitar ao usuário a identificação do servidor

(25)

(5) obter dados sobre o servidor a partir de arquivos de informação do sistema (6) usar um outro protocolo / serviço de rede para obter os dados do servidor

O primeiro modo embora possa parecer bastante restritivo, ainda assim pode ser empregado se em vez de se armazenar um endereço IP fixo no código da aplicação, se armazenar um nome de servidor. Desta forma, pelo uso de aliases, um administrador de sistema poderá configurar o cliente para o seu sistema de forma apropriada.

Porém as formas mais usuais para clientes utilizados diretamente por usuários finais são as (2), (3) e (4) que permitem ao cliente ser configurado com o nome do servidor quando é executado pelo usuário. As formas (5) e (6) também são bastante utilizadas, mas em geral em clientes mais próximos do S.O., que fornecem serviços mais básicos (como acesso a arquivos, impressoras ou portas seriais remotas) ou em outras arquiteturas de rede que não o TCP/IP (a alternativa (6) em particular). A implementação destas alternativas é muito dependente tanto do S.O. sendo usado quanto, também, da arquitetura de rede empregada. Por exemplo, embora o TCP/IP não tenha muitos mecanismos de divulgação de que serviços uma dada irá prestar, existem arquiteturas de redes como o IPX/SPX que são fortemente baseadas em mecanismos de divulgação ou publicação de serviços.

Independente do modo como o identificador do servidor é obtido, é importante que o cliente tenha a capacidade de trabalhar com nomes de servidores e não apenas com endereços IP destas máquinas. Para tanto é necessário não apenas reconhecer e armazenar corretamente estes nomes, mas também chamar as rotinas apropriadas para converter estes nomes em endereços (não esqueça que as primitivas sockets de comunicação usam endereços e não nomes).

A rotina que faz esta conversão é a:

struct hostent *gethostbyname( char *name )

que retorna o endereço referente ao nome na estrutura hostent, vista a seguir:

struct hostent

{

char *h_name; /* nome oficial do host */

char **h_aliases; /* nomes adicionais */ int h_addrtype; /* tipo do endereco */

int h_length; /* tamanho do endereco */

char **h_addr_list; /* lista de enderecos */

};

Os campos que contem nomes ou endereços são organizados como listas porque as máquinas (hosts) podem ter múltiplas interfaces e, portanto, múltiplos endereços e possivelmente nomes. Porém para o caso mais comum, em que se quer o (primeiro) endereço correspondente a um nome, pode-se usar a notação h_addr ao invés de h_addr_list graças a seguinte definição contida no(s) header(s) da interface sockets:

(26)

#define h_addr h_addr_list[0]

Para se obter o endereço correspondente ao nome do servidor, basta usar um trecho de código similar ao seguinte:

...

struct hostent *phe;

char *serv_name = “www.ulbra.tche.br”; ...

if ( (phe = gethostbyname( serv_name )) != NULL )

{

/* Endereco IP valido, que esta’ contido em phe->h_addr */

}

else {

/* Houve algum erro na resolucao do nome */

}

Uma vez de posse do endereço correto do servidor pode-se passar a etapa seguinte.

4.4. Identificação do serviço

O serviço que um dado cliente irá solicitar a um servidor remoto está implícito na própria implementação do cliente (e do servidor também), ou seja, se foi implementado um cliente para se fazer tranferência de arquivos do servidor remoto de/para a máquina local, não se deve esperar que seja possível usar este serviço para fazer, por exemplo, o login remoto ao shell de comandos do servidor remoto.

Apesar disso, a identificação de onde, dentro do servidor remoto, “se encontra” este serviço pode mudar de servidor para servidor. Além disso, diferentes tipos de “serviço”, pelo menos do ponto de vista do usuário final, podem ser acessados por um mesmo tipo de cliente: voltando ao caso do login remoto, um cliente como o TELNET que provê acesso ao login remoto, pode facilmente prover acesso da mesma forma a, por exemplo, uma aplicação de controle de contabilidade que seja baseada em menus e formularios textuais.

Na arquitetura TCP/IP a identificação do serviço é feita por uma porta do TCP ou do UDP, dependendo se ele é prestado por, respectivamente, streams ou datagramas. O TCP/IP não dispôe de um mecanismo genérico de divulgação e publicação automática de serviços (exceto pelo mecanismo relativamente simples implementado pelo DNS), sendo assim a associação de que porta pertence a que serviço (e de que porta remota o cliente deverá usar) é feita através de três formas básicas (não mutuamente exclusivas):

(1) uma definição padrão, publicada em RFC e STD sobre os serviços reconhecidos publicamente, com suas respectivas portas (well known ports)

(27)

(2) arquivos locais aos sistemas que dão nomes aos serviços associados as portas, sejam estes serviços públicos ou proprietários

(3) especificação direta do número de porta a ser usada no servidor remota quando da execução do cliente

Desta forma um cliente que busca páginas HTML numa máquina remota, pode se valer do fato de que a porta padrão para este tipo de serviço é a 80 e usar este número se nenhuma informação adicional for fornecida.

Além disso, caso o cliente não tenha internamente armazenado o número propriamente dito da porta padrão dos servidores HTML, ele pode se valer do fato de que esta porta está associada ao protocolo denominado de “http” e buscar no arquivo de identificação de serviços do sistema, qual a porta correspondente a este protocolo.

Por último, o cliente pode solicitar ao usuário que forneça a porta que irá usar para se conectar ao servidor remoto.

Nos casos em que o cliente irá buscar o número da porta pelo nome do protocolo ou serviço (seja ele pré-definido na aplicação ou fornecido pelo usuário) existe uma rotina específica que fará este tipo de tradução:

struct servent *getservbyname( char *nome_servico, char *nome_protocolo )

que retorna a seguinte estrutura de dados:

struct servent

{

char *s_name; /* nome oficial do servico */

char **s_aliases; /* nomes adicionais */

int s_port; /* porta deste servico */}

char *s_proto; /* nome do protocolo (udp ou tcp) */

};

Para se obter a porta padrão para o protocolo “http” basta executar um trecho de código similar ao seguinte:

(28)

...

struct servent *pse; ...

if ( (pse = getservbyname( “http”, “tcp”)) != NULL )

{

/* Porta padrao localizada e armazenada em pse->s_port */

}

else {

/* Nao achou nenhuma pora para o nome de servico e nome de protocolo indicados */

}

4.5. Preenchimento da estrutura de endereços

Depois da localização do servidor remoto e identificação do serviço, basta definir e preencher a estrutura de armazenamento de endereços padrão para a interface sockets (estrutura sockaddr_in). Na seção Especificando endereços do Capítulo III - Interface Sockets, se apresenta esta estrutura e as regras para o seu preenchimento, salientando-se o cuidado que se deve ter em termos de conversão de tipos de dados (principalmente inteiros short e long) do formato padrão da Internet para o formato da arquitetura de máquina em que se está trabalhando (rotinas htons e htonl) e vice-versa (rotinas ntohs e ntohl).

Um ponto importante em relação aos endereços IP, quando se está analisando a operação dos clientes, tem a ver com o fato de que uma identificação completa de um socket na Internet precisa de 4 valores: dois já foram obtidos, o endereço IP e a porta do servidor remoto e dois são relativos à máquina local: o endereço IP desta e a porta TCP ou UDP que se usará como origem para o socket.

A regra básica para os clientes é bastante simples:

No caso dos clientes não se preocupe em especificar o endereço IP ou porta local, deixe esta função para o S.O. que se encarregará de encontrar os valores apropriados.

Forçar a amarração de endereços (através da primitiva bind) poderá ocasionar problemas e conflitos não só com o pool de números de porta sob controle do S.O. mas também poderá ser de difícil gerenciamento principalmente quando a máquina dispuser de diversas interfaces de rede e, por conseguinte, diversos endereços IP.

4.6. Criação do socket

A criação do socket é um processo simples e direto, sem maiores problemas, cujas características já forma apresentadas na primeira seção do documento Parte III - Interface Sockets que é justamente a seção Criando e eliminando sockets. O terceiro parâmetro da primitiva sockets pode ser sempre deixado com zero porque: somente o protocolo TCP fornece o serviço de conexões (SOCK_STREAM) e somente o

(29)

UDP fornece o serviço de mensagens / datagramas (SOCK_DGRAM), sendo assim o último parâmetro é irrelevante e pode ser deixado em 0.

Notas:

Existe uma diferença entre a interface WinSocket usada nos sistemas Windows 9x, 2000 e NT  e a interface BSD Socket usado nos diversos tipos de UNIX, no Linux e no FreeBSD: o valor retornado pela primitiva socket no caso dos sistemas Windows tem o tipo SOCKET que não pode ser considerado como um int. Sendo assim no caso dos sistemas Windows a regra é testar o valor retornado pela primitiva socket contra a constante INVALID_SOCKET, se eles forem iguais isto implica que o Windows não conseguir criar o novo socket. No caso dos sistemas UNIX-like, uma opção para manter a compatibilidade de código fonte é definir a constante INVALID_SOCKET como (-1).

Outro detalhe importante: nos sistemas Windows, antes de se chamar qualquer primitiva ou rotina da interface sockets é obrigatório chamar a rotina WSAStartup para avisar ao S.O. que esta aplicação irá usar a interface sockets (para saber os detalhes sobre esta rotina consulte a documentação da Microsoft)

4.7. Estabelecimento da conexão com o servidor

Caso tenha sido escolhida a comunicação por conexões, então o próximo passo após a criação do socket é (tentar) estabelecer a conexão com o servidor remoto. Obviamente que este não é um processo determinístico que irá sempre ser atendido. Várias coisas podem acontecer nesta etapa de forma que a conexão não seja estabelecida:

(1) endereço IP fornecido pode ser inválido (não existe máquina com este endereço),

(2) endereço pode ser válido mas o servidor pode estar desligado ou ter sofrido algum tipo de pane, (3) endereço pode estar OK, o servidor pode estar ligado, mas o segmento de rede que o conecta ao resto

da Internet pode estar com algum tipo de falha,

(4) a porta fornecida pode simplesmente estar errada, ou seja, o servidor não atende nenhum serviço nesta porta,

(5) ou ainda a rede pode estar congestionada e o tempo entre o envio de uma requisição e a chegada da resposta (tempo de resposta) pode ser inaceitável (imagine se ele for de 5 minutos!)

(6) endereço pode estar OK, a porta também, a rede OK, o servidor ligado, mas completamente ocupado atendendo outros clientes, neste caso ele apenas recusa a conexão ou logo no início desta avisa ao cliente que ele não pode atendê-lo,

(7) etc., etc. e etc. ( o número e grau de problemas que podem ocorrer numa rede são simplesmente inacreditáveis, na verdade, o fato das redes funcionarem e funcionarem normalmente bem é que é realmente inacreditável).

Esta enorme série de problemas que podem ocorrer no estabelecimento da conexão (alguns deles obviamente também podem ocorrer depois) serve apenas para alertar o projetista e desenvolvedor que se

(30)

deve fazer um bom trabalho de reconhecimento de tipo de erro que ocorreu e também permitir ao usuário da aplicação cliente ter a chance de alterar suas opções para tentar buscar ou usar outro servidor, porta, etc.

Afora isto, basta chamar a primitiva socket e testar seu resultado. Esta primitiva irá executar 4 tarefas: (1) verificar se o identificador de socket é válido,

(2) preencher o endereço remoto do socket com os dados do segundo parâmetro,

(3) escolher, junto com o S.O. um endereço IP e de porta apropriado para o lado local do socket (caso isto não tenha sido pré-definido com a primitiva bind),

(4) iniciar o processo de estabelecimento da conexão TCP com o servidor remoto, retornando uma indicação se este processo teve sucesso ou não.

4.8. Transferência dos dados

A fase de transferência de dados é a parte crítica da aplicação, porque é nela que o serviço será efetivamente prestado. É durante esta fase que deverá entrar em operação o protocolo de aplicação. Embora as aplicações possam ter distintos tipo de protocolos de aplicação, usando os mais diversos de mecanismos de comunicação, em geral as aplicações de rede que seguem o paradigma cliente / servidor organizam os protocolos de aplicação em interações requisição / resposta.

Este tipo de interação, onde o cliente envia mensagens que fazem requisições ou solicitações ao servidor remoto e, por sua vez, este servidor atende a estas requisições executando alguma tarefa local e enviando uma mensagem de resposta para o cliente, define, na prática, a própria filosofia de se fazer sistemas cliente / servidores.

Além disso, este tipo de interação pode ser facilmente incorporada tanto a operação por conexões (onde a interação requisição / resposta tem um comportamento mais confiável) quanto na operação por datagramas (onde a confiabilidade da interação requisição/resposta não é garantida pela rede, devendo, se necessário, ser garantida pela aplicação).

A seguir é apresentado um trecho de código que justamente demonstra este tipo de interação resposta, para o caso de um cliente HTTP que busca um arquivo remoto (no exemplo se restringiu a operação à versão 0.9 do HTTP, por permitir demonstrar de forma mais simples esta interação):

/* Conseguiu se conectar ao servidor remoto. Agora, usando o protocolo HTTP, solicita o arquivo remoto e o mostra na tela.

*/

/* Envia um request de arquivo no formato mais simples possivel do HTTP (requisicao GET usada no HTTP 0.9) */

sprintf( sbuf, "GET %s \r\n", argv[3] ); sbuf_len = strlen( sbuf );

(31)

if (send( sock_id, sbuf, sbuf_len, 0 ) != sbuf_len)

{

printf( "Erro na transmissao do request!\n" );

exit(0);

}

/* Espera pelo arquivo enviado em resposta e apresenta o mesmo na tela

*/

while ((rbuf_len=recv(sock_id, rbuf, MAXSIZ_RBUF, 0))==MAXSIZ_RBUF)

{ rbuf[MAXSIZ_RBUF] = '\0'; printf( rbuf ); } if (rbuf_len>0) { rbuf[rbuf_len] = '\0'; printf( rbuf ); }

Um ponto importante deve ser ressaltado na implementação da comunicação entre o par cliente / servidor:

O protocolo TCP não preserva (não fornece) nenhum tipo de limitador para registros ou blocos de dados: um stream TCP ligando um par de aplicações se apresenta como um fluxo contínuo bidirecional de caracteres de 8 bits (bytes) sendo transmitidos de uma aplicação para a outra. Na sua forma mais simples, dentro deste fluxo não existirá nenhum tipo de caracter especial ou sinalização de qualquer forma implementada pelo TCP. Isto implica que se houverem limitações lógicas, do ponto de vista do protocolo de aplicação, às mensagens trocadas entre o cliente e o servidor isto deverá ser explicitamente implementado nestas aplicações.

Porém um ponto deve ser levado em conta: assim como o TCP não preserva ou controla qualquer tipo de sinalização / limitação de mensagens, assim também não se deve assumir que ao enviar uma mensagem (ou bloco de dados) de tamanho L por uma primitiva send ou write, esta mensagem ou bloco será recebido com o mesmo tamanho por uma primitiva recv ou read equivalente. Muito provavelmente esta mensagem será recebida numa série de blocos menores de tamanho L1, L2, ...,Ln, cuja soma será igual a L, ou seja, L = L1 + L2 + ... + Ln.

Isto obriga que as aplicações devem estar preparadas para receber os blocos de dados vindos da ponta remota em pequenas partes por vez, e, quando necessário, remontar estes pequenos blocos de dados em mensagens lógicas do protocolo de aplicação.

Do lado da transmissão, embora este problema não seja tão crítico deve-se ao menos tentar evitar a transmissão direta (numa única chamada) de blocos muito grandes, que excedam, por exemplo o tamanho máximo de um datagrama IP (algo em torno de 64.000 bytes).

(32)

4.9. Finalização da conexão

Normalmente a operação de encerramento da conexão será controlada pelo protocolo de aplicação: quando este protocolo chegar a fase final da transferência de informações entre o servidor e o cliente (e o usuário não fizer nenhuma solicitação adicional) então a conexão poderá ser encerrada.

A forma usual para encerrar uma conexão é através da primitiva close (closesocket no caso dos sistemas Windows). Esta primitiva irá encerrar a comunicação nos dois sentidos, liberar o socket de volta ao S.O e avisar a ponta remota que a conexão TCP foi encerrada.

Entretanto é necessário que tanto a aplicação cliente quanto a servidora estejam preparadas para o encerramento anômalo (abort) da conexão que pode ocorrer pelas mais diversas causas (algumas já vistas na seção que trata do estabelecimento da conexão).

Outra situação que pode ocorrer é se chegar a necessidade de encerrar a conexão apenas num sentido: por exemplo o cliente já enviou todas as requisições que queria ao servidor e não enviará mais nenhuma solicitação, porém não sabe por quanto tempo irá receber mensagens de resposta do servidor. Neste exemplo a conexão poderia ser encerrada apenas num sentido: no sentido de transmissão do cliente ao servidor.

Para tratar destes casos especiais, muitas implementações da interface socket (inclusive a WinSocket) disponibilizam o uso da primitiva shutdown :

errcode = shutdown( sock_id, direction )

que permite encerrar a comunicação em apenas um sentido (ou nos dois se assim for especificado, neste caso ela se reduz a primitiva close ou closesocket ), especificado através do parâmetro direction da seguinte forma:

0 - a recepção de dados não será mais permitida neste socket

1 - este socket não aceitará mais operações de transmissão de dados

2 - nem a transmissão nem a recepção serão permitidas

4.10. Comunicação por datagramas

No caso de um cliente que utiliza datagramas como forma de comunicação e que, portanto, opera sobre o protocolo UDP pode-se duas estratégias usar duas estratégias básicas de projeto e implementação da aplicação:

(a) Usar procedimentos de estabelecimento, transferência e finalização de conexão similares aos empregados na comunicação por conexão (mesmas chamadas connect, send / write e recv / read). (b) Usar procedimentos específicos para o envio e recepção de mensagens individuais (respectivamente

primitivas sendto e recvfrom)

Porém um ponto deve ficar claro, a estratégia (a) não implica que uma conexão real de rede seja estabelecida entre o cliente e o servidor, a estratégia (a) apenas serve para indicar ao S.O. quais os

(33)

endereços IP e de porta que devem ser incorporados aos pacotes UDP enviados de uma máquina para outra quando uma chamada send ou write é feita. Nada mais é feito pelo S.O., uma conexão não é estabelecida, mensagens perdidas não são detectadas, se a ordem de chegada das mensagens foi alterada em relação a ordem de saída destas isto também não é percebido pelo S.O. A bem da verdade se uma mensagem foi subdividida em múltiplas mensagens menores, isto também não é detectado.

Se a ocorrência destes eventos pode ocasionar problemas para o cliente ou para o servidor, então código específico para tratar estas ocorrências deve ser incorporado à aplicação.

Descontando estes fatos e as possíveis implicações em termos do projeto e implementação de uma aplicação, a forma de uma aplicação que use a estratégia (a) é bem similar a de uma aplicação que use conexões, tal como já foi apresentado nas diversas seções anteriores.

Já no caso (b) a forma da aplicação muda, sendo composta essencialmente se séries de chamadas as primitivas sendto e recvfrom. No caso do cliente a ordem usual será executar primeiro uma ou mais chamadas a sendto, para enviar a(s) requisição(ões). Depois haverá uma ou mais chamadas a recvfrom para se receber as respostas do servidor remoto.

Referências

Documentos relacionados

• Quando o navegador não tem suporte ao Javascript, para que conteúdo não seja exibido na forma textual, o script deve vir entre as tags de comentário do HTML. &lt;script Language

• Capacitação e Transferência da metodologia do Sistema ISOR ® para atividades de Coaching e/ou Mentoring utilizando o método das 8 sessões;.. • Capacitação e Transferência

Este desafio nos exige uma nova postura frente às questões ambientais, significa tomar o meio ambiente como problema pedagógico, como práxis unificadora que favoreça

Portanto, mesmo percebendo a presença da música em diferentes situações no ambiente de educação infantil, percebe-se que as atividades relacionadas ao fazer musical ainda são

[r]

Excluindo as operações de Santos, os demais terminais da Ultracargo apresentaram EBITDA de R$ 15 milhões, redução de 30% e 40% em relação ao 4T14 e ao 3T15,

Outros fatores que contribuíram para o avanço tecnológico das máquinas elétricas foram: o desenvolvimento de materiais, como os ímãs de ferrita e os de

Esta pesquisa discorre de uma situação pontual recorrente de um processo produtivo, onde se verifica as técnicas padronizadas e estudo dos indicadores em uma observação sistêmica