Poesía Binaria

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


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:

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

Asimismo los opcodes pueden ser:

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:

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:

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

Un ejemplo práctico: echo


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:

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,

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:

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:

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