Publi

Creando un cliente para un servicio de red con pocas líneas en C++

neon_open_splitshire_r

En la era actual, es muy importante que múltiples aplicaciones accedan a servicios online para obtener la información que desean (o incluso enviarla). Es decir, las aplicaciones han perdido su simplicidad de ejecutarse en una sola máquina, y han pasado a ejecutarse en múltiples máquinas conectadas a través de Internet.

Y, aunque muchos piensan que C++ no es un lenguaje muy indicado para ello, y que no se pueden hacer estas cosas. Yo creo que C++ está en buena forma, tienes que cocinar un poco más las cosas (o si no te las cocinas, utilizar bibliotecas de otras personas que lo hayan hecho antes).

Desde hace meses mantengo un proyecto en Github, llamado Glove (para compilar el ejemplo necesitaremos la biblioteca) cuyo objetivo es hacer facilitar un poco el acceso y creación de servicios de red (por TCP/IP), abstrayendo al programador un poco del uso de sockets, incluso con la posibilidad de correr tareas en segundo plano, dividirse en threads, monitorizar conexiones, etc. Reconozco que al principio, en mi cabeza era mucho más sencillo, pero vi interesante su utilización en algunos de mis programas y fui introduciendo nuevas características como SSL, envoltorio para HTTP, filtros, etc.

De todas formas, haré una serie de posts dedicados a esta biblioteca que servirán como documentación de la misma, y comentar algunas de sus características, así como una forma de tener ejemplos para copiar y pegar; y, por qué no, encontrar bugs y solucionarlos.

¿Otra biblioteca para Internet? ¿Por qué? ¿Por qué?

Porque me apetecía, quería aprender algunas cosas nuevas, y porque con alguna biblioteca que he probado me ha sido imposible cambiar algunas cosas, por ejemplo configurar timeouts de manera más flexible, interceptar las lecturas (algún día traeré un ejemplo), implementar seguridad, etc.
Falta mucho trabajo por hacer, y mucho soporte por dar, hay algunas cosas que no funcionan o no están probadas con IPv6, tal vez en el futuro deba distinguir mejor clientes de servidores…
Aunque, de todos modos, lo considero un buen punto de partida para hacer posibles algunos accesos sencillos a diferentes servicios (o incluso montar un servicio en modo de servidor).

Cliente de comunicación bidireccional

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "glove.hpp"
#include <iostream>

using namespace std;

const std::string WHITESPACE = " \n\r\t";

std::string TrimLeft(const std::string& s)
{
    size_t startpos = s.find_first_not_of(WHITESPACE);
    return (startpos == std::string::npos) ? "" : s.substr(startpos);
}

std::string TrimRight(const std::string& s)
{
    size_t endpos = s.find_last_not_of(WHITESPACE);
    return (endpos == std::string::npos) ? "" : s.substr(0, endpos+1);
}

std::string Trim(const std::string& s)
{
    return TrimRight(TrimLeft(s));
}

int main(int argc, char *argv[])
{
  Glove g;
  try
    {
      cout << "Buffer: "<< g.buffer_size(10)<<endl;
      g.timeout(0.01);
      g.remove_exceptions(Glove::EXCEPTION_TIMEOUT);
      g.connect(argv[1], 50000);
      g<<"Hello, tell me something..."<<endl;
      std::string data = g.receive(-1, true);

      while (1)
    {
      if (!data.empty())
        {
          if (data.substr(0,3)=="FIN")
        break;
          else
        {
          data = Trim (data);
          cout << "Server sent: "<<data<<endl;
          g<< "Hi: "<<data<<" for you."<<endl;
        }
        }

      if (GloveBase::select(0, 0, GloveBase::SELECT_READ) == 0)
        {
          std::string input;
          getline(cin, input);
          g<<input<<endl;
        }

      data = g.receive(-1, true);
    }
      g.disconnect();
    }
  catch (GloveException &e)
    {
      cout << "Exception: "<<e.what() << endl;
    }

  return 0;
}

Para compilar, podemos hacerlo así:

$ g++ -o example5 example5.cc glove.cpp -DENABLE_OPENSSL=0 -std=c++11

Es tan largo porque, por un lado, debemos desactivar el compilado con soporte O

No hay comprobación de errores de entrada. Para probarlo, podemos montar un servidor netcat escuchando en el puerto TCP 50000 con el siguiente comando:

$ netcat -l -p50000

Luego, ejecutamos example5 de la siguiente manera:

$ ./example5 localhost

Haremos una conexión local en la que podremos enviar mensajes desde cliente a servidor y viceversa casi sin retardo, hasta que el servidor envía FIN, momento en el cual el cliente desconecta.

¿Qué podemos hacer con Glove?

Con g, un objeto de clase Glove:

  • g.buffer_size(10) : hace que el buffer de entrada sea de 10bytes. Por ejemplo, si enviamos un mensaje desde el servidor al cliente de más de 10 letras, éste se dividirá en dos mensajes automáticamente. Podemos hacer que el buffer sea grande, un buen valor puede ser 4096 o 16384, aunque depende de nuestra aplicación.
  • g.timeout(0.01) : establecemos el tiempo tras el cual pensaremos que no ha llegado nada (timeout). Este tiempo vienen dado en segundos, y podemos utilizar un double para expresarlo. En este ejemplo, todo el tiempo que no estemos recibiendo mensajes desde el servidor, estaremos dando timeouts, para verificar la entrada de teclado.
  • g.remove_exceptions(Glove::EXCEPTION_TIMEOUT): Normalmente cuando ocurre un timeout devolvemos una excepción. Por ejemplo, si estamos conectando con algún tipo de servicio de Internet, el hecho de recibir un timeout, puede causar que los datos no se hayan recibido bien, o que el servidor no nos está haciendo caso, por lo que deberemos dejar lo que estamos haciendo. Debido a la naturaleza del ejemplo, esto no es así en este caso. Podemos eliminar las excepciones Glove::EXCEPTION_DISCONNECTED, éstas ocurren cuando el servidor nos ha desconectado.
  • g.connect(argv[1], 50000): Conecta con el servidor argv[1] en el puerto 50000
  • g<<“Hello, tell me something…”<<endl : Envía con la conexión actual el mensaje “Hello, tell me something…” y un salto de línea. Igual que cuando hacemos cout, podemos enviar contenidos por la red así. También vale el operador >>para recibir desde el socket y escribir en una variable local tipo string
  • string data = g.receive(-1, true): Otra forma de recibir información, de esta manera tenemos dos argumentos extra, el primero es el timeout que, si lo dejamos a -1 cogerá el timeout por defecto (que definimos con g.timeout(double), el segundo vale para realizar una sola lectura de datos y devolver lo que se ha leído (si es que se ha leído algo). Veremos más abajo que hay otra forma de hacerlo usando operadores.
  • GloveBase::select(0, 0, GloveBase::SELECT_READ) : Esta línea detecta cuándo viene información por la entrada estándar, en este caso, el teclado, el primer argumento designa el descriptor de fichero que queremos controlar, el segundo el timeout (el tiempo que vamos a esperar mientras vienen datos o no vienen, y por último GloveBase::SELECT_READ define que estamos esperando una lectura. Si todo va bien, devolverá 0, -1 significa que ha habido un fallo del sistema, y -2 significa que ha transcurrido el timeout

Lectura de datos con operadores

Como antes, dije que la lectura la podemos preparar también con operadores, al más puro estilo C++, es más podemos definir parámetros que afectarán a la lectura tal y como podemos cambiar la precisión de un double con iomanip.
Por ejemplo podemos hacer:

1
2
   std::string data;
   g >> GloveBase::set_read_once(true) >> data;

Y estamos definiendo el argumento deseado de g.receive() directamente. Esmás, podemos utilizar los siguientes manipuladores de entrada:

  • set_read_once(bool): Como antes, definimos que sólo queremos leer una vez y devolver el dato.
  • set_timeout_when_data(bool): Definimos si queremos devolver un timeout cuando ya hemos leído datos (sólo válido si set_read_once(false) antes. Es decir, el hecho de devolver un timeout puede depender de una lectura anterior.
  • set_enable_input_filters(bool): Definimos que queremos activar los filtros a los datos de entrada. Veremos más adelante que podemos pasar todos los datos entrantes por un callback automáticamente.
  • set_timeout(double): Sobreescribe el valor de timeout de la lectura actual.
  • set_exception_on_timeout(bool): Define si queremos lanzar una excepción o no cuando se produzca un timeout
  • set_exception_on_disconnection(bool): Define si queremos lanzar una excepción o no cuando se produzca desconexión del host de destino.

Filtros de entrada

Podemos hacer que todos los mensajes entrantes pasen por un callback que los transforme, de la siguiente manera:

1
2
3
4
std::string FiltroDeEntrada(const std::string& s)
{
  return "ENTRADA FILTRADA -- "+s+"---";
}

y luego con nuestro objeto (g):

1
      g.add_filter(Glove::FILTER_INPUT, "myFilter", FiltroDeEntrada);

Donde myFilter es el nombre del filtro (para poder gestionarlo luego), y FiltroDeEntrada nuestra función que gestiona el filtro. Es más podemos añadir varios filtros, si hacemos:

1
2
      g.add_filter(Glove::FILTER_INPUT, "trim", Trim);
      g.add_filter(Glove::FILTER_INPUT, "myFilter", FiltroDeEntrada);

haremos primero el Trim y luego el nuevo filtro que hemos creado.

Filtros de salida

De la misma manera que los filtros de entrada, podemos configurar filtros de salida, así todo mensaje que enviemos pasará por alguna(s) función(es) antes de salir:

1
2
3
4
std::string FiltroDeSalida(const std::string& s)
{
  return "VA A SALIR ESTO -- "+s+"---";
}

y desde nuestro main():

1
      g.add_filter(Glove::FILTER_OUTPUT, "outFilter", FiltroDeSalida);

Seguridad

Vamos a correr el ejemplo sobre un protocolo seguro, y ya podemos crear un chat simple entre dos ordenadores a través de Internet sin miedo a que nos lean los mensajes. Lo primero es crear el certificado:

$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

Rellenamos los datos del certificado (no pide nada raro, en el Common Name, podemos escribir simplemente “server” o lo que queramos. Ahora sí, tenemos que hacer que nuestro programa acepte conexiones seguras (es más, copio el programa completo, con filtros y todo):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "glove.hpp"
#include <iostream>

using namespace std;

const std::string WHITESPACE = " \n\r\t";

std::string TrimLeft(const std::string& s)
{
    size_t startpos = s.find_first_not_of(WHITESPACE);
    return (startpos == std::string::npos) ? "" : s.substr(startpos);
}

std::string TrimRight(const std::string& s)
{
    size_t endpos = s.find_last_not_of(WHITESPACE);
    return (endpos == std::string::npos) ? "" : s.substr(0, endpos+1);
}

std::string Trim(const std::string& s)
{
    return TrimRight(TrimLeft(s));
}

std::string FiltroDeSalida(const std::string& s)
{
  return "VA A SALIR ESTO -- "+s+"---\n";
}

std::string FiltroDeEntrada(const std::string& s)
{
  return "ENTRADA FILTRADA -- "+s+"---\n";
}

int main(int argc, char *argv[])
{
  Glove g;
  try
    {
      cout << "Buffer: "<< g.buffer_size(16386)<<endl;
      //      g.timeout_when_data(false);
      g.timeout(0.01);
      g.remove_exceptions(Glove::EXCEPTION_TIMEOUT);
      g.add_filter(Glove::FILTER_OUTPUT, "outFilter", FiltroDeSalida);
      g.add_filter(Glove::FILTER_INPUT, "trim", Trim);
      g.add_filter(Glove::FILTER_INPUT, "myFilter", FiltroDeEntrada);
      g.connect(argv[1], 50000, -1, GLOVE_DEFAULT_DOMAIN, true);
      g<<"Hello, tell me something..."<<endl;
      /* std::string data = g.receive(-1, true); */
      std::string data;
      g >> GloveBase::set_read_once(true) >> data;
      while (1)
    {
      if (!data.empty())
        {
          if (data.substr(0,3)=="FIN")
        break;
          else
        {
          data = Trim (data);
          cout << "Server sent: "<<data<<endl;
          g<< "Hi: "<<data<<" for you.";
        }
        }

      if (GloveBase::select(0, 0, GloveBase::SELECT_READ) == 0)
        {
          std::string input;
          getline(cin, input);
          g<<input;
        }

      g >> GloveBase::set_read_once(true) >> data;
    }
      g.disconnect();
    }
  catch (GloveException &e)
    {
      cout << "Exception: "<<e.what() << endl;
    }

  return 0;
}

Ahora, compilaremos example5 de la siguiente manera:

$ g++ -o example5 example5.cc glove.cpp -lssl -lcrypto -std=c++11

Cargaremos el servidor, en lugar de netcat usaremos openssl para aprovechar el certificado anterior.

$ openssl s_server -key key.pem -cert cert.pem -accept 5000

Como véis para activar SSL sobre la conexión sólo ha hecho falta activar poner algunos argumentos a connect. El formato general es:
connect(const std :: string & host, const int port, double timeout, int domain, int secure)
donde,

  • timeout lo podemos poner a -1 para dejar el timeout por defecto
  • domain apuntar a GLOVE_DEFAULT_DOMAIN que por defecto es AF_INET (AF_INET6 también vale)
  • secure, podemos ponerlo a true para establecer una conexión segura

Las conexiones seguras sólo serán posibles si Glove se ha configurado con soporte para ello. Si lo compilamos con dicho soporte podremos hacer conexiones seguras e inseguras, pero si lo configuramos sin él, sólo podremos hacer conexiones inseguras.

Foto: Splitshire

También podría interesarte....

Leave a Reply