Para entender que es un Websocket tendríamos que irnos un poco más atrás con un par de conceptos de comunicaciones, full duplex y half duplex. Duplex representa el estado que un dispositivo de red tiene para poder enviar y recibir información. Half duplex representa una comunicación de un dispositivo a otro de manera bidireccional, pero no al mismo tiempo, un ejemplo podría ser los Walkie-Talkie o el telegrama, donde los mensajes se enviaban de un lado a otro, pero había que esperar una respuesta para poder continuar con la comunicación. Full duplex en su caso, es una comunicación bidireccional al mismo tiempo entre dos entidades sin necesidad de esperar una respuesta, por ejemplo, la mensajería instantánea o las llamadas telefónicas.

Entendiendo esto podremos empezar a definir lo que el protocolo Websocket significa, una tecnología empleada en Web para la comunicación bidireccional full duplex sobre una conexión TCP. Resuelve una falta que tiene el protocolo HTTP, que es half duplex.

Para iniciar el ejemplo de un Chat mediante Websockets con Javascript, lo primero es tener un cliente y un servidor, que serán los encargados de abrir un canal de comunicación full duplex y una librería especial llamada socket.io que ofrece una comunicación bidireccional y de baja latencia para la implementación que necesitamos.

Si te gusta ver el código antes de la explicación puedes ir al repositorio aquí.

Implementación lado cliente

El chat quedará visualmente algo como esto:

la terminal

Para darle un poco de estilo a la vista web del cliente use un poco de tailwind, el HTML queda algo como esto:

Hay elementos HTML con un #id que se necesitan para realizar ciertas tareas, un archivo llamado connection.js contiene lo siguiente:

Este archivo es el encargado de distribuir la lógica del cliente, donde iniciamos con una instancia de socket.io lado cliente:

import { io } from "https://cdn.socket.io/4.3.2/socket.io.esm.min.js";
 
// inicializamos una instancia del websocket del lado del cliente
// en el puerto 8080
const socket = io.connect("http://localhost:8080", {
 forceNew: true,
});

Estos son los elementos HTML que comentaba hace un momento los cuales nos ayudarán a realizar ciertas tareas dentro del experimento.
const messagesList = document.getElementById("messages");
const inputmessage = document.getElementById("message");
const btnSendMessage = document.getElementById("send");
const btnDisconnect = document.getElementById("disconnect");
const btnReconnect = document.getElementById("reconnect");

Para que funcione, se requiere añadir ciertas acciones a los eventos de cada elemento:
// añadimos un evento onclick para el buton de enviar mensaje
btnSendMessage.addEventListener("click", sendMesssage, false);
 
// añadimos un evento onclick para el buton desconectar
btnDisconnect.addEventListener(
 "click",
 function () {
   alert("El socket está desconectado, no habrá comunicación");
   socket.disconnect();
 },
 false
);
 
// añadimos un evento onclick para el buton reconectar
btnReconnect.addEventListener(
 "click",
 function () {
   console.log("reconnect");
   socket.connect();
 },
 false
);
 
// añadimos un evento keypress para el
// input pueda enviar el mensaje al presionar Enter
inputmessage.addEventListener(
 "keypress",
 function (event) {
   if (event.key === "Enter") {
     event.preventDefault();
     sendMesssage();
   }
 },
 false
);

Tenemos un método para el envío de mensajes, obtiene el string del mensaje del input donde escribimos.
function sendMesssage() {
 const text = inputmessage.value;
 
 // listar el mensaje enviado
 pushMessages(text, "client");
 
 // hay que emitir este mensaje al servidor
 socket.emit("listen_client", text);
 
 // reset input
 inputmessage.value = "";
}

Utilizamos otro método o función que "pushea" los mensajes del cliente y del server, para mostrarlos en la pantalla web.
function pushMessages(msg, from) {
 // crear elemento div
 const el = document.createElement("div");
 
 // añadir una clase chat para el div
 el.classList.add("chat-message");
 
 if (from === "server") {
   // añadir html si es mensaje del server
   el.innerHTML = `
       <div class="flex items-end justify-end">
         <div class="flex flex-col space-y-2  max-w-xs mx-2 order-1 items-end">
             <div><span class="px-4 py-2 rounded-lg inline-block rounded-br-none bg-blue-600 text-white">${msg}</span></div>
         </div>
         <img src="https://i.pravatar.cc/150?img=69" alt="Server profile" class="w-6 h-6 rounded-full order-1" />
       </div>`;
 } else {
   // añadir html si es mensaje del cliente
   el.innerHTML = `
   <div class="flex items-end">
     <div class="flex flex-col space-y-2 max-w-xs mx-2 order-2 items-start">
         <div><span class="px-4 py-2 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600">${msg}</span></div>
     </div>
     <img src="https://i.pravatar.cc/150?img=15" alt="Server profile" class="w-6 h-6 rounded-full order-1" />
   </div>`;
 }
 
 // push element
 messagesList.appendChild(el);
}

Por último tenemos los eventos que necesitamos del socket que abrimos al principio de este archivo. Es algo muy importante para una aplicación real tener controlados todos los eventos necesarios en la interacción cliente-servidor. Por ejemplo el evento server_message recibe la información que manda el servidor.
// recibimos al realizar la conexión exitosa
socket.on("connect", () => {
 // este es id de la conexión
 console.log(socket.id);
});
 
// aquí se podrían notificar el error de la conexión
socket.on("connect_error", () => {
 console.error("Ocurrió un error al conectarse.");
});
 
// escuchamos lo que dice el servidor
socket.on("server_message", function (data) {
 pushMessages(data, "server");
});

Implementación lado server

Para arrancar el servidor tenemos este archivo:

Para levantar un servidor del lado cliente utilizamos expressjs express una librería que cuenta con un ecosistema amplio de funcionalidades para aplicaciones web.

Bajo el concepto de middlewares que utiliza express podemos habilitar cors y la carpeta pública donde están los archivos del cliente, el index.html y el connections.js

// habilitamos cors
app.use(cors());
 
// hay que incluir en el home de la app la carpeta client
app.use("/", express.static(path.resolve(__dirname, "../client")));

Para el Websocket exclusivamente se necesita activar el evento connection, para abrir las conexiones con el cliente.
io.on("connection", function (socket) {
 console.log("new websocket connection open");
 
 // mensaje de bienvenida
 socket.emit(
   "server_message",
   "¡Bienvenido estamos en una conexión TCP para enviar y recibir mensajes!"
 );
 
 // este canal va a escuchar los mensajes del cliente
 socket.on("listen_client", function (text) {
   console.log(`client says: "${text}"`);
   socket.emit("server_message", `Recibi tu mensaje: "${text}"`);
 });
 
 // emitimos un mensaje en el canal server_message
 // un mensaje con un cierto intervalo de tiempo (3s)
 setInterval(function () {
   console.log("sending message to client");
   socket.emit("server_message", randomMessages());
 }, 3000);
});

Incluimos un método llamado randomMessages() que se encarga de enviar distintos saludos "hardcodeados" en una función:
const messages = [
 "Hola soy el servidor, mucho gusto!",
 "Que bueno encontrarte por aquí",
 "Estamos en una conexión TCP, enviando mensajes a través de un canal full-duplex",
 "Hola de nuevo",
 "Con esto puedes crear grandes cosas, ¿No te parece?",
 "Disculpa si repito las cosas",
];
 
function randomMessages() {
 const randomIndex = Math.floor(Math.random() * messages.length);
 return messages[randomIndex];
}
 
module.exports = randomMessages;

Por último hay que levantar el servidor en el puerto 8080:

server.listen(8080, function () {
 console.log("server listen in http://localhost:8080");
});

Este ejemplo se abstrajo en un contenedor simple, para probarlo solo hay que ejecutar:

docker-compose up

Al terminar de construir la imagen y el contenedor podemos abrir el navegador el siguiente enlace http://localhost:8080/ y veremos la magia, podemos utilizar el chat mandando mensajes al servidor y el mismo nos contestará con lo que le mandemos en el mensaje.

También tenemos los logs en la consola del servidor, para observar qué está sucediendo de su lado.

la terminal

Sé que es un ejemplo básico pero para entender cómo funciona un chat, está de lujo.

Anímate a probarlo levantando los servicios con la aplicación dockerizada.

¡Saludos!