Mudanças entre as edições de "PTC29008: Projeto 1: Integração com subsistema de rede do Linux"

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar
 
(29 revisões intermediárias por 2 usuários não estão sendo mostradas)
Linha 1: Linha 1:
[[PTC29008:_Projeto_2:_Protocolo_de_aplica%C3%A7%C3%A3o|Próxima aula]]
+
<!--[[PTC29008:_Projeto_2:_Protocolo_de_aplica%C3%A7%C3%A3o|Próxima aula]]-->
 +
[[PTC29008:_Projeto_1:_Mecanismos_Básicos_de_um_Protocolo_de_Comunicação|Próxima aula]]
  
 
__toc__
 
__toc__
 +
 +
O protocolo em desenvolvimento se situa na camada de enlace. Sendo assim, ele se situa entre as camadas de rede e física, como ilustrado nesta figura.
 +
 +
[[imagem:PTC-Proto-layer-linux.jpg]]
 +
<br>''As camadas de rede e a localização do protocolo''
 +
  
 
A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:
 
A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:
Linha 165: Linha 172:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Multiplexação do acesso a descritores de arquivos ===
+
Para o protocolo PTP, pode ser mais conveniente pensar na interface ''tun'' como um objeto. Sua implementação como uma classe pode ser feita como mostrado a seguir:
 +
 
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/tun-cpp.tgz Uma classe C++ para a interface tun]
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/tun-py.tgz Uma classe Python para a interface tun] (ver [http://tele.sj.ifsc.edu.br/~msobral/ptc/tun.html documentação])
 +
 
 +
 
 +
= Um modelo assíncrono para o protocolo =
 +
 
 +
* [http://krondo.com/in-which-we-begin-at-the-beginning/ Uma breve introdução a um modelo assíncrono de concorrência]
 +
* [http://www-di.inf.puc-rio.br/~endler/courses/RT-Analytics/transp/Books/Event%20Processing%20in%20Action.pdf Event Processiong in Action (livro sobre programação orientada a eventos)]
 +
* [https://www.techopedia.com/definition/7083/event-driven-program Event-driven programming]
 +
 
 +
 
 +
A primeira versão do protocolo em edições anteriores desta disciplina seguiu um modelo síncrono, em que a interface de acesso ao protocolo é composta por chamadas bloqueantes. Isso simplificou o projeto, porém tornou-o limitado. Por exemplo:
 +
* o protocolo somente recebia um quadro se a aplicação que o utiliza chamar a operação ''recebe''.
 +
* o gerenciamento de sessão (será visto mais pra frente) não era capaz de realizar a manutenção do enlace
 +
* comunicações bidirecionais não funcionavam bem
 +
 
 +
A segunda versão do protocolo buscou sanar essas limitações, redesenhando-o segundo um modelo assíncrono. Nesse modelo, que segue um [https://www.techopedia.com/definition/7083/event-driven-program paradigma orientado-a-eventos] (''event-driven''), o protocolo fica no controle das ações, detectando eventos para tratá-los. Cada evento detectado é encaminhado para o bloco funcional a que se destina. Nenhuma ação do protocolo é bloqueante, pois eventos devem ser tratados o mais rápido possível. Um protocolo que funcione dessa forma deve portanto possuir algum mecanismo de detecção e encaminhamento de eventos.
 +
 
 +
[[imagem:ptc-Event-loop.jpg|400px]]
 +
<br>''Um loop de eventos (ou dispatcher, poller): eventos são detectados e encaminhados para tratamento para '''callbacks''' previamente definidos''
 +
 
 +
 
 +
Na versão de referência do protocolo feita em C++, a detecção e encaminhamento de eventos é realizada por um componente denominado ''Poller''. O ''Poller'' monitora um conjunto de descritores de arquivos para fins de leitura. Cada descritor é associado a algum bloco funcional do protocolo por meio de um [http://stackoverflow.com/questions/9596276/how-to-explain-callbacks-in-plain-english-how-are-they-different-from-calling-o Callback], o qual inclui também um tempo máximo de espera por dados nesse descritor. Quando um descritor tem dados para serem lidos, o ''Poller'' executa o ''Callback'' correspondente, que tem a responsabilidade de realizar a leitura do descritor e processar os valores lidos. Se um timeout ocorrer, o ''Poller'' excuta o ''Callback'' correspondente, para que possa tratar o timeout. Por fim, se um descritor for um número negativo, o ''Poller'' o considera um ''timer'' (uma vez que somente seu timeout deve ser levado em conta). Esse ''Poller'' está declarado como mostrado a seguir:
 +
 
 +
<syntaxhighlight lang=c>
 +
// Poller: um despachador de eventos
 +
// Um objeto poller é capaz de monitorar um conjunto de descritores de arquivos
 +
// e executar um callback para cada desccritor pronto para acesso
 +
// Cada descritor pode especificar um timeout próprio
 +
class Poller {
 +
public:
 +
  Poller();
 +
  ~Poller();
 +
 
 +
  // adiciona um evento a ser vigiado, representado por um Callback
 +
  void adiciona(Callback * cb);
 +
 
 +
  // remove callback associado ao descritor de arquivo fd
 +
  void remove(int fd);
 +
  void remove(Callback * cb);
 +
 
 +
  // remove todos callbacks
 +
  void limpa();
 +
 
 +
  // vigia os descritores cadastrados e despacha os eventos (chama os callbacks)
 +
  // para ser lido, ou até que expire o timeout (em milissegundos)
 +
  void despache_simples();
 +
  void despache();
 +
 
 +
protected:
 +
    list<Callback*> cbs_to;
 +
    map<int,Callback*> cbs;
 +
};
 +
</syntaxhighlight>
 +
 
  
Pelo diagrama sobre a integração ao subsistema de rede do Linux, o driver do protocolo de enlace deve ler dados tanto do descritor de arquivo da interface ''tun'' quanto da interface serial do transceiver RF. Porém, ao tentar ler algo de um descritor de arquivos (ex: com chamada de sistema [http://manpages.ubuntu.com/manpages/trusty/en/man2/read.2.html read]) o processo é bloqueado. Enquanto estiver bloqueado não se pode ler dados que porventura estejam disponíveis no outro descritor de arquivos.  Isso se deve ao fato de que leituras com ''read'' serem bloqueantes. Para poder aguardar por dados em ambos descritores de arquivo simultaneamente, o processo do protocolo deve multiplexar o acesso a esses descritores.
+
Um ''Callback'', por sua vez, é definido pela classe abstrata ''Callback''. Cada tipo de evento deve ter seu ''Callback'' específico definido como uma especialização dessa classe.
  
Algumas técnicas são bem conhecidas para aguardar dados através de múltiplos descritores de arquivos:
 
# '''Espera ocupada:''' os descritores de arquivo são configurados para serem não-bloqueantes, e assim pode-se implementar um laço que tente ler algo de cada descritor de arquivo em sequência. Se um descritor não tiver dados disponíveis, a leitura retorna imediatamente. Esse tipo de solução tem a grande desvantagem de desperdiçar tempo de processador.
 
# '''Uso de threads:''' cria-se uma thread para ler de cada descritor de arquivo. Como threads são concorrentes, uma thread pode ficar bloqueada esperando dados em um descritor de arquivo enquanto outra lê outro descritor e processa os dados recebidos. Uma solução como essa envolve um projeto cuidadoso para tratar questões de sincronismo das threads.
 
# '''Uso da chamada de sistema [http://manpages.ubuntu.com/manpages/trusty/en/man2/select.2.html select]:''' a chamada ''select'' possibilita esperar por dados em um conjunto de descritores de arquivos. A chamada em si é bloqueante, porém retorna imediatamente se ao menos um dos descritores de arquivos estiver disponível para acesso. A chamada informa quais descritores podem ser acessados sem risco de bloqueio. Essa solução é simples e não precisa dos cuidados com sincronismo como no caso de ''threads''. O exemplo a seguir mostra o uso de ''select'' para esperar por dados vindos da entrada padrão ou a recepção de uma conexão em um socket TCP.
 
{{collapse top|Exemplo de uso de select}}
 
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
#include <sys/select.h>
+
// classe abstrata para os callbacks do poller
 +
class Callback {
 +
public:
 +
    // fd: descritor de arquivo a ser monitorado. Se < 0, este callback é um timer
 +
    // tout: timeout em milissegundos. Se < 0, este callback não tem timeout
 +
    Callback(int fd, long tout);
 +
   
 +
    // cria um callback para um timer (equivalente ao construtor anterior com fd=-1)
 +
    // out: timeout
 +
    Callback(long tout);
 +
   
 +
    // ao especializar esta classe, devem-se implementar estes dois métodos !
 +
    // handle: trata o evento representado neste callback
 +
    // handle_timeout: trata o timeout associado a este callback
 +
    virtual void handle() = 0;
 +
    virtual void handle_timeout() = 0;
 +
 
 +
    // operator==: compara dois objetos callback
 +
    // necessário para poder diferenciar callbacks ...
 +
    virtual bool operator==(const Callback & o) const;
 +
   
 +
    // getter para o descritor de arquivo a ser monitorado
 +
    int filedesc() const;
 +
 
 +
    // getter do valor de timeout remanescente
 +
    int timeout() const;
 +
 
 +
    // ajusta timeout restante
 +
    void update(long dt);
  
#include <sys/time.h>
+
    // recarrega valor de timeout inicial
#include <sys/types.h>
+
    void reload_timeout();
#include <sys/socket.h>
+
 
#include <netinet/in.h>
+
    // desativa timeout
#include <arpa/inet.h>
+
    void disable_timeout();
#include <unistd.h>
+
 
#include <stdio.h>
+
    // reativa timeout
#include <string.h>
+
    void enable_timeout();
 +
 
 +
protected:
 +
    int fd; // se < 0, este callback se torna um simples timer
 +
    long tout;
 +
    long base_tout;// milissegundos. Se <= 0, este callback não tem timeout
 +
};
 +
</syntaxhighlight>
 +
 
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/Poller.tgz Código-fonte do Poller (C++)]
 +
 
 +
 
 +
Um exemplo para uso do Poller é este:
 +
 
 +
<syntaxhighlight lang=c>
 +
#include <iostream>
 +
#include <string>
 +
#include "poller.h"
 +
 
 +
using namespace std;
  
#define PORT 5555
+
class CallbackStdin: public Callback {
 +
public:
 +
  CallbackStdin(long tout): Callback(0, tout) {}
  
int cria_socket(int port) {
+
  void handle() {
  int sd = socket(AF_INET, SOCK_STREAM, 6);
+
    string w;
  struct sockaddr_in addr;
 
  socklen_t len;
 
  
  // Vincula um endereco local qualquer ao socket
+
    getline(cin, w);
  addr.sin_family = AF_INET;
+
    cout << "Lido: " << w << endl;
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
   }
   addr.sin_port = htons(port);
 
  
   len = sizeof(addr);
+
   void handle_timeout() {
  int ok = 1;
+
    cout << "Timeout !!!" << endl;
 
  setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (void*)&ok, sizeof(ok));
 
 
  if (bind(sd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
 
    perror("Ao vincular endereço ao socket");
 
    return -1;
 
 
   }
 
   }
  
   listen(sd, 2);
+
};
 +
 
 +
int main() {
 +
   CallbackStdin cb(5000);
 +
  Poller sched;
  
   return sd;
+
   sched.adiciona(&cb);
 +
  sched.despache();
 
}
 
}
 +
</syntaxhighlight>
 +
 +
== Algo parecido em Python ==
 +
 +
Existe uma API Python chamada [https://eng.paxos.com/python-3s-killer-feature-asyncio asyncio] (veja também [https://realpython.com/async-io-python/ este tutorial]), que é capaz de escalonar eventos e tarefas. Essa API pode ser usada para escrever programas orientados a eventos, porém é um pouco mais complexa do que o ''poller'' aqui apresentado. Sendo assim, foi escrita uma versão do ''poller'' para Python (mas ''asyncio'' tem muito mais recursos !).
 +
 +
A biblioteca padrão Python possui a classe [https://docs.python.org/3/library/selectors.html DefaultSelector], que se assemelha ao ''Poller''. Porém ''DefaultSelector'' não possibilita que se associe um diferente timeout para cada ''callback''. Esse tipo de controle deve ser realizado pelo código que usa ''DefaultSelector''.
 +
 +
Este exemplo mostra o uso de ''DefaultSelector'':
 +
 +
<syntaxhighlight lang=python>
 +
#!/usr/bin/python3
 +
 +
import selectors
 +
import sys
 +
 +
Timeout = 5 # 5 segundos
 +
 +
# um callback para ler do fileobj
 +
def handle(fileobj):
 +
  s = fileobj.readline()
 +
  print('Lido:', s)
 +
 +
sched = selectors.DefaultSelector()
 +
sched.register(sys.stdin, selectors.EVENT_READ, handle)
 +
 +
while True:
 +
  eventos = sched.select(Timeout)
 +
  if not eventos: # timeout !
 +
    print('Timeout !')
 +
  else:
 +
    for key,mask in eventos:
 +
      cb = key.data # este é o callback !
 +
      cb(key.fileobj)
 +
</syntaxhighlight>
 +
 +
 +
Devido a essa limitação em DefaultSelector, a classe Poller foi implementada também em Python.
 +
 +
{{collapse top|O Poller reimplementado em Python}}
 +
As classes Poller e Callback foram implementadas em Python, porém explorando algumas facilidades existentes nessa linguagem. Ambas estão disponíveis no módulo ''poller'':
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/pypoller.tgz Módulo poller]
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/poller.html Documentação sobre módulo poller] ''(obtida com [https://docs.python.org/3/library/pydoc.html pydoc])''
 +
 +
 +
Um programa de demonstração parecido com aquele em C++ foi criado para mostrar o uso da classe Poller:
  
int main() {
+
<syntaxhighlight lang=python>
  printf("Esperando que se tecle algo + ENTER, ou se receba conexão no port %d ...\n\n", PORT);
+
#!/usr/bin/env python3
  fflush(stdout);
 
  
  int fd = 0; // o descritor 0 corresponde à entrada padrão ...
+
import poller
  int sd = cria_socket(PORT); // cria um socket TCP no port PORT
+
import sys,time
  int max_fd = fd;
 
  if (sd > max_fd) max_fd = sd;
 
  
  struct timeval timeout; // para especificar o timeout
+
class CallbackStdin(poller.Callback):
  timeout.tv_sec = 5; //timeout de 2 segundos
+
   
  timeout.tv_usec = 0;
+
    def handle(self):
 +
        l = sys.stdin.readline()
 +
        print('Lido:', l)
 +
       
 +
    def handle_timeout(self):
 +
        print('Timeout !')
 +
 
 +
class CallbackTimer(poller.Callback):
  
  fd_set espera; // um conjunto de descritores
+
    t0 = time.time()
  FD_ZERO(&espera); // zera o conjunto de descritores
+
   
  FD_SET(fd, &espera); // adiciona "fd" ao conjunto de descritores
+
    def __init__(self, tout):
  FD_SET(sd, &espera); // adiciona "sd" ao conjunto de descritores
+
        poller.Callback.__init__(self, None, tout)
 +
       
 +
    def handle_timeout(self):
 +
        print('Timer: t=', time.time()-CallbackTimer.t0)
 +
       
 +
cb = CallbackStdin(sys.stdin, 3)
 +
timer = CallbackTimer(2)
 +
sched = poller.Poller()
  
  // aqui se usa select para monitorar os descritores contidos em "espera"
+
sched.adiciona(cb)
  if (select(max_fd+1, &espera, NULL, NULL, &timeout) == 0) {
+
sched.adiciona(timer)
    // timeout !!
 
    puts("Timeout !");
 
  } else {
 
    puts("Algo chegou ...");
 
  
    // a seguir se verifica que descritores podem ser lidos sem risco de bloqueio
+
sched.despache()
    // i.e.: que descritores estão prontos para serem acessados
+
</syntaxhighlight>
 +
{{collapse bottom}}
  
    if (FD_ISSET(fd, &espera)) {
+
= Atividade =
      char linha[128];
 
      int n;
 
  
      n = read(fd, linha, 128);
+
O protocolo até o momento se resume ao ''enquadramento''. Integre-o com o kernel Linux, de forma que o enlace estabelecido seja apresentado como uma interface de rede ponto-a-ponto ... você precisará usar uma interface do tipo ''tun'' e remodelá-lo para que os eventos sejam tratados assincronamente.
      printf("Leu %d caracteres da entrada padrão: %s\n", n, linha);
 
    }
 
  
    if (FD_ISSET(sd, &espera)) {
+
{{collapse top|Classe Enquadramento como uma especialização de Callback (exemplo C++)}}
      char * msg = "***\r\nrecebi conexão mas desconectei em seguida ...\r\n\r\n";
+
<syntaxhighlight lang=c>
      int n, con;
+
#ifndef FRAMING_H
      struct sockaddr_in addr;
+
#define FRAMING_H
      socklen_t len = sizeof(addr);
+
 +
#include <cstdint>
 +
#include "Serial.h"
 +
#include "Callback.h"
 +
 +
class Enquadramento : public Callback {
 +
public:
 +
  Enquadramento(Serial & dev, int bytes_min, int bytes_max);
 +
  ~Enquadramento();
 +
 +
  // envia o quadro apontado por buffer
 +
  // o tamanho do quadro é dado por bytes
 +
  void envia(char * buffer, int bytes);
 +
 +
  // espera e recebe um quadro, armazenando-o em buffer
 +
  // retorna o tamanho do quadro recebido
 +
  int recebe(char * buffer);
  
      con = accept(sd, (struct sockaddr*)&addr, &len);
+
  // os tratadores de eventos chamados pelo poller
      printf("Recebeu conexão de (%s,%d)\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+
  void handle();
      send(con, msg, strlen(msg), 0);
+
  void handle_timeout();
      shutdown(con,  SHUT_RDWR);
+
 
    }
+
private:
  }
+
  int min_bytes, max_bytes; // tamanhos mínimo e máximo de quadro
}
+
  Serial & porta;
 +
  char buffer[4096]; // quadros no maximo de 4 kB (hardcoded)
 +
 +
  enum Estados {Ocioso, RX, ESC};
 +
 +
  // tipos de eventos associados à serial
 +
  enum TipoEvento {Dado, Timeout};
 +
 
 +
  // tipo Evento: representa um evento
 +
  struct Evento {
 +
    TipoEvento tipo;
 +
    uint8_t octeto;
 +
 
 +
    // este construtor cria um Evento do tipo Timeout
 +
    Evento(): tipo(Timeout) {}
 +
 
 +
    // este construtor cria um Evento do tipo Dado
 +
    Evento(uint8_t x): tipo(Dado), octeto(x) {}
 +
  };
 +
 
 +
  // bytes recebidos pela MEF até o momento 
 +
  int n_bytes;  
 +
 +
  // estado atual da MEF
 +
  int estado;
 +
 +
  // aqui se implementa a máquina de estados de recepção
 +
  // retorna true se reconheceu um quadro completo
 +
  // A máquina de estados recebe como parâmetro um Evento
 +
  bool handle_fsm(Evento & ev);
 +
 +
};
 +
 +
#endif
 
</syntaxhighlight>
 
</syntaxhighlight>
 
{{collapse bottom}}
 
{{collapse bottom}}
  
 +
== Plataforma de desenvolvimento e teste com Virtualbox ==
  
Excetuando a técnica 1, as demais podem ser usadas na implementação do protocolo.
+
O teste de comunicação através de um enlace entre duas interfaces ''tun'' demanda que essas interfaces residam em hosts diferentes. Se ambas interfaces estiverem no mesmo host, a comunicação se dará na realidade através da interface ''loopback''. Uma plataforma de teste simplificada pode ser implantada usando o Virtualbox:
 +
# '''Na máquina real execute o serialemu:''' uma das pontas da serial emulada deve ser usada pelo protocolo na máquina real.
 +
# '''Porta serial na máquina virtual''': antes de executar a máquina virtual, habilite sua primeira porta serial  e associe-a à outra serial do serialemu (ver figura a seguir).
 +
# '''Porta serial dentro da máquina virtual''': uma vez iniciada a máquina virtual, deve-se nela executar o protocolo de forma que use a porta serial ''/dev/ttyS0''.
  
== Atividade ==
 
  
Integre seu protocolo com o kernel Linux. Antes de qualquer coisa, o enquadramento e a detecção de erros devem estar funcionando corretamente.
+
[[imagem:PTC-Serial-virtualbox.png|800px]]
 +
<br>''Configuração da porta serial na máquina virtual: clique na máquina virtual com o botão da direita, e selecione o menu Configurações (ou Settings)''

Edição atual tal como às 15h49min de 27 de março de 2020

Próxima aula

O protocolo em desenvolvimento se situa na camada de enlace. Sendo assim, ele se situa entre as camadas de rede e física, como ilustrado nesta figura.

PTC-Proto-layer-linux.jpg
As camadas de rede e a localização do protocolo


A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:

  1. Criação de um device driver no kernel Linux: uma tarefa árdua, e que envolve um bom conhecimento sobre o kernel Linux e desenvolvimento de device drivers. As bibliotecas de programação usuais disponíveis em espaço de usuário não podem ser usadas. A depuração é difícil. O protocolo fica bem integrado ao sistema operacional.
  2. Implementação do protocolo em espaço de usuário: um protótipo em espaço de usuário é mais fácil de criar, pois pode usar as APIs existentes para a linguagem de programação escolhida. A depuração fica facilitada. Porém a integração com o sistema operacional apresenta um certo overhead devido ao fluxo de processamento transitar frequentemente entre espaços de sistema e de usuário.


Para o objetivo do projeto 1, a segunda opção é a mais adequada. A implementação em espaço de usuário deve usar uma interface de rede do tipo tun para fazer a integração com o subsistema de rede. Com isso, existe uma interface de rede associada ao enlace estabelecido pelo protocolo, a qual possui parâmetros de rede IP (endereço, máscara), e rotas podem ser definidas para destinos alcançáveis através dela. Com isso, qualquer aplicação TCP/IP pode se comunicar usando o protocolo desenvolvido.


A figura a seguir mostra um diagrama que esquematiza a integração do protótipo com o Linux, evidenciando a interface de rede do tipo tun.


PTC-Proto-tap.jpg


A criação e configuração de uma interface tun pode ser feita de forma programática, usando chamadas de sistema do Linux. O código-fonte a seguir demonstra como uma interface dessas pode ser criada e configurada.


função para criar uma interface tun
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> 


// tun_alloc: cria uma interface tun
// Parâmetro de entrada: 
//  char * dev: o nome da interface ser criada. Se for NULL, interface será denominada pelo sistema (ex: tun0)
//
// Retorno: valor inteiro
//  - se > 0: o descritor de arquivo da interface tun criada.
//  - se < 0, ocorreu um erro

#define MTU 256
#define MASK "255.255.255.252"

int tun_alloc(char *dev)
  {
      struct ifreq ifr;
      int fd, err;

      if( (fd = open("/dev/net/tun", O_RDWR)) < 0 ) {
         perror("");
         return  -1;
      }

      memset(&ifr, 0, sizeof(ifr));

      /* Flags: IFF_TUN   - TUN device (no Ethernet headers) 
       *        IFF_TAP   - TAP device  
       *
       *        IFF_NO_PI - Do not provide packet information  
       */ 
      //ifr.ifr_flags = IFF_TUN | IFF_NO_PI; 
      ifr.ifr_flags = IFF_TUN; 
      if( *dev )
         strncpy(ifr.ifr_name, dev, IFNAMSIZ);

      err = ioctl(fd, TUNSETIFF, (void *) &ifr);
      if( err < 0 ){
         close(fd);
	 perror("");
         return err;
      }
      return fd;
  }    

  // esta outra função configura a interface tun: define os endereços IP do enlace ponto-a-ponto,
  // a máscara de rede /30 e ativa a interface. Ela faz o mesmo que o comando:
  // ifconfig nome_tun IP_da_interface dstaddr IP_da_outra_ponta_do_enlace
  // 
  // esta função deve ser usada assim, supondo que a interface tun se chame tun0, e os endereços IP
  // do enlace sejam 10.0.0.1 e 10.0.0.2:
  //
  // if (set_ip("tun0", "10.0.0.1", "10.0.0.2") < 0) {
  //   perror("so configurar a interface tun");
  //   return 0;
  // }
  //
  // Maiores detalhes sobre as chamadas de sistemas utilizadas: ver "man netdevice", ou 
  // http://man7.org/linux/man-pages/man7/netdevice.7.html

int set_ip(char *dev, char * ip, char * dst) {
  struct ifreq ifr;
  struct sockaddr_in *addr;
  int ok;
  int sd;

  // cria um socket para configurar a interface
  // esse socket não tem nada de especial ... 
  sd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sd < 0) return sd;

  // zera todos os bytes da struct ifreq
  // essa struct contém os atributos a serem configurados na interface
  bzero(&ifr, sizeof(ifr));

  // usa o ponteiro addr para referenciar o campo de endereço da struct ifreq
  // isso facilita o preenchimento dos atributos desse campo
  addr = (struct sockaddr_in*)&(ifr.ifr_ifru.ifru_addr);

  // preenche o campo endereço com o enderço IP da interface
  addr->sin_addr.s_addr = inet_addr(ip);
  addr->sin_family = AF_INET;
  addr->sin_port = 0;

  // escreve o nome da interface na struct ifreq. Isso é necessário
  // para o kernel saber que interface é alvo da operação
  strncpy(ifr.ifr_name, dev, IFNAMSIZ);

  // executa uma operação de configuração de endereço IP de interface
  ok = ioctl(sd, SIOCSIFADDR, &ifr);
  if (ok < 0) return ok;

  // preenche o campo endereço com o endereço IP da outra ponta do enlace
  addr->sin_addr.s_addr = inet_addr(dst);

  // executa uma operação de configuração de endereço IP de destino da interface  
  ok = ioctl(sd, SIOCSIFDSTADDR, &ifr);
  if (ok < 0) return ok;

  // preenche o campo endereço com a máscara de rede da interface
  addr->sin_addr.s_addr = inet_addr(MASK);


  // executa uma operação de configuração de máscara de rede da interface  
  ok = ioctl(sd, SIOCSIFNETMASK, &ifr);
  if (ok < 0) return ok;

  // executa uma operação de configuração de MTU da interface  
  ifr.ifr_mtu = MTU;
  ok = ioctl(sd, SIOCSIFMTU, &ifr);
  if (ok < 0) return ok;

  // lê as flags da interface
  ok = ioctl(sd, SIOCGIFFLAGS, &ifr);
  if (ok < 0) return ok;

  // acrescenta flags UP (ativa) e RUNNING (pronta para uso)
  ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
  
  // executa uma operação de configuração flags da interface  
  return ioctl(sd, SIOCSIFFLAGS, &ifr);
}


Após criar a interface, pode-se testar a comunicação com um simples ping:

ping 10.0.0.2

Para o protocolo PTP, pode ser mais conveniente pensar na interface tun como um objeto. Sua implementação como uma classe pode ser feita como mostrado a seguir:


Um modelo assíncrono para o protocolo


A primeira versão do protocolo em edições anteriores desta disciplina seguiu um modelo síncrono, em que a interface de acesso ao protocolo é composta por chamadas bloqueantes. Isso simplificou o projeto, porém tornou-o limitado. Por exemplo:

  • o protocolo somente recebia um quadro se a aplicação que o utiliza chamar a operação recebe.
  • o gerenciamento de sessão (será visto mais pra frente) não era capaz de realizar a manutenção do enlace
  • comunicações bidirecionais não funcionavam bem

A segunda versão do protocolo buscou sanar essas limitações, redesenhando-o segundo um modelo assíncrono. Nesse modelo, que segue um paradigma orientado-a-eventos (event-driven), o protocolo fica no controle das ações, detectando eventos para tratá-los. Cada evento detectado é encaminhado para o bloco funcional a que se destina. Nenhuma ação do protocolo é bloqueante, pois eventos devem ser tratados o mais rápido possível. Um protocolo que funcione dessa forma deve portanto possuir algum mecanismo de detecção e encaminhamento de eventos.

Ptc-Event-loop.jpg
Um loop de eventos (ou dispatcher, poller): eventos são detectados e encaminhados para tratamento para callbacks previamente definidos


Na versão de referência do protocolo feita em C++, a detecção e encaminhamento de eventos é realizada por um componente denominado Poller. O Poller monitora um conjunto de descritores de arquivos para fins de leitura. Cada descritor é associado a algum bloco funcional do protocolo por meio de um Callback, o qual inclui também um tempo máximo de espera por dados nesse descritor. Quando um descritor tem dados para serem lidos, o Poller executa o Callback correspondente, que tem a responsabilidade de realizar a leitura do descritor e processar os valores lidos. Se um timeout ocorrer, o Poller excuta o Callback correspondente, para que possa tratar o timeout. Por fim, se um descritor for um número negativo, o Poller o considera um timer (uma vez que somente seu timeout deve ser levado em conta). Esse Poller está declarado como mostrado a seguir:

// Poller: um despachador de eventos
// Um objeto poller é capaz de monitorar um conjunto de descritores de arquivos
// e executar um callback para cada desccritor pronto para acesso
// Cada descritor pode especificar um timeout próprio
class Poller {
 public:
  Poller();
  ~Poller();

  // adiciona um evento a ser vigiado, representado por um Callback
  void adiciona(Callback * cb);
  
  // remove callback associado ao descritor de arquivo fd
  void remove(int fd);
  void remove(Callback * cb);
  
  // remove todos callbacks
  void limpa();
  
  // vigia os descritores cadastrados e despacha os eventos (chama os callbacks)
  // para ser lido, ou até que expire o timeout (em milissegundos)
  void despache_simples();
  void despache();
  
 protected:
     list<Callback*> cbs_to;
     map<int,Callback*> cbs;
};


Um Callback, por sua vez, é definido pela classe abstrata Callback. Cada tipo de evento deve ter seu Callback específico definido como uma especialização dessa classe.

// classe abstrata para os callbacks do poller
class Callback {
public:
    // fd: descritor de arquivo a ser monitorado. Se < 0, este callback é um timer
    // tout: timeout em milissegundos. Se < 0, este callback não tem timeout
    Callback(int fd, long tout);
    
    // cria um callback para um timer (equivalente ao construtor anterior com fd=-1)
    // out: timeout
    Callback(long tout);
    
    // ao especializar esta classe, devem-se implementar estes dois métodos !
    // handle: trata o evento representado neste callback
    // handle_timeout: trata o timeout associado a este callback
    virtual void handle() = 0;
    virtual void handle_timeout() = 0;

    // operator==: compara dois objetos callback
    // necessário para poder diferenciar callbacks ...
    virtual bool operator==(const Callback & o) const;
    
    // getter para o descritor de arquivo a ser monitorado
    int filedesc() const;

    // getter do valor de timeout remanescente
    int timeout() const;

    // ajusta timeout restante
    void update(long dt); 

    // recarrega valor de timeout inicial
    void reload_timeout();

    // desativa timeout
    void disable_timeout();

    // reativa timeout
    void enable_timeout();

protected:
    int fd; // se < 0, este callback se torna um simples timer
    long tout; 
    long base_tout;// milissegundos. Se <= 0, este callback não tem timeout
};


Um exemplo para uso do Poller é este:

#include <iostream>
#include <string>
#include "poller.h"

using namespace std;

class CallbackStdin: public Callback {
 public:
  CallbackStdin(long tout): Callback(0, tout) {}

  void handle() {
    string w;

    getline(cin, w);
    cout << "Lido: " << w << endl;
  }

  void handle_timeout() {
     cout << "Timeout !!!" << endl;
  }

};

int main() {
  CallbackStdin cb(5000);
  Poller sched;

  sched.adiciona(&cb);
  sched.despache();
}

Algo parecido em Python

Existe uma API Python chamada asyncio (veja também este tutorial), que é capaz de escalonar eventos e tarefas. Essa API pode ser usada para escrever programas orientados a eventos, porém é um pouco mais complexa do que o poller aqui apresentado. Sendo assim, foi escrita uma versão do poller para Python (mas asyncio tem muito mais recursos !).

A biblioteca padrão Python possui a classe DefaultSelector, que se assemelha ao Poller. Porém DefaultSelector não possibilita que se associe um diferente timeout para cada callback. Esse tipo de controle deve ser realizado pelo código que usa DefaultSelector.

Este exemplo mostra o uso de DefaultSelector:

#!/usr/bin/python3

import selectors
import sys

Timeout = 5 # 5 segundos

# um callback para ler do fileobj
def handle(fileobj):
  s = fileobj.readline()
  print('Lido:', s)

sched = selectors.DefaultSelector()
sched.register(sys.stdin, selectors.EVENT_READ, handle)

while True:
  eventos = sched.select(Timeout)
  if not eventos: # timeout !
    print('Timeout !')
  else:
    for key,mask in eventos:
      cb = key.data # este é o callback !
      cb(key.fileobj)


Devido a essa limitação em DefaultSelector, a classe Poller foi implementada também em Python.

O Poller reimplementado em Python

As classes Poller e Callback foram implementadas em Python, porém explorando algumas facilidades existentes nessa linguagem. Ambas estão disponíveis no módulo poller:


Um programa de demonstração parecido com aquele em C++ foi criado para mostrar o uso da classe Poller:

#!/usr/bin/env python3

import poller
import sys,time

class CallbackStdin(poller.Callback):
    
    def handle(self):
        l = sys.stdin.readline()
        print('Lido:', l)
        
    def handle_timeout(self):
        print('Timeout !')
   
class CallbackTimer(poller.Callback):

    t0 = time.time()
    
    def __init__(self, tout):
        poller.Callback.__init__(self, None, tout)
        
    def handle_timeout(self):
        print('Timer: t=', time.time()-CallbackTimer.t0)
        
cb = CallbackStdin(sys.stdin, 3)
timer = CallbackTimer(2)
sched = poller.Poller()

sched.adiciona(cb)
sched.adiciona(timer)

sched.despache()

Atividade

O protocolo até o momento se resume ao enquadramento. Integre-o com o kernel Linux, de forma que o enlace estabelecido seja apresentado como uma interface de rede ponto-a-ponto ... você precisará usar uma interface do tipo tun e remodelá-lo para que os eventos sejam tratados assincronamente.

Classe Enquadramento como uma especialização de Callback (exemplo C++)
#ifndef FRAMING_H
#define FRAMING_H
 
#include <cstdint>
#include "Serial.h"
#include "Callback.h"
 
class Enquadramento : public Callback {
 public:
  Enquadramento(Serial & dev, int bytes_min, int bytes_max);
  ~Enquadramento();
 
  // envia o quadro apontado por buffer
  // o tamanho do quadro é dado por bytes 
  void envia(char * buffer, int bytes);
 
  // espera e recebe um quadro, armazenando-o em buffer
  // retorna o tamanho do quadro recebido
  int recebe(char * buffer);

  // os tratadores de eventos chamados pelo poller 
  void handle();
  void handle_timeout();

 private:
  int min_bytes, max_bytes; // tamanhos mínimo e máximo de quadro
  Serial & porta;  
  char buffer[4096]; // quadros no maximo de 4 kB (hardcoded)
 
  enum Estados {Ocioso, RX, ESC};
 
  // tipos de eventos associados à serial
  enum TipoEvento {Dado, Timeout};

  // tipo Evento: representa um evento
  struct Evento {
    TipoEvento tipo;
    uint8_t octeto;

    // este construtor cria um Evento do tipo Timeout
    Evento(): tipo(Timeout) {}

    // este construtor cria um Evento do tipo Dado
    Evento(uint8_t x): tipo(Dado), octeto(x) {}
  };

  // bytes recebidos pela MEF até o momento  
  int n_bytes; 
 
  // estado atual da MEF
  int estado;
 
  // aqui se implementa a máquina de estados de recepção
  // retorna true se reconheceu um quadro completo
  // A máquina de estados recebe como parâmetro um Evento
  bool handle_fsm(Evento & ev);
 
};
 
#endif

Plataforma de desenvolvimento e teste com Virtualbox

O teste de comunicação através de um enlace entre duas interfaces tun demanda que essas interfaces residam em hosts diferentes. Se ambas interfaces estiverem no mesmo host, a comunicação se dará na realidade através da interface loopback. Uma plataforma de teste simplificada pode ser implantada usando o Virtualbox:

  1. Na máquina real execute o serialemu: uma das pontas da serial emulada deve ser usada pelo protocolo na máquina real.
  2. Porta serial na máquina virtual: antes de executar a máquina virtual, habilite sua primeira porta serial e associe-a à outra serial do serialemu (ver figura a seguir).
  3. Porta serial dentro da máquina virtual: uma vez iniciada a máquina virtual, deve-se nela executar o protocolo de forma que use a porta serial /dev/ttyS0.


PTC-Serial-virtualbox.png
Configuração da porta serial na máquina virtual: clique na máquina virtual com o botão da direita, e selecione o menu Configurações (ou Settings)