Entrega del servidor
Hay dos formas comunes de sincronizar el estado del cliente al reconectarse:
- el servidor envía todo el estado
- o el cliente mantiene registro del último evento que procesó y el servidor envía las piezas faltantes
Ambas son soluciones totalmente válidas y elegir una dependerá de tu caso de uso. En este tutorial, iremos con la segunda.
Primero, persistamos los mensajes de nuestra aplicación de chat. Hoy hay muchas opciones geniales, usaremos SQLite aquí.
Si no estás familiarizado con SQLite, hay muchos tutoriales disponibles en línea, como este.
Instalemos los paquetes necesarios:
- NPM
- Yarn
- pnpm
- Bun
npm install sqlite sqlite3
yarn add sqlite sqlite3
pnpm add sqlite sqlite3
bun add sqlite sqlite3
Simplemente almacenaremos cada mensaje en una tabla SQL:
- CommonJS
- ES modules
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');
const { Server } = require('socket.io');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
async function main() {
// abrir el archivo de base de datos
const db = await open({
filename: 'chat.db',
driver: sqlite3.Database
});
// crear nuestra tabla 'messages' (puedes ignorar la columna 'client_offset' por ahora)
await db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_offset TEXT UNIQUE,
content TEXT
);
`);
const app = express();
const server = createServer(app);
const io = new Server(server, {
connectionStateRecovery: {}
});
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
io.on('connection', (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
// almacenar el mensaje en la base de datos
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO manejar el fallo
return;
}
// incluir el offset con el mensaje
io.emit('chat message', msg, result.lastID);
});
});
server.listen(3000, () => {
console.log('servidor corriendo en http://localhost:3000');
});
}
main();
import express from 'express';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { Server } from 'socket.io';
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
// abrir el archivo de base de datos
const db = await open({
filename: 'chat.db',
driver: sqlite3.Database
});
// crear nuestra tabla 'messages' (puedes ignorar la columna 'client_offset' por ahora)
await db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_offset TEXT UNIQUE,
content TEXT
);
`);
const app = express();
const server = createServer(app);
const io = new Server(server, {
connectionStateRecovery: {}
});
const __dirname = dirname(fileURLToPath(import.meta.url));
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
io.on('connection', (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
// almacenar el mensaje en la base de datos
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO manejar el fallo
return;
}
// incluir el offset con el mensaje
io.emit('chat message', msg, result.lastID);
});
});
server.listen(3000, () => {
console.log('servidor corriendo en http://localhost:3000');
});
El cliente luego mantendrá registro del offset:
- ES6
- ES5
<script>
const socket = io({
auth: {
serverOffset: 0
}
});
const form = document.getElementById('form');
const input = document.getElementById('input');
const messages = document.getElementById('messages');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
socket.on('chat message', (msg, serverOffset) => {
const item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
socket.auth.serverOffset = serverOffset;
});
</script>
<script>
var socket = io({
auth: {
serverOffset: 0
}
});
var form = document.getElementById('form');
var input = document.getElementById('input');
var messages = document.getElementById('messages');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
socket.on('chat message', function(msg, serverOffset) {
var item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
socket.auth.serverOffset = serverOffset;
});
</script>
Y finalmente el servidor enviará los mensajes faltantes al (re)conectarse:
// [...]
io.on('connection', async (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO manejar el fallo
return;
}
io.emit('chat message', msg, result.lastID);
});
if (!socket.recovered) {
// si la recuperación del estado de conexión no fue exitosa
try {
await db.each('SELECT id, content FROM messages WHERE id > ?',
[socket.handshake.auth.serverOffset || 0],
(_err, row) => {
socket.emit('chat message', row.content, row.id);
}
)
} catch (e) {
// algo salió mal
}
}
});
// [...]
Veámoslo en acción:
Como puedes ver en el video anterior, funciona tanto después de una desconexión temporal como de un refresco completo de la página.
La diferencia con la característica de "Recuperación del estado de conexión" es que una recuperación exitosa podría no necesitar consultar tu base de datos principal (podría obtener los mensajes de un stream de Redis, por ejemplo).
OK, ahora hablemos de la entrega del cliente.