Baixe o app para aproveitar ainda mais
Prévia do material em texto
Universidade Federal de Minas Gerais Curso de Engenharia Elétrica Redes TCP-IP – 2014/1 Trabalho Prático em Grupo 1 – Implementação um programa para comunicação via rede utilizando soquetes – Parte 1 1. Introdução A proposta deste trabalho consiste na implementação de um programa simples de cliente/servidor que utiliza interfaces soquetes para a transmissão de mensagens através de uma conexão TCP. Este programa permite que o usuário em uma máquina digite e envie uma mensagem para um usuário em outra máquina. É uma versão simplificado do programa de chat do Unix. Para isso, escolheu-se utilizar o sistema operacional Ubuntu, versão 12.04 32 bits, disponível para download gratuitamente na internet. O sistema foi utilizado em uma máquina virtual, sendo que o programa escolhido para rodá-la foi o VM Virtual Box, versão 4.3.8 da Oracle. O Ubuntu é um sistema operacional constituído principalmente por software livre, construído a partir do núcleo Linux. Ele se caracteriza por ter versões lançadas semestralmente e tem como proposta oferecer um sistema que qualquer pessoa possa utilizar sem dificuldades, já que possui uma interface gráfica amigável. A grande vantagem do uso do Linux é sua gratuidade e sua fácil instalação. Um soquete é um ponto de comunicação no qual dois processos podem se comunicar através da rede. Ele é um mecanismo de interface onde processos podem mandar mensagens. Existem diversos tipos de redes com diferentes protocolos, diferentes formas de endereçamento e de comunicação. Logo, existem diferentes tipos de soquetes. Para criar um soquete, é preciso realizar a seguinte operação: int socket(int domain, int type, int protocol) A razão para que esta função receba três argumentos é que soquetes são projetados para suportar qualquer tipo de protocolo. O argumento domain especifica a família que vai ser usada, e normalmente é especificado como PF_INET, que significa a família da internet. O argumento type indica a semântica de comunicação: SOCK_STREAM é usado para denotar um stream de bytes; SOCK_DGRAM indica que um serviço orientado a mensagens, como o provido pelo UDP. Quando utilizados juntos, PF_INET e SOCK_STREAM implicam em uso de TCP. O último argumento é um número para identificar um determinado tipo de protocolo. No caso do TCP e do UDP, ambos só têm um tipo de protocolo, então o valor passado neste parâmetro é 0. O próximo passo depende se o programa é um cliente ou um servidor. No servidor, a aplicação executa uma abertura passiva, ou seja, o servidor diz que está pronto para aceitar conexões, mas não chega a estabelecer uma conexão. O servidor faz isso invocando três operações: int bind(int socket, struct sockaddr *address, int addr len) int listen(int socket, int backlog) int accept(int socket, struct sockaddr *address, int *addr len) A operação de bind associa o soquete criado a um endereço específico. Este endereço é o da máquina local, o servidor, e é composto por um endereço de IP juntamente com uma porta. A operação de listen define quantas conexões podem ficar pendentes no soquete especificado. Já a operação de accept realiza a abertura passiva. Esta última operação bloqueia o programa até que algum cliente estabeleça uma conexão, retornando um novo soquete correspondente à conexão recém-estabelecida, bem como o endereço do cliente que foi conectado. Quando o accept retorna, o soquete original ainda existe e ainda corresponde à abertura passiva, de forma que pode ser utilizado em futuras invocações do accept. O cliente realiza uma abertura ativa, ou seja, ele diz que quer se comunicar utilizando a seguinte função: int connect(int socket, struct sockaddr *address, int addr len) Essa função não retorna até que uma conexão TCP seja estabelecida com sucesso, momento quando a aplicação está livre para enviar dados. Neste caso, address contém apenas o endereço do participante remoto. Na prática, o cliente normalmente especifica apenas o endereço do servidor e deixa o sistema preencher as informações locais. Enquanto um servidor utiliza uma porta específica, um cliente não se preocupa com qual porta vai usar para se comunicar, porque o sistema operacional escolhe uma livre no momento da comunicação. Com a conexão estabelecida, os processos das aplicações invocam as seguintes funções para enviar e receber dados: int send(int socket, char *message, int msg len, int flags) int recv(int socket, char *buffer, int buf len, int flags) A primeira função envia uma dada mensagem através de um soquete específico, enquanto a segunda função recebe a mensagem do soquete e a salva em um buffer. As duas funções têm alguns flags de controle de certos detalhes da operação. 2. Experimento O livro-texto (referência [1]) implementa um código do programa “Simplex-Talk”, que utiliza as funções aqui descritas para realizar a comunicação entre um cliente e um servidor. Foi então realizado um experimento que vem do exercício número 32 do capítulo 1 do livro-texto, de forma que algumas modificações foram realizadas no código do livro. O enunciado do problema 32 é o seguinte (tradução livre): Obtenha e construa o programa exemplo de soquete “Simplex-talk” mostrado no texto. Comece um servidor e um cliente, em janelas separadas. Enquanto o primeiro cliente está rodando, inicie mais 10 outros clientes que conectam com o mesmo servidor; esses outros devem ser iniciados em background com sua entrada direcionada de um arquivo. O que acontece com os 10 outros clientes? A conexão deles falha, ou dá “time out”, ou acontece com sucesso? Outras chamadas são bloqueadas? Agora deixe o primeiro cliente sair. O que acontece? Tente isso também com o valor de MAX_PENDING igual a 1. Para realizar a implementação do código do Simplex-Talk, utilizou-se a IDE Eclipse Kepler para C e C++. Para instalar o Eclipse, é necessário ter uma plataforma JDK instalada na máquina. Para Linux, o JDK mais utilizado é o OpenJDK, que pode ser baixada com o comando de terminal $ sudo apt-get install openjdk-7-jre Após instalar o JDK, foi possível então instalar o Eclipse. No Eclipse, foi criado um novo projeto para o código servidor e outro projeto para o código do cliente, e o compilador escolhido para eles foi o Linux GCC. 2.1 Servidor O código implementado para o servidor foi o seguinte: #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> /* Bibliotecas que tiveram que ser incluídas */ #include <string.h> #include <strings.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h> #define SERVER_PORT 54321 #define MAX_PENDING 5 #define MAX_LINE 256 int main() { struct sockaddr_in sin; char buf[MAX_LINE]; socklen_t len; int s, new_s; /* construção da estrutura de endereçamento */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(SERVER_PORT); /* configuração da abertura ativa */ if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("simplex-talk: socket"); exit(1); } if ((bind(s, (struct sockaddr *)&sin, sizeof(sin))) < 0) { perror("simplex-talk: bind"); exit(1); } listen(s, MAX_PENDING); len = sizeof(sin); /* espera pela conexão, então recebe e imprime o texto */ while(1) { if ((new_s = accept(s, (struct sockaddr *)&sin, &len)) < 0) { perror("simplex-talk: accept"); exit(1); } while ((len = recv(new_s, buf,sizeof(buf), 0))) { fputs(buf, stdout); close(new_s); } } Foram necessárias algumas modificações em relação ao código original do livro. Em primeiro lugar, o Eclipse acusou a falta de diversas bibliotecas. Vários trechos do código não estavam sendo reconhecidos e de acordo com os erros que o Eclipse indicava, pesquisas na internet levaram à conclusão de que estava faltando mais uma biblioteca. A variável “len” no código original é do tipo inteiro (int). No entanto, foi necessário alterar seu tipo para “socklen_t” , que é um tipo específico para receber o tamanho de um endereço ou guardar o tamanho de um buffer. Também foi preciso iniciar a variável “len” logo depois de realizar a operação listen() . “len” foi iniciada com o tamanho da estrutura de endereçamento. 2.2 Cliente O código implementado para o servidor foi o seguinte: #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> /*bibliotecas que foram incluídas*/ #include <stdlib.h> #include <strings.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define SERVER_PORT 54321 #define MAX_LINE 256 int main(int argc, char * argv[]) { char *host; struct sockaddr_in sin; char buf[MAX_LINE]; int s; int len; if (argc==2) { host = argv[1]; } else { fprintf(stderr, "usage: simplex-talk host\n"); exit(1); } /* construção da estrutura de endereçamento */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(host); sin.sin_port = htons(SERVER_PORT); /* abertura ativa */ if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("simplex-talk: socket"); exit(1); } if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("simplex-talk: connect"); close(s); exit(1); } /* loop principal: obtém e envia linhas de texto */ while (fgets(buf, sizeof(buf), stdin)) { buf[MAX_LINE-1] = '\0'; len = strlen(buf) + 1; send(s, buf, len, 0); } return 0; } Novamente, foram necessárias modificações em relação ao código original do livro. Assim como no caso do servidor, foi necessário incluir novas bibliotecas. Para uso em rede local onde não há registro de nomes DNS, não é necessário obter o host pelo DNS. Ao contrário, o programa obtém um IP em dotted notation, que é digitado na linha de comando ao executar o programa do cliente. Ele é o único argumento necessário para iniciar a conexão, visto que a porta está declarada com um valor fixo, que é a mesma porta que em que o servidor realiza a abertura passiva. Para colocar o endereço inserido na estrutura de endereçamento em um bloco de 32 bits em network byte order, é utilizada a função inet_addr(). De maneira semelhante, a função htons() converte o número da porta para network byte order. 2.3 Testes e Resultados 2.3.1 Checagem de funcionamento do Simplex-Talk Primeiramente, para verificar o funcionamento do Simplex-talk, realizou-se um teste simples de conexão entre dois computadores de uma rede local wifi. Uma máquina rodou o programa do servidor, enquanto outra máquina rodou o programa do cliente. A máquina original estava conectada em uma rede wifi, mas a máquina virtual mapeia a conexão como se ela fosse realizada através de um cabo Ethernet. A configuração utilizada na máquina virtual é “Placa em Modo Bridge” (Figura 1). O endereço da máquina em seu sistema operacional original (Windows 7) era 192.168.1.7, enquanto o IP da máquina virtual (onde estava o programa do servidor) foi dado por 192.168.1.8 (Figura 2). Com isso, é possível perceber que uma máquina virtual realmente representa uma nova máquina na rede, e possui um IP próprio independente. Figura 1 – Configuração de rede da máquina virtual Figura 2 – Verificação do endereço IP da máquina do servidor Verificado o endereço IP da máquina do servidor, passamos à tarefa de iniciar o programa. Para executá-lo, bastou digitar o seu caminho no terminal no Linux. O caminho corresponde ao diretório em que o programa do servidor foi compilado em modo Release: $ /home/clarisse/workspace/Server/Release/Server Na máquina do cliente, também compilou-se o código em modo Release. Depois, para executá-lo no terminal do Linux, realizou-se um comando semelhante ao anterior, localizando o programa do cliente no diretório onde ele foi compilado, mas acrescentando-se um parâmetro, que é o endereço IP do servidor: $ /home/clarisse/workspace/Client/Release/Client 192.168.1.8 Com isso, mensagens digitadas no terminal do cliente foram enviadas para o servidor com sucesso, demonstrando o funcionamento correto do Simplex-talk. 2.3.2 Execução de 11 clientes simultaneamente com MAX_PENDING = 5 Após a checagem de funcionamento do programa, realizou-se então o exercício 32 do livro-texto, destacando que o número máximo de conexões pendentes estava configurado para ser igual a 5. Reiniciou-se o programa do servidor e o do cliente. No programa do cliente, a mensagem “cliente 1” foi enviada. Abriram-se então mais 10 janelas de terminais, e em cada uma delas executou-se novamente o programa do cliente. Na segunda janela aberta, enviou-se a mensagem “cliente 2”. Na terceira janela aberta, enviou-se “cliente 3”, e assim respectivamente até a mensagem do “cliente 11”. O servidor, até então, só mostrou a mensagem do primeiro cliente: Figura 3 – Saída do servidor quando 11 programas de clientes foram abertos Esperou-se cerca de 2 minutos com essa situação e então a janela do cliente 1 foi fechada. Imediatamente, o servidor recebeu a mensagem “cliente 2”. Fechando a janela de “cliente 2”, o servidor recebe “cliente 3”, e assim sucessivamente até o “cliente 7”. No entanto, quando a janela do “cliente 8” é fechada, essa mensagem não aparece no terminal do servidor. O mesmo acontece com os outros clientes abertos. Nenhuma mensagem de erro é emitida pelos clientes cujas mensagens não apareceram no terminal do servidor. A saída do servidor é mostrada da Figura 4: Figura 4 – Saída do servidor quando as janelas dos clientes vão sendo fechadas após um certo tempo com MAX_PENDING = 5 Repetindo este procedimento sem esperar o tempo de 2 minutos, o resultado é outro. Novamente, abriram-se 11 janelas de clientes, e para agilizar o processo, cada cliente mandou uma mensagem com um número que representa a ordem das conexões: “1”, “2”, “3”, ... “11”. Imediatamente, fecharam-se os terminais na ordem em que eles foram conectados, ou seja, primeiro o terminal que mandou a mensagem “1”, depois o que mandou a mensagem “2” e assim até o terminal que mandou a mensagem “11”. A Figura 5 mostra a saída do servidor nesta situação: Figura 5 – Saída do servidor quando os terminais dos clientes são fechados imediatamente após o envio das mensagens Mediante estes dois últimos testes, é possível perceber o que acontece com os clientes de 1 a 7. Inicialmente, o cliente 1 se conecta ao servidor. Neste momento, a “accept()” cria um novo soquete (new_s), e entra no último loop do código, que é um while correspondente à recepção e exibição de mensagens enviadas por esse cliente, que é destacado na Figura 6: Figura 6 – Destaque do loop final do código do servidor Quando o terminal do primeiro cliente é fechado, então o “accept()” é novamente realizado. Aparentemente, só então o Simplex-talk reconhece os pedidos de conexão realizados pelos outros terminais. A conexão número 2é imediatamente aceita e a mensagem “cliente 2” é exibida. Como o limite de conexões na fila é de 5, os clientes de número 3 a 7 são enfileirados e ficam aguardando sua vez de obter um soquete exclusivo para realizar a comunicação com o servidor. O que acontece com os clientes de número 8 a 11 parece ser um timeout, apesar de não haver nenhuma mensagem de erro emitida por eles. Isso porque quando se espera um tempo aproximado entre 1 e 2 minutos para se fechar os terminais dos clientes, as mensagens enviadas por eles não chegam a aparecer na tela, o que indica que nenhum novo soquete foi criado para que eles pudessem estabelecer conexão com o servidor. Já quando os terminais são fechados imediatamente após o envio de todas as mensagens, as mensagens desses clientes chegam, ainda que fora de ordem. Isso indica provavelmente que os clientes 8 a 11 ainda estavam tentando realizar a conexão, o que tornou possível encaixá-los na fila um a um a cada vez que uma janela de terminal de outro cliente era fechada. Em uma tentativa de entender o problema, vários trechos de “printf()” foram colocados no código e o exercício foi repetido. O trecho do código do servidor alterado é mostrado na Figura 7 (apenas o trecho final foi alterado, sendo que foi declara uma variável “numAcp” que é um contador de número de “accept()”’s que o servidor realizou). Novamente, um tempo entre 1 e 2 minutos foi dado antes de se começar a fechar os terminais dos clientes abertos. A saída nesta situação está mostrada na Figura 8. Figura 7 – Inserção de prints para testar o código do servidor Figura 8 – Saída do servidor após a inserção dos prints Com este teste, é possível ver que após o cliente 7 ser fechado, o programa volta ao início do loop mas não consegue prosseguir. O que é possível deduzir é que o código do servidor fica preso no “accept()”, porque se essa função retornasse algum valor, o código do if/else seria executado e imprimiria alguma das opções de saída presentes no if e no else. 2.3.3 Execução de 11 clientes simultaneamente com MAX_PENDING = 1 Como é solicitado no exercício 32, alterou-se o código do servidor de forma que o número máximo de conexões pendentes fosse alterado de 5 para 1. Isso significa que apenas uma conexão pode ser enfileirada pelo servidor. Apenas o “define” foi alterado no início do código: #define MAX_PENDING 1 Repetiram-se então os testes com a abertura de 11 janelas de clientes, cada um mandando uma mensagem numerada de 1 a 11, assim como foi descrito na seção anterior. Primeiro, realizou-se o teste com a espera de 2 minutos para começar a fechas os terminais dos clientes. A saída é mostrada na Figura 9: Figura 9 – Saída do servidor quando as janelas dos clientes vão sendo fechadas após um certo tempo com MAX_PENDING = 1 Também realizou-se o teste de fechar as janelas dos 11 terminais imediatamente após o envio de todas as mensagens. Diferentes saídas foram obtidas no servidor para este caso. As figuras 10 e 11 mostram dois exemplos: Figura 10 – Primeira saída do servidor quando os terminais dos clientes são fechados imediatamente após o envio das mensagens e MAX_PENDING = 1 Figura 11 – Segunda saída do servidor quando os terminais dos clientes são fechados imediatamente após o envio das mensagens e MAX_PENDING = 1 Novamente, o resultado sugere que há ocorrência de timeout, apesar do programa não gerar os erros esperados. No primeiro caso, quando espera-se o tempo de 1 a 2 minutos para fechar as janelas dos clientes, a saída mostra apenas os clientes 1, 2 e 3. Assim como ocorreu anteriormente, é possível assumir que quando o servidor aceita a conexão 1, ele fica “preso” no loop mostrado na Figura 6. Quando o terminal do cliente 1 é fechado, ele aceita a conexão do cliente 2 e coloca apenas o próximo cliente na fila: o cliente 3. Os próximos clientes acabam não conseguindo um lugar na fila e provavelmente atingem seus respectivos tempos limites de tentativas de conexão. O programa então fica novamente preso no código do “accept()”, que não retorna nenhum valor. Em seguida, os clientes são abertos, enviam mensagens contendo seus respectivos números de ordem de abertura e são fechados imediatamente após todas as mensagens serem enviadas. Esse teste foi realizado 5 vezes, e as saídas foram diferentes em todas elas. Duas saídas foram mostradas nas figuras 8 e 9. Devido à incapacidade humana de realizar este experimento exatamente com a mesma duração, algumas conexões aparentam dar timeout enquanto com outras isso não acontece. É importante notar que as conexões números 1, 2 e 3 sempre chegam ao servidor. 2.3.4 Teste de conexão do cliente em uma porta diferente daquela do servidor Alterando-se a porta do programa do servidor para 54322 e executando-se o cliente, esperava-se que a conexão fosse recusada. Isso foi exatamente o que aconteceu. No código do servidor, o “define” ficou o seguinte: #define SERVER_PORT 54322 A saída do cliente para essa situação é mostrada na Figura 12. Figura 12 – Conexão recusada pelo servidor após a alteração de sua porta 3. Conclusões O programa Simplex-talk funciona, mas não da forma esperada. Quando existe apenas um cliente conectado a um servidor, as mensagens que o cliente envia chegam corretamente. Quando um cliente tenta se conectar a um endereço ou porta errados, a conexão é recusada. Quando vários clientes tentam se conectar e mandar mensagens ao mesmo tempo, apenas um cliente consegue se comunicar por vez. Aparentemente, ao aceitar a conexão do primeiro cliente, o programa do servidor fica “preso” no loop de leitura e impressão das mensagens. Quando a conexão do primeiro cliente é fechada, aqueles que ficaram tentando se conectar são então processados na fila de espera. Com a inserção de “prints” no código do servidor, foi possível ver que após o último cliente que estava na fica ser fechado, o servidor fica bloqueado no “accept()”, que não retorna nenhum valor para o novo soquete. Se há um número de clientes tentando se conectar que é maior do que o tamanho máximo permitido na fila, parece haver um erro de timeout com os clientes que tentam se conectar por último, apesar de que nenhum deles exibe mensagem de erro. 4. Bibliografia [1] PETERSON, L. L.; DAVIE, B. S. Computer Networks: a system approach. 5th Edition. Burlington, USA. Ed. Elsevier, 2012. 921 p. [2] Ubuntu. Wikipedia. Disponível em: < http://pt.wikipedia.org/wiki/Ubuntu>. Acesso em: 17 mar. 2014. [3] How to download and install prebuilt OpenJDK packages. Disponível em: < http://openjdk.java.net/install/index.html>. Acesso em: 5 mar. 2014.
Compartilhar