Tras cumplir el año de edad y con el cambio de década (bienvenidos a “los nuevos 20”), nos hemos propuesto, sin dejar de lado aquello que sabemos que os gusta, iterar en nuestro repertorio de temáticas de artículos, para dar paso a un nuevo conjunto de vías de exploración. Con esa intención fue con la que hace ya más de un mes empezamos a escribir sobre el desarrollo de videojuegos en Go, y con esa misma intención es con la que hoy queremos empezar a hablar de un nuevo protocolo de comunicación: los websockets. Pues, si bien es cierto que aún nos quedan muchas vías de exploración en el ámbito del protocolo HTTP/S, y que con las nuevas versiones del mismo (HTTP/2 y HTTP/3) aún tendremos más por explorar, existen otras tecnologías que están plenamente integradas en nuestro día a día y que no queremos dejar de lado.
Así que, una vez contextualizada nuestra decisión, veamos, lo primero de todo, qué son esos websockets de los que vamos a hablar en este artículo.
¿Qué son los WebSockets?
Los websockets son un protocolo de comunicación para las aplicaciones diseñadas con la arquitectura cliente-servidor, que nos permiten establecer una comunicación bidireccional (es decir, que los datos pueden fluir del cliente al servidor y viceversa) y dúplex (es decir, que la comunicación en ambas direcciones puede suceder de manera simultánea).
Este protocolo es, habitualmente, confundido o mezclado con el protocolo HTTP, pues ambos dependen de una conexión TCP (o socket) y suelen usar los mismos puertos (80 / 443). Además, y para más confusión, con la última actualización del estándar HTML (HTML5), el lenguaje utilizado para el diseño de páginas web, cuyo contenido viaja por HTTP, se añadió una [API con soporte para websockets] (https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). Es decir, que cuándo estamos navegando por una página web (mediante el protocolo HTTP), puede ser que también nos estemos comunicando con un servidor mediante websockets.
Sin embargo, la mayor evidencia de que son protocolos de comunicación distintos es la forma en que establecemos una nueva conexión mediante los mismos:
http://
para el protocolo HTTP.ws://
para el protocolo de websockets.
Ahora ya sabemos qué son los websockets, con lo que podríamos seguir con la siguiente pregunta: ¿para qué sirven?
¿Cuándo necesitamos hacer uso de los WebSockets?
Históricamente, el protocolo HTTP ha mostrado una clara debilidad a la hora de actualizar el contenido de una página web de forma dinámica (es decir, sin tener que recargar la página -entera-). Y, pese a la inclusión de [AJAX] (https://es.wikipedia.org/wiki/AJAX), siempre ha sido necesaria la interacción del usuario para dar lugar a dichas actualizaciones. Es decir, en aquellas situaciones en las que necesitamos tener una actualización de la información en tiempo real (o casi), como el seguimiento en vivo (live) de un evento deportivo, tradicionalmente ha sido el usuario (cliente) el que ha tenido que solicitar esa actualización, aunque esa solo fuera parcial.
Para solicitar esas actualizaciones del contenido al servidor, existen diferentes técnicas más o menos eficientes:
-
Una de ellas es el polling, que consiste en una técnica en la que el cliente hace constantes (de forma reiterada) peticiones al servidor, de forma que el primero recibirá la actualización del contenido tan pronto como el servidor responda a una de las peticiones que llegó una vez los datos ya habían sido actualizados.
-
Una variante de la técnica anterior, algo optimizada, es el long polling, en cuyo caso el servidor mantiene la petición sin responder hasta que detecta una actualización de los datos o hasta que transcurre un período de tiempo determinado.
Sin embargo, parece evidente que si la cantidad de actualizaciones que sufren los datos es considerable, esa técnica basada en crear una nueva conexión y hacer una nueva petición, una vez tras otra, no es nada eficiente. Y ahí es dónde entran en juego los websockets. Cuyo modus operandi es completamente el opuesto: crear una única conexión que será reaprovechada cada vez que cliente y servidor tengan que intercambiar datos.
De nuevo, parece evidente que esta última no será la mejor estrategia cuándo no tengamos esa necesidad de actualizar los datos con tal inmediatez, pues mantener un sinfín de conexiones sin una razón para ello también puede resultar contraproducente. Ahí ya será nuestro trabajo tomar una decisión u otra en función de las necesidades de cada caso.
WebSockets en Go
Vale, ya hemos aprendido qué son los websockets y hemos revisado en qué casos de uso nos van a ser útiles. Ahora es el
momento de escribir algo de código Go para poner en práctica esos conocimientos teóricos. Para ello vamos a recurrir a
un clásico: la gente de Gorilla. Así como cuándo hablamos de routers HTTP nunca
puede faltar la referencia a gorilla/mux
, cuándo hablamos de websockets en Go no
puede faltar la referencia a gorilla/websocket
.
Como decíamos antes, lo que vamos a hacer será establecer una conexión cliente-servidor que nos permitirá intercambiar
mensajes. Para ver un ejemplo práctico vamos a hacer un servidor que responderá al cliente con el reverso de los mensajes
que éste le envíe. Es decir, cuándo el cliente envíe HELLO
, el servidor responderá al mismo con OLLEH
.
Una implementación simplificada de dicho servidor podría ser la siguiente:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
func reverse(w http.ResponseWriter, r *http.Request) {
ws, _ := upgrader.Upgrade(w, r, nil)
defer ws.Close()
for {
// Receive message
mt, message, _ := ws.ReadMessage()
log.Printf("Message received: %s", message)
// Reverse message
n := len(message)
for i := 0; i < n/2; i++ {
message[i], message[n-1-i] = message[n-1-i], message[i]
}
// Response message
_ = ws.WriteMessage(mt, message)
log.Printf("Message sent: %s", message)
}
}
func main() {
http.HandleFunc("/reverse", reverse)
log.Fatal(http.ListenAndServe(":5555", nil))
}
Lo primero que puede sorprender de este fragmento de código, es que, en efecto, estamos exponiendo un servicio HTTP. Eso es debido a que, como comentamos anteriormente, ambos protocolos recaen en el [TCP Handshake] (https://en.wikipedia.org/wiki/Handshaking#TCP_three-way_handshake) para establecer al conexión.
Sin embargo, en esta ocasión no vamos a responder a la petición y a seguir esperando, sino que vamos a hacer uso del
websocket.Upgrader
para convertir dicha conexión
HTTP en un websocket.
Una vez establecida la conexión (mediante el método upgrader.Upgrade
),
ya estaremos en disposición de recibir y enviar mensajes por ese websocket. En esta ocasión, y con el fin de simplificar
el ejemplo, lo que vamos a hacer será un bucle infinito en el que, para cada mensaje recibido, responderemos con el reverso.
Por otro lado, la implementación simplificada del cliente podría tener un aspecto similar a este:
package main
import (
"bufio"
"log"
"net/url"
"os"
"github.com/gorilla/websocket"
)
func main() {
u := url.URL{Scheme: "ws", Host: ":5555", Path: "/reverse"}
// Establish connection
c, _, _ := websocket.DefaultDialer.Dial(u.String(), nil)
defer c.Close()
// Receive messages
go func() {
for {
_, message, _ := c.ReadMessage()
log.Printf("Message received: %s", message)
}
}()
// Read from stdin and send through websocket
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
_ = c.WriteMessage(websocket.TextMessage, []byte(scanner.Text()))
log.Printf("Message sent: %s", scanner.Text())
}
}
Qué como podéis ver, no es ciencia de cohetes. De nuevo, vamos a hacer uso de los mecanismos que nos proporciona
la librería de Gorilla (Dialer.Dial
) para establecer una
conexión con el servidor. En esta ocasión, sin embargo, como queremos ir recibiendo los mensajes que nos llegan
del servidor en todo momento, el bucle infinito de recepción de mensajes lo vamos a ejecutar dentro una gorrutina.
Intercambio de datos JSON
Como vimos en el ejemplo anterior, el envío (Conn.WriteMessage
)
y la recepción (Conn.ReadMessage
) de los mensajes
lo podemos hacer a nivel de bytes. Sin embargo, a veces nos interesará enviar estructuras de datos más complejas, en cuyo
caso podremos hacer uso de los métodos Conn.ReadJSON
y Conn.WriteJSON
. Los que, como podemos
imaginar, hacen uso del paquete encoding/json
internamente.
Gestión de múltiples conexiones
Finalmente, otra opción que podría ser interesante en la implementación anterior del servidor sería la de gestionar múltiples conexiones. De hecho, ese será un caso de uso habitual. Para ello, podríamos adaptar un poco el código para obtener algo así:
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
)
var upgrader = websocket.Upgrader{}
var connections []*websocket.Conn
func reverse(w http.ResponseWriter, r *http.Request) {
ws, _ := upgrader.Upgrade(w, r, nil)
defer ws.Close()
for {
// Receive message
mt, message, _ := ws.ReadMessage()
log.Printf("Message received: %s", message)
// Reverse message
n := len(message)
for i := 0; i < n/2; i++ {
message[i], message[n-1-i] = message[n-1-i], message[i]
}
// Response message
_ = ws.WriteMessage(mt, message)
log.Printf("Message sent: %s", message)
}
}
func broadcastMessage(msg []byte, connections []*websocket.Conn) {
// Reverse message
n := len(msg)
for i := 0; i < n/2; i++ {
msg[i], msg[n-1-i] = msg[n-1-i], msg[i]
}
for _, conn := range connections {
_ = conn.WriteMessage(websocket.TextMessage, msg)
}
}
func main() {
incomingMessages := make(chan []byte)
incomingConnections := make(chan *websocket.Conn)
go func() {
for {
select {
case msg := <-incomingMessages:
broadcastMessage(msg, connections)
case conn := <-incomingConnections:
connections = append(connections, conn)
}
}
}()
http.HandleFunc("/reverse", func(writer http.ResponseWriter, request *http.Request) {
conn, _ := upgrader.Upgrade(writer, request, nil)
defer conn.Close()
incomingConnections <- conn
for {
// Receive message
_, message, _ := conn.ReadMessage()
log.Printf("Message received: %s", message)
incomingMessages <- message
}
})
log.Fatal(http.ListenAndServe(":5555", nil))
}
Si lo analizamos en detalle, nos damos cuenta de que, en realidad, la mayor adaptación del código se ha producido para
soportar concurrencia en el lado servidor. Es decir, a diferencia del caso anterior, en el que una vez se iniciaba una
nueva conexión websocket, esta era tratada de manera independiente (concurrencia a nivel de handler HTTP). Ahora,
lo que estamos haciendo es mantener sincronizado un slice de conexiones, sobre el que haremos [broadcasting]
(https://es.wikipedia.org/wiki/Radiodifusi%C3%B3n) cada vez que nos llegue un nuevo mensaje. Para ello, hemos añadido
un par de canales como método de sincronización, ya que, en caso de actualizar directamente la variable connections
,
si en ese momento se estuviera iterando sobre ella (para hacer un envío), entonces se produciría una race condition
y la aplicación finalizaría su ejecución.
El resto de código del servidor (así como la implementación del cliente), sin embargo, se mantendran inalteradas y, de hecho,
los métodos que usaremos de la librería gorilla/websocket
seguirán siendo los mismos que en el ejemplo anterior.
Y bien, ahora que ya hemos jugado un poco con los websockets, ¡es vuestro turno! En futuros artículos profundizaremos más en este protocolo de comunicación, veremos cómo usar los diferentes tipos de mensajes que la librería soporta, como hacer un graceful shutdown de nuestras conexiones, y más.
Pero, por el momento, ya podéis empezar a trastear para interiorizar los conceptos que hoy hemos introducido.
¿Habíais usado alguna vez los WebSockets con Go?
¿Se os ocurre algún caso de uso que queráis compartir?
Como siempre, estaremos muy agradecidos de que nos dejéis vuestras experiencias personales, dudas o sugerencias en los comentarios o en nuestra cuenta de Twitter.