Advertisement
  1. Code
  2. Web Development

Chat en tiempo real con Readline y Socket.io de Node.js

Scroll to top
Read Time: 9 min

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

Node.js tiene un módulo poco apreciado en su librería estándar que es sorprendentemente útil. El módulo Readline hace lo que dice en el cuadro: lee una línea de entrada desde la terminal. Lo puedes usar para hacerle una o dos preguntas al usuario, o para crear un mensaje en la parte inferior de la pantalla. En este tutorial te mostraré la capacidad de Readline y cómo crear una sala de chat CLI en tiempo real respaldada por Socket.io. El cliente no solo podrá enviar mensajes sencillos, sino que tendrá comandos para emoticones con /me, mensajes privados con /msg y permitirá cambiar los apodos con /nick.

Un poco sobre Readline

Este es probablemente el uso más simple de Readline:

1
var readline = require('readline');
2
3
var rl = readline.createInterface(process.stdin, process.stdout);
4
5
rl.question("What is your name? ", function(answer) {
6
	console.log("Hello, " + answer );
7
	rl.close();
8
});

Incluimos el módulo, creamos la interfaz de Readline con los streams estándar de entrada y salida, luego le hacemos una única pregunta al usuario. Este es el primer uso de Readline: hacer preguntas. Si necesitas confirmar algo con un usuario, tal vez de la forma más común, "¿Quieres hacer esto? (s/n)", la cual domina las herramientas CLI, readline.question() es la forma de hacerlo.

La otra funcionalidad que ofrece Readline es el prompt, que se puede personalizar a partir de su carácter predeterminado ">"  y se puede pausar temporalmente para evitar las entradas. Esta será la interfaz principal para nuestro cliente del chat de Readline. Solo habrá una aparición de readline.question() para pedirle al usuario un apodo, pero todo lo demás será readline.prompt().

Administra tus dependencias

Comencemos con la parte aburrida: las dependencias. Este proyecto usará el servidor socket.io, el paquete socket.io-client y ansi-color. Tu archivo packages.json debería verse así:

1
{
2
    "name": "ReadlineChatExample",
3
	"version": "1.0.0",
4
	"description": "CLI chat with readline and socket.io",
5
	"author": "Matt Harzewski",
6
	"dependencies": {
7
		"socket.io": "latest",
8
		"socket.io-client": "latest",
9
		"ansi-color": "latest"
10
	},
11
	"private": true
12
}

Ejecuta npm install y deberías estar listo para conectarte.

El servidor

En este tutorial utilizaremos un servidor Socket.io increíblemente simple. No hay nada más básico que esto:

1
var socketio = require('socket.io');
2
3
// Listen on port 3636

4
var io = socketio.listen(3636);
5
6
io.sockets.on('connection', function (socket) {
7
8
    // Broadcast a user's message to everyone else in the room

9
	socket.on('send', function (data) {
10
		io.sockets.emit('message', data);
11
    });
12
13
});

Lo que hace es tomar un mensaje entrante de un cliente y pasarlo a todos los demás. El servidor probablemente podría resistir una aplicación de mayor escala, pero para este simple ejemplo debería ser suficiente.

Esto debe guardarse en el directorio del proyecto como server.js.

El Cliente: Incluir y Configurar

Antes de llegar a la parte divertida, debemos incluir nuestras dependencias, definir algunas variables e iniciar la interfaz de Readline y la conexión del socket.

1
var readline = require('readline'),
2
socketio = require('socket.io-client'),
3
util = require('util'),
4
color = require("ansi-color").set;
5
6
7
var nick;
8
var socket = socketio.connect('localhost', { port: 3636 });
9
var rl = readline.createInterface(process.stdin, process.stdout);

El código se explica por sí solo en este momento. Tenemos nuestra variable de apodo, la conexión del socket (a través del paquete socket.io-client) y nuestra interfaz Readline.

Socket.io se conectará al localhost a través del puerto 3636 en este ejemplo, si estuvieras creando una aplicación de chat de producción esto se cambiaría al dominio y al puerto de tu propio servidor. (¡No tiene mucho sentido chatear contigo mismo!)

El cliente: preguntar por el nombre del usuario

Para el primer uso de Readline queremos pedirle al usuario que elija un apodo que lo identifique en la sala del chat. Para esto, usaremos el método question() de Readline.

1
// Set the username

2
rl.question("Please enter a nickname: ", function(name) {
3
    nick = name;
4
	var msg = nick + " has joined the chat";
5
	socket.emit('send', { type: 'notice', message: msg });
6
	rl.prompt(true);
7
});

Configuramos la variable nick desde antes en el valor recopilado del usuario, enviamos un mensaje al servidor (que será transmitido a los otros clientes) donde dice que nuestro usuario se unió al chat, luego cambiamos la interfaz de Readline al modo de solicitud. El valor true pasado a prompt() asegura que el carácter de la solicitud se muestre correctamente. (De lo contrario, el cursor puede moverse a la posición cero en la línea y no se mostrará el ">").

Desafortunadamente, Readline tiene un problema frustrante con el método prompt(). No funciona bien con console.log(), que generará un texto en la misma línea que el carácter de solicitud, dejando caracteres ">" extraviados en todas partes y otras rarezas. Para arreglar esto, no usaremos console.log en ninguna parte de la aplicación, salvo en un lugar. En cambio, la salida debe transferirse a esta función:

1
function console_out(msg) {
2
    process.stdout.clearLine();
3
	process.stdout.cursorTo(0);
4
	console.log(msg);
5
	rl.prompt(true);
6
}

Esta solución ligeramente trillada asegura que la línea actual en la consola esté vacía, y que el cursor esté en la posición cero antes de imprimir la salida. Luego, se solicita explícitamente que la solicitud se envíe nuevamente.

En el resto de este tutorial, verás console_out() en lugar de console.log().

El cliente: manejar la entrada

Hay dos tipos de entrada con la que un usuario puede ingresar: el chat y los comandos. Sabemos que antes de los comandos hay un slash, por lo que es fácil diferenciar los dos.

Readline tiene varios controladores de eventos, pero el más importante es line. Cada vez que se detecta un carácter de nueva línea en la secuencia de entrada (de la tecla return o enter), se activa este evento. Por lo tanto, debemos conectarnos en el line de nuestro controlador de entrada.

1
rl.on('line', function (line) {
2
    if (line[0] == "/" && line.length > 1) {
3
		var cmd = line.match(/[a-z]+\b/)[0];
4
		var arg = line.substr(cmd.length+2, line.length);
5
		chat_command(cmd, arg);
6
7
	} else {
8
		// send chat message

9
		socket.emit('send', { type: 'chat', message: line, nick: nick });
10
		rl.prompt(true);
11
	}
12
});

Si el primer carácter de la línea de entrada es un slash, sabemos que es un comando, por lo cual requerirá más procesamiento. De lo contrario, solo enviamos un mensaje de chat normal y restablecemos el mensaje. Ten en cuenta la diferencia entre los datos enviados a través del socket y el mensaje de unión en el paso anterior. Estás usando un type diferente, por lo que el cliente receptor sabe cómo formatear el mensaje y también transferimos la variable nick.

El nombre del comando (cmd) y el texto que sigue (arg) se aíslan con una pequeña expresión regular y la magia de la subcadena, luego las pasamos a una función que procesa el comando.

1
function chat_command(cmd, arg) {
2
    switch (cmd) {
3
4
		case 'nick':
5
			var notice = nick + " changed their name to " + arg;
6
			nick = arg;
7
			socket.emit('send', { type: 'notice', message: notice });
8
			break;
9
10
		case 'msg':
11
			var to = arg.match(/[a-z]+\b/)[0];
12
			var message = arg.substr(to.length, arg.length);
13
			socket.emit('send', { type: 'tell', message: message, to: to, from: nick });
14
			break;
15
16
		case 'me':
17
			var emote = nick + " " + arg;
18
			socket.emit('send', { type: 'emote', message: emote });
19
			break;
20
21
		default:
22
			console_out("That is not a valid command.");
23
24
	}
25
}

Si el usuario digita /nick gollum, la variable nick se restablece para que sea gollum, donde antes podría haber sido smeagol y se envía un aviso al servidor.

Si el usuario digita /msg bilbo Where is the precious?, se usa la misma expresión regular para separar el destinatario y el mensaje, entonces un objeto de tipo tell se envía al servidor. Esto se mostrará un poco diferente a un mensaje normal y no debería ser visible para otros usuarios. Es cierto que nuestro servidor simple enviará el mensaje a todos al azar, pero el cliente ignorará las indicaciones que no están dirigidas a su apodo. Un servidor más sólido podría ser más discreto.

El comando de emoticones se usa en la forma /me is eating second breakfast. El apodo se antepone al emoticón de una manera que debería ser familiar para cualquier persona que haya utilizado un IRC (Internet Relay Chat) o haya jugado un juego de rol multijugador, luego se envía al servidor.

El cliente: manejar los mensajes entrantes

Ahora el cliente necesita una forma de recibir mensajes. Todo lo que tenemos que hacer es conectar el evento message del cliente en Socket.io y formatear los datos adecuadamente para la salida.

1
socket.on('message', function (data) {
2
    var leader;
3
	if (data.type == 'chat' && data.nick != nick) {
4
		leader = color("<"+data.nick+"> ", "green");
5
		console_out(leader + data.message);
6
	}
7
	else if (data.type == "notice") {
8
		console_out(color(data.message, 'cyan'));
9
	}
10
	else if (data.type == "tell" && data.to == nick) {
11
		leader = color("["+data.from+"->"+data.to+"]", "red");
12
		console_out(leader + data.message);
13
	}
14
	else if (data.type == "emote") {
15
		console_out(color(data.message, "cyan"));
16
	}
17
});

Los mensajes de tipo chat que no fueron enviados por el cliente utilizando nuestro apodo se muestran con el apodo y el texto del chat. El usuario puede ver lo que escribió en la línea de lectura, por lo que no tiene sentido volver a enviarlo. Aquí estoy usando el paquete ansi-color para colorear un poco la salida. No es estrictamente necesario, pero hace que sea más fácil entender el chat.

Los mensajes de tipo notice o emote se imprimen tal cual, aunque de color cian.

Si el mensaje es un tell y el apodo es igual al nombre actual de este cliente, la salida toma la forma de [Somebody->You] ¡Hi! Obviamente esto no es muy privado. Si quisieras ver los mensajes de todos, lo que tendrías que hacer es quitar la parte de && data.to == nick. Lo ideal es que el servidor sepa a qué cliente enviar el mensaje y no enviarlo a los clientes que no lo necesitan. Pero eso solo añade una complejidad innecesaria que está más allá del alcance de este tutorial.

¡Enciéndelo!

Ahora veamos si todo funciona. Para ponerlo a prueba, inicia el servidor ejecutando node server.js y luego abre un par de nuevas ventanas de la terminal. En las nuevas ventanas, ejecuta node client.js e ingresa un apodo. Suponiendo que todo salga bien deberías poder chatear entre ellos.

En este tutorial espero haberte mostrado lo fácil que es comenzar con el módulo Readline. Es posible que quieras intentar agregar más funcionalidades a la aplicación del chat para practicar más. Por último, consulta la documentación de Readline para obtener la API completa.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.