Publi

¿Cómo crear un chat utilizando WebSockets en C++? Y no morir en el intento

websockets_en_cpp
Los WebSockets proporcionan un canal bidireccional entre el servidor y el navegador y nos permiten crear aplicaciones aún más dinámicas y rápidas. Hace unas semanas vimos cómo funcionan los WebSockets por dentro. En este post vamos a ver una implementación de los mismos en C++, en realidad la parte de navegador como habréis imaginado será Javascript, HTML y CSS, como siempre; será la parte de servidor la que programemos en C++.

Para no hacer este desarrollo desde cero, he utilizado Glove, una biblioteca con la que podemos crear rápidamente un servidor web en C++ con muchas posibilidades. Aunque para analizar un poco las tripas de los WebSockets, vamos a analizar parte del código utilizado dentro de Glove, vamos esto no tendremos que hacerlo para nuestra aplicación, pero nos viene bien para entender cómo funciona. En principio, tenemos tres clases:

  1. WebSocketHandler: encargada de enviar y recibir frames al cliente.
  2. WebSocketFrame: encargada de leer un stream de datos proveniente de un frame y obtener la información del mismo (usado cuando recibamos información). Así como crear el stream de datos de un frame a partir de la información que vamos a enviar (usado para enviar).
  3. WebSocketData: La información por WebSockets puede estar fragmentada, por lo que un dato serán muchos frames.

Para ello, el código utilizado es el siguiente:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#pragma once

#include <string>
#include <chrono>
#include "glove.hpp"

class GloveWebSocketFrame
{
public:
    GloveWebSocketFrame(std::string& data);
    GloveWebSocketFrame(unsigned char opcode, std::string& data, bool fin=true, bool masked=true);
   
    std::string& data()
    {
        return _data;
    }

    bool fin()
    {
        return _fin;
    }

    bool masked()
    {
        return _masked;
    }
   
    bool error();
    bool iscontrol();
    bool isdata();
   
    std::string raw();

    unsigned short opcode() const
    {
        return _opcode;
    }
    static const unsigned char TYPE_CONT = 0x0;
    static const unsigned char TYPE_TEXT = 0x1;
    static const unsigned char TYPE_BIN = 0x2;
    static const unsigned char TYPE_CLOSE = 0x8;
    static const unsigned char TYPE_PING = 0x9;
    static const unsigned char TYPE_PONG = 0xa;
    /* All reserved non-control 0x3-0x7 and control 0xb-0xf
         are considered errors and cannot be handled */

    static const unsigned char TYPE_ERROR = 0xf;
   
private:
    std::string _data;
    unsigned short _opcode;
    uint64_t payloadLen;
    bool frameError;
    bool _masked;
    bool _fin;
};

class GloveWebSocketData
{
public:
    GloveWebSocketData();

    void update(GloveWebSocketFrame& frame);

    unsigned char type()
    {
        return dataType;
    }
   
    std::string& data()
    {
        return _data;
    }

    std::string::size_type length()
    {
        return _data.length();
    }

    bool empty()
    {
        return _data.empty();
    }

    static const unsigned char INVALID_DATATYPE = 0xff;

private:
    /* Clears data when something comes */
    bool clearWhenData;
    unsigned char dataType;
    std::string _data;

};

class GloveWebSocketHandler
{
public:
    GloveWebSocketHandler(Glove::Client& client, uint64_t fragmentation);
    /* Send pong */
    bool pong(GloveWebSocketFrame& frame);
    /* Receive pong */
    bool pong();
    void ping(std::string data ="", std::function<void(GloveWebSocketHandler&)> callback=nullptr);
    void close(GloveWebSocketFrame& frame);
    void close(uint16_t closeCode, std::string closeMessage);
    unsigned clientId()
    {
        return _client.id();
    }
   
    unsigned char type ()
    {
        return _type;
    }

    double latency()
    {
        return _latency;   
    }
   
    unsigned char type (unsigned char type)
    {
        _type = type;
        return _type;
    }

    uint64_t fragmentation()
    {
        return _fragmentation;
    }

    uint64_t fragmentation(uint64_t val)
    {
        _fragmentation = val;
        return _fragmentation;
    }

    int send(std::string data, unsigned char type=0);
private:
    std::vector <GloveWebSocketFrame> divideMessage(unsigned char opcode, std::string& data, bool masked=true);
    Glove::Client& _client;
    unsigned char _type;
    uint64_t _fragmentation;
    bool waitingForPong;
    std::function<void(GloveWebSocketHandler&)> pongCallback;
    double _latency;
    std::chrono::time_point<std::chrono::steady_clock> temp_start;
};

WebSocketFrame

Podremos generar un WebSocketFrame desde un string con los datos en bruto o a partir de la información necesaria como el opcode, datos, fin (si es el último frame de una serie y si está enmascarado). En definitiva. Podemos utilizar los constructores:

  • GloveWebSocketFrame(std::string& data);
  • GloveWebSocketFrame(unsigned char opcode, std::string& data, bool fin=true, bool masked=true);

Y tras ello, podemos consultar información de dicho frame:

  • std::string& data() : Para obtener la información de dicho frame (texto o binaria)
  • bool fin() : Para saber si es un último frame de serie
  • bool masked() : Para saber si viene enmascarado.
  • bool error() : ¿Ha habido algún fallo leyendo o generando el frame?
  • bool iscontrol() : ¿Es un frame de control?
  • bool isdata() : ¿Es un frame de datos? Será de datos si no es de control, pero tenemos otra función para hacer el sistema más humano
  • std::string raw() : Devuelve los datos que serán enviados o que han sido recibidos por red
  • unsigned short opcode() const : Devuelve el opcode de dicho frame.

Asimismo los opcodes pueden ser:

  • TYPE_CONT: El frame es continuación de una secuencia
  • TYPE_TEXT: Es un frame de texto
  • TYPE_BIN: Es un frame binario
  • TYPE_CLOSE: Petición o respuesta de cierre de sesión
  • TYPE_PING: Ping, para verificar respuesta de la otra parte.
  • TYPE_PONG: Respuesta de Ping
  • TYPE_ERROR: Este tipo no está en la especificación, pero lo usamos en la biblioteca para indicar que ha habido un problema con el frame.

Más o menos esto nos servirá para hacernos una idea de cómo funciona esto. Aunque vamos a ver qué métodos tenemos para que nuestro programa utilice WebSockets a modo de servidor.

WebSocketData

Una clase mucho más pequeña, en la que juntamos los datos de varios Frames. El objetivo es que los Frames se vayan destruyendo a medida que se reciban y sus datos se añadan a una instancia de esta clase. Con el futuro se podrá optimizar en memoria ya que los tamaños que pueden alcanzar los frames pueden ser desorbitados (vamos, el sistema está preparado para los años venideros). Para utilizar esta clase tenemos los métodos:

  • GloveWebSocketData(): Constructor que inicializa atributos de la clase.
  • void update(GloveWebSocketFrame& frame): Añade un nuevo frame
  • unsigned char type(): Indica el tipo de datos que se han recibido (coinciden con el opcode del primer frame).
  • std::string& data(): Devuelve los datos que se han recibido.
  • std::string::size_type length(): Tamaño de los datos.
  • bool empty(): ¿Hay datos ahora mismo?

WebSocketHandler

Será la clase con la que interactuaremos directamente en las funciones de envío, recepción y control (inicio/fin de sesión, mantenimiento de conexión, etc) de datos de tipo WebSockets; además, en el ejemplo del chat veremos cómo cada uno de los usuarios conectados tendrá un WebSocketHandler asociado, con el que podremos enviarle mensajes en cualquier momento. Dicha clase tendrá los siguientes métodos:

  • GloveWebSocketHandler(Glove::Client& client, uint64_t fragmentation): Inicializa el objeto. fragmentation indicará un recorte automático de los mensajes estableciendo un tamaño máximo de los frames que envía el sistema (podemos optimizar el sistema tocando este valor). Este objeto lo suele crear automáticamente GloveHttpServer, así que a nosotros se nos entregará siempre una referencia de un objeto de esta clase.
  • void ping(std::string data =””, std::function callback=nullptr): Envía un ping al cliente que tengamos conectado. En el ping podemos enviar también algo de información que teóricamente tiene que venir devuelta (así que tampoco enviemos el Quijote). Además, desde esta función podemos establecer un callback que se llamará cuando recibamos el pong.
  • bool pong(GloveWebSocketFrame& frame): Envía un pong al cliente. Se suele llamar automáticamente por Glove.
  • bool pong(): Recibe un pong del cliente. Cuando se recibe un pong() se puede llamar adicionalmente a un callback que establecemos cuando enviamos el ping.
  • void close(GloveWebSocketFrame& frame): Responde una petición de cierre de sesión
  • void close(uint16_t closeCode, std::string closeMessage): Envía una petición de cierre de sesión. con el código y mensaje que indiquen el por qué.
  • unsigned clientId(): Devuelve el Id del cliente.
  • unsigned char type(): Devuelve el tipo por defecto de frame que se envía (pueden ser frames de tipo texto o binarios, no tendría mucho sentido enviar por defecto pings o close, o incluso pong, porque muchos clientes rechazan la conexión cuando reciben un pong sin venir a cuento.
  • double latency(): Tiempo desde que se envió un ping hasta que se recibió el pong.
  • unsigned char type (unsigned char type): Establece el tipo de frame por defecto.
  • uint64_t fragmentation(): Devuelve el número de bytes máximo de cada frame, es decir fragmentaremos los mensajes en frames de este tamaño.
  • uint64_t fragmentation(uint64_t val): Establecemos el valor de la fragmentación.
  • int send(std::string data, unsigned char type=0): Enviamos datos. Si type es 0, se enviarán del tipo por defecto.

Ya estamos listos para jugar con los WebSockets utilizando Glove en C++.

Un ejemplo práctico: echo

Screenshot 21-11-2016-081158
Pero vamos al lío, a crear algo que podamos utilizar después. Los ejemplos los podemos encontrar en GitHub, y tal vez allí se vean actualizados. Lo primero que vamos a ver será el código C++ de nuestro servidor. Éste se encargará de aceptar las peticiones http:// y ws:// y procesarlas. Primero implementaremos un servidor echo, es decir, todo lo que le mandemos, vamos a reenviarlo. Es algo sencillo, pero nos vale como prueba de concepto (menos de 50 líneas):

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
#include "glovehttpserver.hpp"
#include <iostream>
#include <chrono>
#include <thread>
#include <zlib.h>

void echoEngine(GloveHttpRequest &request, GloveHttpResponse& response)
{
  response << "This is an Echo engine for Web Sockets.\n";
    auto uri = request.getUri().servicehost("ws");
    response << "Try connecting to " << uri <<"/echo/ \n";
}

void echoMessage(GloveWebSocketData& data, GloveWebSocketHandler& ws)
{
    if (data.length()>300)
        ws.send("Message too long");
    else
        ws.send("Echo: "+data.data());
}

bool chatmaintenance(GloveWebSocketHandler& ws)
{
}

int main(int argc, char *argv[])
{
  GloveHttpServer serv(8080, "", 2048);

    std::cout << "Timeout: "<<serv.timeout()<<std::endl;
    std::cout << "Keepalive: "<<serv.keepalive_timeout()<<std::endl;

  serv.addWebSocket("/echo/", echoEngine, nullptr, echoMessage);
  serv.addRoute("/websocket_test.js", std::bind(GloveHttpServer::fileServerFixed, std::placeholders::_1, std::placeholders::_2, "websocket_test.js"));
  serv.addRoute("/websocket_test.css", std::bind(GloveHttpServer::fileServerFixed, std::placeholders::_1, std::placeholders::_2, "websocket_test.css"));
  serv.addRoute("/", std::bind(GloveHttpServer::fileServerFixed, std::placeholders::_1, std::placeholders::_2, "websockets.html"));

  std::cout << "READY"<<std::endl;
  while(1)
    {
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

Como vemos, lo que hacemos es crear el servidor con el constructor de GloveHttpServer, ya que en sí es un servidor HTTP en el puerto 8080 y más tarde añadimos rutas:

  • /echo/ : Será la ruta a la que debemos conectar nuestro WebSocket. Desde aquí se inicia el handshake y se establece la comunicación. Si conectamos desde http a esta dirección se enviará una web estática.
  • /websocket_test.js : Es el archivo Javascript que se envía el cliente. Ya que tenemos un servidor iniciado, éste se encargará de este tipo de cosas también.
  • /websocket_test.css : Es el archivo CSS asociado y que también se envía al cliente.
  • / : Es el directorio principal. Se envía un HTML con un entorno para que el cliente envíe mensajes.

Y, como vemos se utilizarán los métodos addRoute (para rutas HTTP) y addWebSocket (para rutas WS) de la siguiente forma:

1
addRoute(std::string ruta, GloveHttpServer::url_callback callback)

donde,

  • ruta : Indica la dirección del recurso que escuchamos. Es decir, cuando accedamos a dicha ruta, llamaremos a una función que la procese.
  • callback : Será la función que se llamará para generar la salida deseada (enviar una web, añadir un dato, o lo que sea. Los callbacks tienen la forma:
    void (GloveHttpRequest& request, GloveHttpResponse& response)
    donde GloveHttpRequest nos proporciona información sobre la petición y en GloveHttpResponse enviaremos la información sobre la respuesta.

En realidad addRoute puede admitir más argumentos como el virtualhost al que estamos accediendo, número máximo y mínimo de argumentos (por si en la ruta hay variables o los métodos HTTP admitidos. Poco a poco voy documentando.

1
2
3
4
5
6
addWebSocket(std::string ruta,
   GloveHttpServer::url_callback callback,
   GloveHttpServer::ws_accept_callback acceptCallback,
   GloveHttpServer::ws_receive_callback receiveCallback,
   GloveHttpServer::ws_maintenance_callback maintenanceCallback=nullptr,
   GloveHttpServer::ws_maintenance_callback closeCallback=nullptr)

En este caso, tenemos más callbacks que coincidirán con los diferentes eventos que se pueden producir:

  • callback : Es la función que se llamará cuando se acceda por HTTP. Con la misma forma que el callback de addRoute.
  • acceptCallback : Será la función llamada cuando un cliente acabe de establecer una conexión, algo así como un login en el sistema. Tendrá la forma void (GloveHttpRequest& request, GloveWebSocketHandler& ws). Si este callback es nullptr, no haremos nada cuando entre un cliente,
  • receiveCallback : Será la función llamada cuando se recibe un mensaje desde el cliente. Tendrá la forma void (GloveWebSocketData& data, GloveWebSocketHandler& ws). Si este callback es nullptr no haremos nada cuando se reciba un mensaje.
  • maintenanceCallback : Se llamará periódicamente para realizar tareas de mantenimiento sobre un cliente en cuestión. Tendrá la forma void (GloveWebSocketHandler& ws). Tenemos que tener cuidado y no alargar mucho esta función porque si tenemos muchos clientes, puede llegar a tener muchas llamadas simultáneas. Si este callback es nullptr no haremos nada periódicamente.
  • closeCallback : Se llamará cuando un cliente pida cerrar la conexión. Tendrá la misma forma que un callback de mantenimiento. Si este callback es nullptr no haremos nada cuando un cliente cierre la conexión, Glove se encarga de cerrar bien la conexión, pero nada más.

Si miramos la función en el archivo hpp podremos ver que acepta más argumentos, tal y como hace addRoute(), pero con esto, tenemos para montar nuestro servidor de echo con WebSockets. Aunque paso a escribir los archivos CSS, JS y HTML necesarios para ejecutar este ejemplo.

websockets.html:

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
<html>
        <head>
                <meta charset="UTF-8">

                <link rel="stylesheet" media="all" type="text/css" href="websocket_test.css" />
                </style>
        </head>
        <body>
                <div id="page-wrapper" class="waiting">
                        <h1>Glove Echo Demo</h1>
                       
                        <div id="status" class="closed">Disconnected from Server.</div>
                       
                        <ul id="messages"></ul>
                       
                        <form method="post" action="#" id="message-form">
                                <textarea required="" placeholder="Write your message here..." id="message"></textarea>
                                <button type="submit">Send Message</button>
                                <button id="close" type="button">Close Connection</button>
                        </form>
                </div>
                <script src="websocket_test.js" type="text/javascript"></script>
        </body>
        <!-- Demo based on a codepen by Matt West: https://codepen.io/matt-west/pen/tHlBb -->
</html>

websocket_test.js:

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
window.onload = function () {
  var form = document.getElementById('message-form');
  var messageField = document.getElementById('message');
  var messagesList = document.getElementById('messages');
  var socketStatus = document.getElementById('status');
  var closeBtn = document.getElementById('close');
  var socket = new WebSocket('ws://localhost:8080/echo/');

  socket.onerror = function (error) {
    console.log('WebSocket Error: ' + error);
  };
  socket.onopen = function (event) {
        document.getElementById('page-wrapper').className='connected';
    socketStatus.innerHTML = 'Connected to: ws://localhost:8080/echo/';
    socketStatus.className = 'open';
  };
  socket.onmessage = function (event) {
    var message = event.data;
    messagesList.innerHTML += '<li class="received"><span>Received:</span>' + message + '</li>';
        messagesList.scrollTop = messagesList.scrollHeight;    
  };
  socket.onclose = function (event) {
        document.getElementById('page-wrapper').className='disconnected';
    socketStatus.innerHTML = 'Disconnected from WebSocket.';
    socketStatus.className = 'closed';
  };
  form.onsubmit = function (e) {
    e.preventDefault();
    var message = messageField.value;
    socket.send(message);
    messagesList.innerHTML += '<li class="sent"><span>Sent:</span>' + message + '</li>';
        messagesList.scrollTop = messagesList.scrollHeight;
    messageField.value = '';
    return false;
  };
  closeBtn.onclick = function (e) {
    e.preventDefault();
    socket.close();
    return false;
  };
};

websocket_test.css:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
*, *:before, *:after {
  box-sizing: border-box;
}

html {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 100%;
  background: #334;
}

#page-wrapper {
  width: 850px;
  background: #FFF;
  padding: 1em;
  margin: 1em auto;
  box-shadow: 0 2px 10px rgba(0,0,0,0.8);
}

#page-wrapper.waiting {
  border-top: 5px solid #696969;
}

#page-wrapper.connected {
  border-top: 5px solid #69c773;
}

#page-wrapper.disconnected {
  border-top: 5px solid #c76973;
}

h1 {
    margin-top: 0;
}

#status {
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

.open {
  color: green;
}

.closed {
  color: red;
}


ul {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.95rem;
}

ul li {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid #EEE;
}

ul li:first-child {
  border-top: 1px solid #EEE;
}

ul li span {
  display: inline-block;
  width: 90px;
  font-weight: bold;
  color: #999;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.sent {
  background-color: #F7F7F7;
}

.received {}

ul#messages {
        max-height:300px;
        overflow-y : auto;
}

#message-form {
  margin-top: 1.5rem;
}

textarea {
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
  border: 1px solid #D9D9D9;
  border-radius: 3px;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
  min-height: 100px;
  margin-bottom: 1rem;
}

button {
  display: inline-block;
  border-radius: 3px;
  border: none;
  font-size: 0.9rem;
  padding: 0.6rem 1em;
  color: white;
  margin: 0 0.25rem;
  text-align: center;
  background: #BABABA;
  border-bottom: 1px solid #999;
}

button[type="submit"] {
  background: #86b32d;
  border-bottom: 1px solid #5d7d1f;
}

button:hover {
  opacity: 0.75;
  cursor: pointer;
}

Aquí tenemos todo lo necesario para ejecutar el ejemplo y jugar con él. Para compilar el ejemplo podremos hacerlo con

g++ -g -o webserver webserver.cpp glove.cpp glovehttpserver.cpp glovewebsockets.cpp glovehttpcommon.cpp glovecoding.cpp -std=c++11 -lpthread -lcrypto -lssl -lz

Si queremos soporte SSL (tendremos que instalar libssl-dev o un paquete similar en nuestra distribución). En el siguiente ejemplo cuento cómo activar SSL y por tanto deberemos acceder con wss:// (con esto, estaremos utilizando un protocolo seguro). También necesitaremos zlib si queremos habilitar la compresión. O:
g++ -g -o webserver webserver.cpp glovehttpserver.cpp glove.o -std=c++11 -lpthread -lz -DENABLE_OPENSSL=0

Si no queremos dicho soporte. O incluso podemos indicar -DENABLE_COMPRESSION=0 si no queremos habilitar compresión de la salida, quitando a la vez -lz.

Otro ejemplo: nuestro propio chat en C++

Pero la gracia de esto está en que podemos montar un servidor de chat con WebSockets en unas 200 líneas de C++ con cierto control de usuarios online.

Primero crearemos una clase SimpleChat que sea capaz de manejar los eventos que puedan ocurrir como la entrada y salida de usuarios, establecimiento de su nickname o alias y el envío y recepción de mensajes. Esta clase tendrá los siguientes métodos:

  • void login(GloveHttpRequest& request, GloveWebSocketHandler& ws) : llamada cuando entra un usuario nuevo.
  • void message(GloveWebSocketData& data, GloveWebSocketHandler& ws) : llamada cuando se recibe un mensaje. Puede haber mensajes de comando, precedidos de /, por ahora sólo vale el comando /name que cambia el nombre del usuario.
  • void setName(GloveWebSocketHandler& ws, std::string name) : comprueba que el nombre del usuario no esté siendo utilizado y asocia un nombre con un identificador interno de usuario (ws.clientId()). Con esto, siempre que un usuario envíe un mensaje, el mensaje aparecerá junto con el nombre que el usuario haya elegido.
  • bool nameIsUsed(std::string& name) : Verifica que el nombre especificado está siendo usado.
  • std::string userName(GloveWebSocketHandler& ws) : Devuelve el nombre de usuario asociado con un Id de cliente.
  • bool quit(GloveWebSocketHandler& ws) : Un usuario ha abandonado el chat. Normalmente los navegadores cierran bien la conexión cuando el cliente se marcha. Si no, cuando ocurre un timeout en el socket también se cierra automáticamente la conexión.

Paso a poner el código fuente:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#include "glovehttpserver.hpp"
#include "glovewebsockets.hpp"
#include <iostream>
#include <chrono>
#include <thread>

bool secure = true;

class SimpleChat
{
public:
    SimpleChat() {}
    ~SimpleChat() {}

    void login(GloveHttpRequest &request, GloveWebSocketHandler& ws)
    {
        users.insert({ws.clientId(), { false, "", ws }});
    }

    void message(GloveWebSocketData& data, GloveWebSocketHandler& ws)
    {
        if (data.type() == GloveWebSocketData::INVALID_DATATYPE)
            return;                                     /* Invalid data type */
        if (data.empty())
            return;                                     /* No message. No action */
       
        if (data.length()>300)
            {
                ws.send("!1!Message too big!");
                return;
            }
        auto _data = data.data();
       
        if (_data[0]=='/')
            {
                auto space=_data.find(' ');
                auto word = (space!=std::string::npos)?_data.substr(1, space-1):_data;
                auto rest = (space!=std::string::npos)?_data.substr(space+1):"";

                if (word == "name")
                    setName(ws, rest);
                else
                    ws.send("!0!Invalid command "+word);
            }
        else
            {
                sendMessage(ws, _data);
            }
    }
   
    void setName(GloveWebSocketHandler& ws, std::string name)
    {
        if (name.length()>15)
            name = name.substr(0, 15); /* cut the name! */

        name = trim(name);
        if ( (name.empty()) || (name.find('@') != std::string::npos) )
            {
                ws.send("!4!Invalid user name");
                return;
            }
       
        auto current = users.find(ws.clientId());
        if (name == current->second.username)           /* This MUST exist */
            return;                                     /* No name change */

        if (nameIsUsed(name))
            {
                ws.send("!2!Your name is being used by other user");
                return;
            }

        current->second.username = name;
        if (!current->second.fullyLoggedIn)
            {
                broadcast("$User "+name+" has logged in");     
            }
        current->second.fullyLoggedIn = true;
        ws.send("$Name changed successfully");
    }

    bool nameIsUsed(std::string& name)
    {
        for (auto u : users)
            {
                if (u.second.username==name)
                    return true;
            }
        return false;
    }

    std::string userName(GloveWebSocketHandler& ws)
    {
        auto current = users.find(ws.clientId());
        return current->second.username;
    }
   
    void sendMessage(GloveWebSocketHandler& ws, std::string& message)
    {
        auto username = userName(ws);
        if (username.empty())
            {
                ws.send("!5!User not identified");
                return;            
            }
       
        broadcast(username+"@"+message, ws.clientId());
    }

    bool quit(GloveWebSocketHandler& ws)
    {
        auto user = users.find(ws.clientId());
        std::string username = user->second.username;
       
        users.erase(user);
        if (!username.empty())
            broadcast("$User "+username+" has quit");      
    }
   
private:
    void broadcast(std::string message, unsigned exclude=0)
    {
        for (auto u : users)
            {
                if ( (exclude) && (u.first == exclude) )
                    continue;
               
                if (u.second.fullyLoggedIn)
                    u.second.handler.send(message);
            }
    }
   
    struct User
    {
        bool fullyLoggedIn;
        std::string username;
        GloveWebSocketHandler& handler;
    };
    std::map < unsigned, User > users;
};

void getChatJs(GloveHttpRequest& request, GloveHttpResponse& response)
{
    std::string filename = "wschat.js";
    std::string extension = fileExtension(filename);
  std::string fileContents = extractFile(filename.c_str());
  if (fileContents.empty())
    {
      response.code(GloveHttpResponseCode::NOT_FOUND);
      return;
    }
    else
        {
            fileContents = string_replace(fileContents, {
                    { "%CHATURL%", request.getUri().servicehost((secure)?"wss":"ws")+"/chat/" }
                });
        }
  response.contentType(GloveHttpServer::getMimeType(extension));
  response << fileContents;
}

void chatEngine(GloveHttpRequest &request, GloveHttpResponse& response)
{
  response << "This is a Chat engine for Web Sockets in C++.\n";
    auto uri = request.getUri().servicehost();
    response << "Try going to " << uri <<"\n";
}

int main(int argc, char *argv[])
{
  GloveHttpServer serv(8080, "", 2048, GLOVE_DEFAULT_BACKLOG_QUEUE, GLOVE_DEFAULT_DOMAIN, GLOVE_DEFAULT_MAX_CLIENTS, GLOVE_DEFAULT_TIMEOUT, GLOVEHTTP_KEEPALIVE_DEFAULT_TIMEOUT, Glove::ENABLE_SSL, "sslserverchain.pem", "sslserver.key");
    SimpleChat chat;
    std::cout << "Timeout: "<<serv.timeout()<<std::endl;
    std::cout << "Keepalive: "<<serv.keepalive_timeout()<<std::endl;
   
  serv.addWebSocket("/chat/", chatEngine,
                                        std::bind(&SimpleChat::login, &chat, std::placeholders::_1, std::placeholders::_2),
                                        std::bind(&SimpleChat::message, &chat, std::placeholders::_1, std::placeholders::_2),
                                        nullptr,
                                        std::bind(&SimpleChat::quit, &chat, std::placeholders::_1));
  serv.addRoute("/wschat.js", getChatJs);
  serv.addRoute("/wschat.css", std::bind(GloveHttpServer::fileServerFixed, std::placeholders::_1, std::placeholders::_2, "wschat.css"));
  serv.addRoute("/", std::bind(GloveHttpServer::fileServerFixed, std::placeholders::_1, std::placeholders::_2, "wschat.html"));

  std::cout << "READY"<<std::endl;
  while(1)
    {
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

Como curiosidad, indicar que la función getChatJs() que devuelve el archivo Javascript de nuestro chat es dinámica. Es decir, aunque tenemos un archivo wschat.js que contiene la información. Dentro de éste hay una palabra clave %CHATURL% que indica la dirección donde el chat está instalado y será dinámica dependiendo de si utilizamos ws:// o wss:// ; esto nos puede dar mucho juego, aunque será más difícil proporcionar caché a nuestros usuarios.

Aquí tenemos el resto de archivos.

wschat.html:

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
<html>
        <head>
                <meta charset="UTF-8">

                <link rel="stylesheet" media="all" type="text/css" href="wschat.css" />
                </style>
        </head>
        <body>
                <div id="page-wrapper" class="waiting">
                        <h1>Glove Webchat Demo</h1>
                       
                        <div id="status" class="closed">Disconnected from Server.</div>
                       
                        <ul id="messages"></ul>
                       
                        <form method="post" action="#" id="message-form">
                                <textarea required="" placeholder="Write your message here..." id="message"></textarea>
                                <button type="submit">Send Message</button>
                                <button id="close" type="button">Close Connection</button>
                        </form>
                </div>
                <script src="wschat.js" type="text/javascript"></script>
        </body>
        <!-- Demo based on a codepen by Matt West: https://codepen.io/matt-west/pen/tHlBb -->
</html>

wschat.js (cuidado con %CHATURL% que es nuestra palabra clave):

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
window.onload = function () {
  var form = document.getElementById('message-form');
  var messageField = document.getElementById('message');
  var messagesList = document.getElementById('messages');
  var socketStatus = document.getElementById('status');
  var closeBtn = document.getElementById('close');
  var socket = new WebSocket('%CHATURL%');
    var hasName = false;

    var askName = function () {
        do
        {
            var name = prompt ("Please enter your name");
            messageField.focus();
        } while (name == null);

        socket.send("/name "+name);
    }

    var sendMessage = function () {
    var message = messageField.value;
    socket.send(message);
        if (message[0] != '/')
        {
            messagesList.innerHTML += '<li class="sent"><span>Sent:</span>' + message + '</li>';
            messagesList.scrollTop = messagesList.scrollHeight;
        }
        messageField.value = '';
    }
   
  socket.onerror = function (error) {
    console.log('WebSocket Error: ' + error);
  };
   
  socket.onopen = function (event) {
        document.getElementById('page-wrapper').className='connected';
    socketStatus.innerHTML = 'Connected to: %CHATURL%';
    socketStatus.className = 'open';
        askName();
  };
   
  socket.onmessage = function (event) {
    var message = event.data;
        if (message[0] == '!')
        {
            var parsed = message.split('!');
            if (parsed.length != 3)
                messagesList.innerHTML += '<li class="error">Unexpected error from server: '+message+'</li>';
            else {
                messagesList.innerHTML += '<li class="error">Error: '+parsed[2]+'</li>';
                if ( ( (parsed[1] == 2) || (parsed[1] == 4)) && (!hasName) )
                    askName();
            }
        }
        else if (message[0] == '$') {
            hasName=true;
            messagesList.innerHTML += '<li class="success">'+message.substr(1)+'</li>';        
        }
        else {
            var sep = message.indexOf('@');
            if (sep == -1)
                messagesList.innerHTML += '<li class="error">Error: Unexpected message</li>';
            else {
                var user = message.substr(0, sep);
                var msg = message.substr(sep+1);
                messagesList.innerHTML += '<li class="received"><span>'+user+'</span>' + msg + '</li>';
            }
        }
        messagesList.scrollTop = messagesList.scrollHeight;

  };
  socket.onclose = function (event) {
        document.getElementById('page-wrapper').className='disconnected';
    socketStatus.innerHTML = 'Disconnected from WebSocket.';
    socketStatus.className = 'closed';
  };

    messageField.onkeypress = function (e) {
        if (e.key=='Enter')
        {
            e.preventDefault();
            sendMessage();
            return false;
        }
        else
            return true;
    }

  form.onsubmit = function (e) {
    e.preventDefault();
        sendMessage();
    return false;
  };
  closeBtn.onclick = function (e) {
    e.preventDefault();
    socket.close();
    return false;
  };
};

wschat.css:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
*, *:before, *:after {
  box-sizing: border-box;
}

html {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 100%;
  background: #334;
}

#page-wrapper {
  width: 850px;
  background: #FFF;
  padding: 1em;
  margin: 1em auto;
  box-shadow: 0 2px 10px rgba(0,0,0,0.8);
}

#page-wrapper.waiting {
  border-top: 5px solid #696969;
}

#page-wrapper.connected {
  border-top: 5px solid #69c773;
}

#page-wrapper.disconnected {
  border-top: 5px solid #c76973;
}

h1 {
    margin-top: 0;
}

#status {
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

.open {
  color: green;
}

.closed {
  color: red;
}


ul {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.95rem;
}

ul li {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid #EEE;
}

ul li:first-child {
  border-top: 1px solid #EEE;
}

ul li span {
  display: inline-block;
  width: 150px;
  font-weight: bold;
  color: #999;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sent {
  background-color: #F7F7F7;
}

.success {
  background-color: #11F711;
}

.error {
  background-color: #F71111;
}

.received {}

ul#messages {
        height:300px;
        overflow-y : auto;
}

#message-form {
  margin-top: 1.5rem;
}

textarea {
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
  border: 1px solid #D9D9D9;
  border-radius: 3px;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
  min-height: 100px;
  margin-bottom: 1rem;
}

button {
  display: inline-block;
  border-radius: 3px;
  border: none;
  font-size: 0.9rem;
  padding: 0.6rem 1em;
  color: white;
  margin: 0 0.25rem;
  text-align: center;
  background: #BABABA;
  border-bottom: 1px solid #999;
}

button[type="submit"] {
  background: #86b32d;
  border-bottom: 1px solid #5d7d1f;
}

button:hover {
  opacity: 0.75;
  cursor: pointer;
}

Para compilar:

g++ -o wschat glovewschat.cc glove.cpp glovehttpserver.cpp glovewebsockets.cpp glovehttpcommon.cpp glovecoding.cpp -std=c++11 -lpthread -lcrypto -lssl -lz

Proxy con Apache

Podéis poner este programa detrás de Apache, por ejemplo utilizando el módulo proxy_wstunnel y utilizando:

1
2
RewriteCond %{QUERY_STRING} transport=websocket    [NC]
  RewriteRule /(.*)           ws://localhost:8080/$1 [P,L]

o

1
2
3
 <Location "/chat/">
    ProxyPass "ws://localhost:9'9'/"
  </Location>

en la configuración del VirtualHost.

¿Cómo usas los WebSockets?

Y tú, ¿cómo usas los WebSockets en tus aplicaciones?
Foto original: Marcus Dall Col

También podría interesarte....

There are 5 comments left Ir a comentario

  1. Pingback: ¿Cómo crear un chat utilizando WebSockets en C++? Y no morir en el intento | PlanetaLibre /

  2. Davis Nunez /
    Usando Google Chrome Google Chrome 54.0.2840.99 en Windows Windows NT

    Hola, estoy muy interesado en esto,
    En la parte del server como se haria con php?
    Saludos y genial publicacion!

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 50.0 en Ubuntu Linux Ubuntu Linux

      Gracias Davis ! Por ejemplo para PHP hay proyectos como http://socketo.me/ o https://github.com/nekudo/php-websocket que te pueden ayudar. Échales un ojo y me cuentas 🙂

  3. Mark /
    Usando Google Chrome Google Chrome 63.0.3239.132 en Windows Windows 7

    Chacho, me sorprende que tengas pocos comentarios. La verdad es que el contenido que tienes en la web está muy bien (he mirado alguna que otra cosilla).

    Me hace suponer que la gente mira el contenido, y no se digna a comentar. En fin, solo quería plasmar mi sorpresa al ver tu web. Sigue así ^^.

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 57.0 en Linux Linux

      Gracias por el comentario Mark! Pues yo no me he comido a nadie (aún), jejeje.

      Me hace mucha ilusión recibir comentarios como el tuyo. En ocasiones miro a ver si sigue funcionando el sistema de comentarios, lo mismo el sistema anti spam está muy fuerte, pero nada 🙂 Yo creo que algunos posts sí que los miran, de hecho a veces se comparten por e-mail, por whatsapp y por telegram.

Leave a Reply