Desenvolvimento

16 mar, 2015

Compartilhando a conexão WebSocket com instâncias da aplicação HTML5 com SharedWorker

Publicidade

Eu realmente gosto de WebSockets. Hoje vamos falar sobre algo relacionado a WebSockets. Deixe-me explicar um pouco.

Imagine que vamos construir um aplicativo web WebSockets. Isso significa que precisamos nos conectar ao servidor WebSockets quando iniciarmos o aplicativo. Se nossa aplicação for uma single-page application, vamos criar um socket por aplicação, mas o que acontece se abrirmos três guias com a aplicação dentro do navegador? A resposta é simples, vamos criar três sockets. Além disso, se recarregarmos uma guia (um refresh completo), vamos desligar o nosso socket e nos conectar novamente. Talvez possamos lidar com isso, mas podemos contornar facilmente essa situação de desconexão-conexão com um recurso do HTML5 chamado SharedWorkers.

O Web Workers nos permite executar o processo JavaScript em background. Também podemos criar SharedWorkers. SharedWorkers podem ser compartilhados dentro da nossa sessão de browser. Isso que significa que é possível colocar o nosso servidor WebSocket dentro do SharedWorker, e se abrirmos várias abas com o nosso navegador só precisamos de um Socket (um socket por sessão em vez de um socket por guia).

Já escrevi uma biblioteca simples chamada gio para executar essa operação. gio usa socket.io para criar WebSockets. WebWorker é um novo recurso do HTML5, e ele precisa de um navegador moderno. O Socket.io também trabalha com navegadores antigos. Ele verifica se WebWorkers estão disponíveis e, se não estiverem, o gio cria uma conexão WebSocket em vez de usar um WebWorker para incluir os WebSockets.

Podemos observar um simples vídeo para ver como funciona. Nele, podemos ver como sockets são criados. Apenas um socket é criado, mesmo que a gente abra mais de uma guia no nosso browser. Mas se abrirmos uma nova sessão (uma sessão anônima, por exemplo), um novo socket é criado.

Aqui podemos ver o código SharedWorker:

"use strict";
 
importScripts('socket.io.js');
 
var socket = io(self.name),
    ports = [];
 
addEventListener('connect', function (event) {
    var port = event.ports[0];
    ports.push(port);
    port.start();
 
    port.addEventListener("message", function (event) {
        for (var i = 0; i < event.data.events.length; ++i) {
            var eventName = event.data.events[i];
 
            socket.on(event.data.events[i], function (e) {
                port.postMessage({type: eventName, message: e});
            });
        }
    });
});
 
socket.on('connect', function () {
    for (var i = 0; i < ports.length; i++) {
        ports[i].postMessage({type: '_connect'});
    }
});
 
socket.on('disconnect', function () {
    for (var i = 0; i < ports.length; i++) {
        ports[i].postMessage({type: '_disconnect'});
    }
});

E aqui podemos ver o código-fonte do gio:

var gio = function (uri, onConnect, onDisConnect) {
    "use strict";
    var worker, onError, workerUri, events = {};
 
    function getKeys(obj) {
        var keys = [];
 
        for (var i in obj) {
            if (obj.hasOwnProperty(i)) {
                keys.push(i);
            }
        }
 
        return keys;
    }
 
    function onMessage(type, message) {
        switch (type) {
            case '_connect':
                if (onConnect) onConnect();
                break;
            case '_disconnect':
                if (onDisConnect) onDisConnect();
                break;
            default:
                if (events[type]) events[type](message);
        }
    }
 
    function startWorker() {
        worker = new SharedWorker(workerUri, uri);
        worker.port.addEventListener("message", function (event) {
            onMessage(event.data.type, event.data.message);
 
        }, false);
 
        worker.onerror = function (evt) {
            if (onError) onError(evt);
        };
 
        worker.port.start();
        worker.port.postMessage({events: getKeys(events)});
    }
 
    function startSocketIo() {
        var socket = io(uri);
        socket.on('connect', function () {
            if (onConnect) onConnect();
        });
 
        socket.on('disconnect', function () {
            if (onDisConnect) onDisConnect();
        });
 
        for (var eventName in events) {
            if (events.hasOwnProperty(eventName)) {
                socket.on(eventName, socketOnEventHandler(eventName));
            }
        }
    }
 
    function socketOnEventHandler(eventName) {
        return function (e) {
            onMessage(eventName, e);
        };
    }
 
    return {
        registerEvent: function (eventName, callback) {
            events[eventName] = callback;
        },
 
        start: function () {
            if (!SharedWorker) {
                startSocketIo();
            } else {
                startWorker();
            }
        },
 
        onError: function (cbk) {
            onError = cbk;
        },
 
        setWorker: function (uri) {
            workerUri = uri;
        }
    };
};

E aqui o código do aplicativo:

(function (gio) {
    "use strict";
 
    var onConnect = function () {
        console.log("connected!");
    };
 
    var onDisConnect = function () {
        console.log("disconnect!");
    };
 
    var ws = gio("http://localhost:8080", onConnect, onDisConnect);
    ws.setWorker("sharedWorker.js");
 
    ws.registerEvent("message", function (data) {
        console.log("message", data);
    });
 
    ws.onError(function (data) {
        console.log("error", data);
    });
 
    ws.start();
}(gio));

Eu também criei um servidor WebSocket simples com socket.io. Nesse pequeno servidor, há uma função setInterval transmitindo uma mensagem por segundo para todos os clientes para ver o aplicativo funcionando.

var io, connectedSockets;
 
io = require('socket.io').listen(8080);
connectedSockets = 0;
 
io.sockets.on('connection', function (socket) {
    connectedSockets++;
    console.log("Socket connected! Conected sockets:", connectedSockets);
 
    socket.on('disconnect', function () {
        connectedSockets--;
        console.log("Socket disconnect! Conected sockets:", connectedSockets);
    });
});
 
setInterval(function() {
    io.emit("message", "Hola " + new Date().getTime());
}, 1000);

O código-fonte está disponível na minha conta do GitHub.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://gonzalo123.com/2014/12/01/enclosing-socket-io-websocket-connection-inside-a-html5-sharedworker/