Diseño y Creación de protocolos de comunicación (Pasos y Alternativas)

Post date: Jul 12, 2016 5:05:57 PM

Un protocolo queda completamente especificado si definimos su semántica, sintaxis y temporización, no obstante, para obtener esta especificación debemos dar una serie de pasos previos. En cualquier caso el diseño de protocolos es un proceso iterativo que puede dar lugar a modificaciones a la hora de su implementación.

Dada una especificación de requisitos, los pasos generales pasan por identificar las entidades, estudiar cómo se relacionan (e.g. identificar qué información y de qué tipo se pasan) y entender cómo evoluciona el estado de las entidades de acuerdo a cómo la información le va llegando.

La especificación de la funcionalidad es el primer paso donde se debe especificar la funcionalidad esperada del protocolo/interfaz, se deben describir las partes involucradas y lo que se espera de la interacción entre las partes. Si como diseñador debe acometer la tarea de realizar una especificación de la funcionalidad de un protocolo lo ideal es que sea lo menos ambigua posible, es decir, no esté sujeta a interpretaciones ya que si son diferentes programadores los que están involucrados en la implementación del protocolo, una especificación ambigua originará muchos problemas a la hora de integrar las diferentes partes.

La especificación debería incluir:

  • Una descripción genérica explicando la funcionalidad.
  • Los servicios que proveerá y requerirá el protocolo.
  • Especificación de entidades y su relación.
  • Especificación de protocolos entre entidades (Si hay varias entidades usando diversos protocolos).
  • Lista de tipos y formatos de los mensajes intercambiados entre entidades.
  • Temporización describiendo la secuencia de intercambio de mensajes para cada servicio.

De forma ideal, en protocolos complejos, se debería realizar una descripción formal de cara a eliminar incoherencias y a, en el mejor de los casos, automatizar la generación de las implementaciones y simplificar la verificación.

Como ya hemos mencionado, un protocolo de comunicaciones entre dos entidades está definido por la sintáxis, semántica y temporización de los mensajes entre dichas entidades y que previamente deben haber acordado. La sintáxis define el formato (campos, rango de cada campo, tamaño, tipo, etc.) de la información a intercambiar, la semántica define qué significa cada campo y cual es su rol en el proceso de comunicaciones y la temporización establece

cómo y cuando se intercambia la información.

El mejor ejemplo de este tipo de definiciones son las RFCs que han hecho posible Internet tal y como lo conocemos hoy en día, por ejemplo, si vemos un extracto de la RFC1350 (protocolo TFTP para la transmisión de archivos), podemos identificar como la descripción incluye la sintaxis en forma de bloques (todas las RFCs son redactadas en texto plano) con su longitud y un acrónimo que posteriormente es usado para su descripción, con posterioridad y de forma textual se describe tanto la temporización como la semántica. Aquí podemos ver un breve ejemplo del protocolo TFTP:

TFTP Formats

Type Op # Format without header

2 bytes string 1 byte string 1 byte

-----------------------------------------------

RRQ/ | 01/02 | Filename | 0 | Mode | 0 |

WRQ -----------------------------------------------

2 bytes 2 bytes n bytes

---------------------------------

DATA | 03 | Block # | Data |

---------------------------------

2 bytes 2 bytes

-------------------

ACK | 04 | Block # |

--------------------

2 bytes 2 bytes string 1 byte

----------------------------------------

ERROR | 05 | ErrorCode | ErrMsg | 0 |

----------------------------------------

Initial Connection Protocol for reading a file

1. Host A sends a "RRQ" to host B with source= A's TID,

destination= 69.

2. Host B sends a "DATA" (with block number= 1) to host A with

source= B's TID, destination= A's TID.

Error Codes

Value Meaning

0 Not defined, see error message (if any).

1 File not found.

2 Access violation.

3 Disk full or allocation exceeded.

4 Illegal TFTP operation.

5 Unknown transfer ID.

6 File already exists.

7 No such user.

Si necesitamos comunicar dos entidades (e.g. servicios software) nos encontramos con dos grandes casos, en un primer caso las entidades software ya existen y necesitan colaborar enviando y recibiendo información de las que habitualmente manejan, por lo que hay que traspasarse estructuras de datos ya existentes entre ellas. En este caso debemos abordar dicho problema desde los mecanismos de serialización que los lenguajes proveen (C++, Java, etc.). Esta aproximación nos permite, a partir de estructuras/objetos del lenguage, pasarlos a un formato común de red, enviarlos e instanciarlos en la otra entidad. Las implicaciones de cara al software existentes se minimizan ya que se manejan estructuras ya definidas. Si se debe establecer el formato en el cual vamos a serializar los datos, en la actualidad por su simplicidad de uso, JSON es uno de los formatos mas utilizados. XML también ha sido utilizado ampliamente en este tipo de procesos.

En el caso de la serialización, la sintaxis no es necesaria definirla ya que la definen las clases/estructuras a compartir y se realiza de forma automática al serializar los objetos, la semántica y temporización bienen definidas en la lógica del cliente/servidor.

De cara a los ejemplos, imaginemos que tenemos la información proporcionada por un sensor conectado a una máquina A donde hay una aplicación que la lee y la muestra por pantalla y ahora la queremos enviar a una Máquina B situada en un lugar remoto a modo de copia de seguridad. A modo de ejemplo, la descripción semántica de los campos proporcionados por el sensor sería un identificador único del sensor, el tipo de sensor que manda la información (de presión, de humedad, etc.), el valor y la unidad de medida de dicho valor.

En C++ y utilizando la librería boost, un ejemplo que emula este comportamiento (por simplicidad substituimos la parte de envío/recepción a la red con un archivo de texto) sería:

// Example based in http://www.boost.org/doc/libs/1\_60\_0/libs/serialization/doc/tutorial.html
#include <fstream>
#include <string>
// include headers that implement an archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
class Sensor
{
public:
  int Id;
  int SensorType;
  float value;
  std::string unit;
private:
  friend class boost::serialization::access;
  template<class Archive>
  void serialize(Archive & ar, const unsigned int version)
  {
    ar & Id;
    ar & SensorType;
    ar & value;
    ar & unit;
  }
public:
  Sensor(){};
  Sensor(int Sensoridentificator, int senstype, float v, std::string u) :
    Id(Sensoridentificator), SensorType(senstype), value(v), unit(u)
  {}
};
int main() {
  std::ofstream ofs("Network");
  const Sensor MachineA(1232, 3, 24.567f, "bars");
  //we emulate the socket with a file
  {
    boost::archive::text_oarchive fileEmulatingaSocket(ofs);
    fileEmulatingaSocket << MachineA;
    // archive and stream closed when destructors are called
  }
  Sensor MachineB;
  {
    std::ifstream ifs("Network");
    boost::archive::text_iarchive ia(ifs);
    ia >> MachineB;
  }
  std::cout<<"Machine A sends.. "<<MachineA.Id<<" "<<MachineA.SensorType<<" "<<MachineA.value<<" "<<MachineA.unit<<std::endl;
  std::cout<<"Machine B receives.. "<<MachineB.Id<<" "<<MachineB.SensorType<<" "<<MachineB.value<<" "<<MachineB.unit<<std::endl;
  // You have a new File called Network where you can see the serialization.
  return 0;
}

La clase Sensor que tiene cuatro campos añade un método serializable que es utilizado para transformar la estructura existente en una cadena de texto que se guarda/recupera de un archivo. Existen varias formas de realizar este paso de la serialización con distintas ventajas/desventajas. En este enlace de donde se ha basado este ejemplo plantea varias alternativas en C++.

Todos los lenguajes de alto nivel tienen librerías que facilitan este concepto de serialización. Un buen ejemplo que puede servir como plantilla en el lenguage Java se puede encontrar en esta web y en Python tenemos, entre otras alternativas, el módulo Pickle Cuando necesitamos centrarnos en el protocolo o en el caso de que se defina desde cero dicho protocolo, podemos centrarnos en el mismo mediante la definición de la sintaxis mediante un lenguaje o formato (google protobuff, ANS1, etc.) que nos permita generar de forma automática (mediante un compilador) y en varios lenguajes las estructuras/clases necesarias para realizar la comunicación usando la estructura definida. Mientras que la sintaxis es definida de esta forma, tanto la semántica como la temporización deben ir programados en el comportamiento de los roles cliente/servidor del protocolo.

Podemos seguir con nuestro ejemplo del sensor para ver unos sencillos ejemplos. En Protobuff la estructura sería algo similar a (archivo sensor.proto):

syntax = "proto2";
message Sensor{
    required int32 Id = 1;
    enum SensorType{
       UNKNOWN=0;
       HUMIDITY = 1;
       ACCELEROMETER = 2;
       PRESSURE =3;
    }
    optional SensorType Type = 2 [default = UNKNOWN];
    optional float value = 3;
    optional string unit = 4;
}

Al igual que en el ejemplo de la serialización de C++, se pasa un identificador del sensor, el tipo de sensor, el valor que proporciona y la unidad en la que proporciona dicho valor. Con este mensaje se podrían generar de forma automática toda la parte de serialización de este tipo de mensajes. Efectivamente si ejecutamos:

$protoc -I=. --cpp\_out=. sensor.proto

se nos generan de forma automática los archivos sensor.pb.cc sensor.pb.h con las clases de los mensajes (en este caso la clase Sensor) con sus métodos para serializar/deserializar los mensajes y mandarlos a través de socket, almacenarlos en archivos, etc. Se soportan los lenguajes C++,C\#, Go, Java y Phyton. En python sólo tendríamos que cambiar la orden al compilador.

$protoc -I=. --python_out=. sensor.proto

Algunos campos que son comunes a muchos protocolos son un identificador de versión del protocolo para que el parser pueda identificar la versión del paquete que recibe, tipo de mensaje de forma que se sepa la información que contiene dicho paquete, una marca de tiempo si el tiempo es relevante, CRC para comprobar que lo que ha llegado es lo mismo que se envío, etc.

Los lenguades de definición de interfaces o IDL (Interface Definition Language) nos permiten realizar algo similar a los lenguajes

anteriores pero a nivel de interfaces. El lenguage Slice del middleware ZeroC Ice es un ejemplo de este tipo de

lenguajes. Obviamente estos lenguajes van a asociados a middlewaresespecíficos. Entre las herramientas que tienen estos middlewares está la de un compilador que, a partir de una especificación concreta de una interfaz, genera los stubs/skeletons, es decir, el código necesario para invocar las operaciones definidas en la interfaz y devolver los resultados asociados y que se encarga de serializar/deserializar los mensajes. El programador no se tiene que encargar del protocolo de comunicaciones en si mismo que es automáticamente generado. El middleware se encargará también de implementar la lógica de comunicaciones (temporización) para que una invocación se lleve a cabo. La definición de interfaces también proporciona, con una elección apropiada de los nombres de los métodos de la interface, un método de captura parcial de la semántica de la comunicación. Capturando una interfaz similar al ejemplo del sensor con Slice sería (archivo sensor.ice):

module uclm{
  enum SensorType {UNKNOWN, HUMIDITY, ACCELEROMETER, PRESSURE};
  interface sensor{
    void communicateNewData(SensorType t, float value, int unit);
  };
};

El compilador de slice2cpp tomaría este archivo y generaría el stubs/skeleton para invocar esa interfaz desde el cliente (el sensor) en el servidor (el que implementa qué hacer con los datos) en c++:

$slice2cpp --impl sensor.ice

Si añadimos la opción --impl nos genera implementaciones de ejemplo de uso de dicho stub/skeleton. Slice y el middleware ZeroC ICE sigue una filosofía de programación orientada a objetos por lo que las interfaces deben ser implementadas por objetos, se pueden definir excepciones, etc. siguiendo una filosofía RMI (Remote Method Invocation)

Existen lenguajes equivalentes para programación procedural en lo que comúnmente se a conocido como RPC (Remote Procedure Call). Por ejemplo, uno de los primeros lenguajes es XDR donde nuestro ejemplo quedaría (sensor.x)

enum sensortype {

UNKNOWN = 0,

HUMIDITY = 1,

ACCELEROMETER = 2,

PRESSURE = 3

};

program UCLM {
    version ONE {
    void communicateNewData(sensortype t, float value, int unit) = 1;
  } = 1;
} = 0x00123;

y de cara a generar dicho procedimiento con toda la lógica para encapsular/desencapsular objetos, tendríamos que invocar:

$rpcgen -N -M -C sensor.x

Es importante en este tipo de interfaces, que el nombre de la interfaz capture la semántica de lo que se hace invocando dicha interfaz/método. Una iniciativa que parece que está teniendo éxito es la plataforma open source gRPC , que, de la mano de google, esta siendo implantada por varias empresas y que se encuentra en su primera versión estable.

No obstante, uno de los tipos de interfaces que más éxito ha tenido en los últimos años es una interfaz con métodos fijos y que deja la semántica totalmente a las estructuras de datos que se pasan entre el servidor y el cliente. Dicha interfaz aprovechan los métodos de comunicación de datos del protocolo HTTP POST, GET PUT, PATCH y DELETE para crear, leer, actualizar/reemplazar, actualizar/modificar y borrar estructuras de datos respectivamente. La semántica/sintaxis en este caso está en la estructura de datos enviada/recibida (a menudo en JSON o XML) a través de este protocolo.

Este tipo de interfaces, no obstante, tienen una enorme ventaja en un campo de aplicación muy concreto, las empresas que proveen de datos a través de internet. Estas empresas, necesitan de este tipo de APIs que utilizan de tecnologías web (HTTP, JSON, etc.) y que no tienen problemas con firewalls que otras aproximaciones podrían tener. Existen todo tipo de herramientas para generar laimplementación tanto del cliente como del servidor una vez descrita elAPI, herramientas como Swagger permiten generar de forma automática cliente, servidor y documentación. Con este tipo de interfaces, al igual que con la serialización, es fundamental conocer la estructura de los datos para implementar cualquier parte de la comunicación.