Poesía Binaria

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

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:

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:

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,

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....