Prof. Universidade Federal de Mato Grosso do Sul brivaldo@facom.ufms.br 24 de abril de 2017
Sumário 1 O núcleo da rede 2 3
Introdução Nesta aula vamos fazer ter uma visão geral de como programas de rede (usando sockets funcionam) em C e Python. Os principais objetivos são: 1 Entender o uso de Sockets;
Introdução Nesta aula vamos fazer ter uma visão geral de como programas de rede (usando sockets funcionam) em C e Python. Os principais objetivos são: 1 Entender o uso de Sockets; 2 Entender o fluxo de comunicação cliente/servidor;
Introdução Nesta aula vamos fazer ter uma visão geral de como programas de rede (usando sockets funcionam) em C e Python. Os principais objetivos são: 1 Entender o uso de Sockets; 2 Entender o fluxo de comunicação cliente/servidor; 3 Desenvolver trabalhos práticos.
Introdução: O que é uma rede? O que nós usualmente chamamos de rede de computadores é composta por um número de camadas de rede, cada uma fornecendo um número diferente de restrições e/ou garantias sobre os dados naquela camada. Os protocolos de cada camada de rede geralmente tem seus próprios formatos de pacotes, cabeçalhos e layout.
Introdução: O que é uma rede? O que nós usualmente chamamos de rede de computadores é composta por um número de camadas de rede, cada uma fornecendo um número diferente de restrições e/ou garantias sobre os dados naquela camada. Os protocolos de cada camada de rede geralmente tem seus próprios formatos de pacotes, cabeçalhos e layout. As sete camadas de rede tradicionais são divididas em dois grupos: superiores inferiores A interface de sockets fornece uma API uniforme para as camadas inferiores de rede e nos permite implementar aplicações nas camadas superiores.
Introdução: IP e Rede Dentro de uma rede de computadores, podem existir diversos tipos de equipamentos e utilizando vários meios físicos de comunicação: ethernet, wifi, bluetooth.
Introdução: IP e Rede Para que dois computadores troquem informações, é necessário entender como esta comunicação. Primeiro vamos analisar o modelo OSI de transmissão de dados:
Transporte Vai ser necessário entender cada equipamento de rede para processarmos uma comunicação direta na rede? Esta é uma pergunta pertinente e a resposta é simples: não!
Transporte Vai ser necessário entender cada equipamento de rede para processarmos uma comunicação direta na rede? Esta é uma pergunta pertinente e a resposta é simples: não! Para realizarmos a comunicação na rede, usamos uma API que cuida dos detalhes de baixo nível chamada de socket. Embora exista uma confusão sobre qual o nível de iteração da API de socket s e o modelo OSI, dizemos que ele está presente nas camadas altas e nas baixas, e vai depender apenas de como sua aplicação funciona.
Transporte O que os sockets fazem? Enquanto a interface de sockets teoricamente permite acesso para outras famílias de protocolos além da IP, na prática, cada camada de rede que você usar na sua aplicação com sockets irá usar o IP.
Transporte O que os sockets fazem? Enquanto a interface de sockets teoricamente permite acesso para outras famílias de protocolos além da IP, na prática, cada camada de rede que você usar na sua aplicação com sockets irá usar o IP. Na camada de transporte, os sockets suportam especificamente dois protocolos: TCP Transmission Control Protocol UDP User Datagram Protocol
Transporte Os sockets não sabem quando estão sendo executados sobre ethernet, token ring ou uma conexão discada, nem tem noção alguma sobre os protocolos de alto nível, como NFS, HTTP ou FTP. As vezes, a interface de sockets não será a sua melhor escolha para programar para a rede. Existem outras bibliotecas (em várias linguagens) que podem utilizar os protocolos em alto nível, sem deixar você se preocupar com os detalhes envolvidos na manipulação das conexões. As camadas mais baixas, como por exemplo no domínio de aplicações para drivers de dispositivos tem muito mais interesse nos endereçamentos e manipulações manuais do socket.
Transporte: TCP Quando estamos programando uma aplicação que utiliza sockets, você deve escolher entre usar TCP ou usar UDP (para transportar os dados). O TCP é um protocolo stream, enquanto o UDP é um protocolo orientado a datagramas. O TCP estabelece uma conexão contínua entre o cliente e o servidor e a mantém aberta durante toda a comunicação. Cada byte pode ser escrito (e a ordem correta é garantida), enquanto a conexão estiver ativa. Entretando os bytes escritos sobre o TCP não tem uma estrutura interna, então, protocolos de alto nível são necessários para delimitar quaisquer dados e campos entre as transmissões.
Transporte: UDP Por outro lado, o UDP não precisa que nenhuma conexão seja estabelecida entre um cliente e um servidor, ele simplesmente transmite a menssagem entre os endereços. Uma funcionalidade interessante do UDP é que ele é capaz de auto-delimitar cada datagrama indicando exatamente onde ele começa e termina. Entretando, ele não garante que os pacotes chegaram na ordem, ou que de fato chegaram ao destino. Protocolos de alto-nível utilizados sobre o UDP, podem é claro, fornecer um formato de handshaking e confirmações. Uma analogia as diferenças entre TCP e UDP podem ser feitas pensando numa chamada telefônica e na postagem de uma carta.
Transporte: analogias TCP: Conversa telefônica: Uma chamada telefônica não está ativa até que uma pessoa disque para outra e esta atenda. O canal telefônico continua ativo enquanto os participantes estiverem conversando e eles são livres para dizer muito ou pouco durante a chamada. A conversa ocorre em uma ordem temporal. UDP: Carta pelo correio: Quando enviamos uma carta, o correio inicia o procedimento de entrega da carta sem ter certeza que o destinatário existe e nem quanto tempo levará para a carta ser entregue. O destinatário pode receber várias cartas em qualquer ordem, e a carta pode ser perdida no caminho.
Transporte Além do protocolo TCP ou UDP, existem duas coisas que um peer (cliente ou servidor) precisa saber sobre a máquina que deseja se comunicar: o enderenço IP e a porta. Um endereço IP é representado como um dado de 32-bits, usualmente representado por quatro campos numéricos separados por ponto, exemplo: 200.129.202.132. A porta é um valor de 16-bits, simplesmente representado por um número menor que 65.536.
Transporte Em ambientes Unix, somente o super usuário (root) pode utilizar portas abaixo da 1024 para serviços. Todos os serviços mapeados pela IANA encontram-se em /etc/services e oficialmente sempre vão existir duas entradas, uma para TCP e outra para UDP, mesmo que o UDP não seja utilizado. Um endereço IP pega um pacote para uma máquina, a porta permite a máquina decidir para que processo/serviço (se tiver algum) para direcioná-lo. Está uma forma simples de explicar, mas a idéia está correta no geral.
Rede Na maioria das vezes quando nós pensamos sobre um computador (cliente ou servidor) na Internet, não lembramos dos números associados a cada um, por exemplo, ninguém lembra o IP do Google de cabeça lembra? mas um nome sim: www.facom.ufms.br Para encontrar um IP associado a um host (computador cliente/servidor), em particular usamos o Serviço de Resolução de Nomes, ou DNS (Domain Name Server), e algumas vezes a resolução local é utilizada antes (geralmente o /etc/hosts). Assumam sempre que endereço IP estará disponível.
Rede Como funciona a resolução de nomes? O programa de linha de comando: nslookup ou o comando dig podem ser utilizados para encontrar o endereço IP de um host através do seu nome simbólico. Atualmente, várias ferramentas simples fazem isso também só que de forma passiva, como o ping ou traceroute. Mas é simples fazer isso programando. Usando a linguagem C, a biblioteca padrão fornece uma função chamada gethostbyname(), que é utilizada para resolver o nome de um endereço. Vejamos uma versão simplificada do aplicativo nslookup.
Rede: meulookup.c # include < stdio.h > /* stderr, stdout */ # include < stdlib.h > /* exit */ # include < netdb.h > /* hostent struct, gethostbyname () */ # include < netinet / in.h > /* in_ addr structure */ # include < arpa / inet.h > /* inet_ ntoa () para formato do endereco IP */ int main ( int argc, char ** argv ) { struct hostent * host ; /* informacao do host */ struct in_addr h_addr ; /* endereco de internet address */ if( argc!= 2) { fprintf ( stderr, " Use : meulookup < inet_address >\ n"); exit (1) ; }
Rede: meulookup.c if (( host = gethostbyname ( argv [1]) ) == NULL ) { fprintf ( stderr, "( mini ) nslookup falhon para %s \n", argv [1]) ; exit (1) ;} h_addr. s_addr = *(( unsigned long *) host - > h_ addr_ list [0]) ; fprintf ( stdout,"%s\n", inet_ntoa ( h_addr )); } return 0;
Rede Sempre que possível as bibliotecas serão comentadas com as funções que foram utilizadas, para facilitar a compreensão. É criada um ponteiro para estrutura que vai armazenar as informações do host que estamos procurando o IP (*host), e uma estrutura que vai manipular o endereço de Internet (h addr). A função inet ntoa() é uma função que converte um endereço de Internet, entregue na ordenação de bytes da rede, para uma string em IPv4 em notação ponto-decimal.
Rede: meulookup.py Como seria a mesma aplicação escrita em Python? #!/ usr / bin / env python import socket, sys print socket. gethostbyname ( sys. argv [1]) Linguagens orientadas a objeto é o poder do encapsulamento e das bibliotecas.
Cliente-Servidor Os exemplos de cliente e servidor a seguir são os mais simples quanto possível. A aplicação deve enviar exatamente o que receber, dai o nome: cliente/servidor echo. Todo computador utiliza este tipo de comunição para propósitos de depuração, por isso se torna tão conveniente como exemplo. Diferente do primeiro exemplo que só realizava consultas, vamos desta vez programar o cliente e o servidor de echo. Mas primeiro vamos entender os passos da comunicação entre os clientes.
Cliente-Servidor Three-way-handshake no TCP Entender como uma comunicação é estabelecida é importante para que
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket 2 TCP: estabelecer a conexão com o servidor
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket 2 TCP: estabelecer a conexão com o servidor 3 Enviar algum dado para o servidor
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket 2 TCP: estabelecer a conexão com o servidor 3 Enviar algum dado para o servidor 4 Receber algum dado devolta.
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket 2 TCP: estabelecer a conexão com o servidor 3 Enviar algum dado para o servidor 4 Receber algum dado devolta. 5 algumas vezes o envio e recepção podem alternar por um tempo
Cliente-Servidor Para enteder a comunicação via socket, é preciso compreender que passos são necessários para escrever uma aplicação cliente: 1 Criar o socket 2 TCP: estabelecer a conexão com o servidor 3 Enviar algum dado para o servidor 4 Receber algum dado devolta. 5 algumas vezes o envio e recepção podem alternar por um tempo 6 TCP: fechar a conexão
Cliente-Servidor: cliente echo.c # include < stdio.h > /* printf */ # include < stdlib.h > /* exit */ # include < string.h > /* bzero */ # include <sys / socket.h > /* struct sockaddr, socket */ # include < netinet / in.h > /* struct sockaddr_ in */ # include < arpa / inet.h > /* inet_pton, htons */ # include < unistd.h > /* read */ # define BUFFSIZE 32 # define SA struct sockaddr A definição do cabeçalho é muito parecida com o anterior, veja que agora estão explicitas as definições.
Cliente-Servidor: cliente echo.c int main ( int argc, char * argv []) { int sockfd, received = 0, bytes = 0; struct sockaddr_ in servaddr ; char buffer [ BUFFSIZE ]; if ( argc!= 4) error (" Use : TCPecho <server_ip > <palavra > <porta >"); sockfd = socket ( PF_INET, SOCK_STREAM, IPPROTO_ TCP ); bzero (& servaddr, sizeof ( servaddr )); servaddr. sin_family = AF_INET ; servaddr. sin_addr. s_addr = inet_addr ( argv [1]) ; servaddr. sin_port = htons ( atoi ( argv [3]) ); Note que foi criado o descritor do socket e inicializada a estrutura servaddr.
Cliente-Servidor: cliente echo.c connect ( sockfd, (SA *) & servaddr, sizeof ( servaddr )); send ( sockfd, argv [2], strlen ( argv [2]), 0); Nós realizamos a conexão passando para a função connect o descritor do socket que é um inteiro, o endereço da estrutura e o tamanho desta estrutura. Um detalhe importante fica por conta do cast (SA *) que força a conversão para a estrutura usada na Internet.
Cliente-Servidor Escrevendo nosso primeiro Cliente-Servidor } printf (" Recebido : "); while ( received < strlen ( argv [2]) ) { bytes = recv ( sockfd, buffer, BUFFSIZE -1, 0); received += bytes ; buffer [ bytes ] = \0 ; printf ("%s", buffer ); } printf ("\n"); close ( sockfd ); exit (0) ; E enquanto não é recebida a mensagem completa como retorno, o programa fica aguardando sua transferência.
Cliente-Servidor: Passos para escrever o Servidor Echo Um servidor de socket é um pouco mais complicado que o cliente, principalmente porque ele precisa estar apto a gerenciar vários clientes realizando conexões. Basicamente, existem dois aspectos em um servidor:
Cliente-Servidor: Passos para escrever o Servidor Echo Um servidor de socket é um pouco mais complicado que o cliente, principalmente porque ele precisa estar apto a gerenciar vários clientes realizando conexões. Basicamente, existem dois aspectos em um servidor: 1 Manipular cada conexão estabelecida
Cliente-Servidor: Passos para escrever o Servidor Echo Um servidor de socket é um pouco mais complicado que o cliente, principalmente porque ele precisa estar apto a gerenciar vários clientes realizando conexões. Basicamente, existem dois aspectos em um servidor: 1 Manipular cada conexão estabelecida 2 Escutar por conexões para estabelecer No nosso exemplo, e na maioria das vezes, você poderá separar o manipulador de uma conexão em uma função de suporte. Você pode fazer isso de formas diferentes (fork, threads, select).
Cliente-Servidor: servidor echo.c Nosso exemplo de servidor de echo tem novamente as entradas que já vimos no nosso exemplo anterior e definindo nos comentários cada uma das funções que são referidas durante o programa. Bibliotecas necessárias:
Cliente-Servidor: servidor echo.c Nosso exemplo de servidor de echo tem novamente as entradas que já vimos no nosso exemplo anterior e definindo nos comentários cada uma das funções que são referidas durante o programa. Bibliotecas necessárias: # include < stdio.h > /* printf */ # include < stdlib.h > /* exit */ # include < string.h > /* bzero */ # include <sys / socket.h > /* recv, send */ # include < arpa / inet.h > /* struct sockaddr */ # include < unistd.h > /* exit */ # define BUFFSIZE 32 # define MAXPENDING 5 # define SA struct sockaddr
Cliente-Servidor: servidor echo.c int main ( int argc, char * argv []) { int listenfd, connfd, clientlen, n; char buffer [ BUFFSIZE ]; struct sockaddr_ in servaddr, client ; if( argc!= 2) error (" Use : SERVERecho <porta >"); A comparação, embora pouco elegante, do argc não é a melhor forma de verificar opções via linha de comando. Estudem a biblioteca getopt.
Cliente-Servidor: servidor echo.c listenfd = socket ( PF_INET, SOCK_STREAM, IPPROTO_ TCP ); bzero (& servaddr, sizeof ( servaddr )); servaddr. sin_family = AF_INET ; servaddr. sin_addr. s_addr = htonl ( INADDR_ANY ); servaddr. sin_port = htons ( atoi ( argv [1]) ); bind ( listenfd, (SA *) & servaddr, sizeof ( servaddr )); listen ( listenfd, MAXPENDING ); Zeramos o endereço do servidor e preenchemos a estrutura de tal forma a definir que a conexão que estamos fazendo é do tipo TCP/IP.
Cliente-Servidor: servidor echo.c Laço de recebimento e resposta da mensagem. for ( ; ; ) { n = -1; clientlen = sizeof ( client ); connfd = accept ( listenfd, (SA *) & client, & clientlen ); n = recv ( connfd, buffer, BUFFSIZE, 0); while (n > 0) { send ( connfd, buffer, n, 0); n = recv ( connfd, buffer, BUFFSIZE, 0); } close ( connfd ); } exit (0) ;
Cliente-Servidor O laço vai ser executado até que o servidor seja terminado, os passos que serão executados neste trecho final do código são: 1 Aceitar a conexão do cliente e armazenar em connfd;
Cliente-Servidor O laço vai ser executado até que o servidor seja terminado, os passos que serão executados neste trecho final do código são: 1 Aceitar a conexão do cliente e armazenar em connfd; 2 Receber alguma quantidade de bytes em n;
Cliente-Servidor O laço vai ser executado até que o servidor seja terminado, os passos que serão executados neste trecho final do código são: 1 Aceitar a conexão do cliente e armazenar em connfd; 2 Receber alguma quantidade de bytes em n; 3 Enquanto tiver algo a enviar, ele envia, recebe o retorno do que ainda não foi enviado e fica neste passo até terminar.
Cliente-Servidor: cliente echo.py #!/ usr / bin / env python " Use : client.py <servidor > <frase > <porta >" from socket import * import sys if len ( sys. argv )!= 4: sys. exit (0) sock = socket ( AF_INET, SOCK_ STREAM ) sock. connect (( sys. argv [1], int ( sys. argv [3]) )) message = sys. argv [2] messlen, received = sock. send ( message ), 0 while received < messlen : data = sock. recv (32) sys. stdout. write ( data ) received += len ( data ) print sock. close ()
Cliente-Servidor servivor echo.py: implementação em Python. #!/ usr / bin / env python " Use : server.py <port >" from SocketServer import BaseRequestHandler, TCPServer import sys, socket class EchoHandler ( BaseRequestHandler ): def handle ( self ): print " Client connected :", self. client_ address self. request. sendall ( self. request. recv (2**16) ) self. request. close () if len ( sys. argv )!= 2: print doc else : TCPServer ((,int ( sys. argv [1]) ), EchoHandler ). serve_forever ()
Perguntas?