Holas, durante mi tiempo libre estuve trabajando en un proyecto llamado Gocho. Esta pequeña aplicación permite compartir archivos en una red local, por ejemplo la red de la casa, red del trabajo, etc. con la característica de auto-descubrimiento de nodos (auto-discovery). En este post explicaré de que se trata, por qué lo hice y algunos de los retos que surgieron al hacer la aplicación.
Por qué lo hice?
Lo hice porque deseaba una aplicación donde pueda compartir un directorio; poder aguantar varias descargas al mismo tiempo; no tener que preguntar la dirección IP o puerto de los recursos que otros comparten; tener algo que sea simple de ejecutar en cualquier sistema operativo (Windows, OSX, GNU/Linux) sin tener que instalar ningún requisito previo.
Este tipo de proyectos siempre los hago a modo de aprender algo nuevo. En este caso ya desde el año pasado (2017) tenía ganas de hacer algo con Go. Si bien hice uno que otro experimento a modo de aprender (Golondrina; EazyPanel), esta aplicación me pareció un buen caso de uso para este lenguaje en específico (lo elaboro más adelante).
Por qué Go?
Si alguien ya ejecutó la aplicación es fácil notar que no es algo de otro mundo. Gocho podría haber sido desarrollado en otros lenguajes como: C/C++, Python, Ruby, Java, CSharp, etc. pero tenía algunas observaciones preliminares:
- Python/Ruby - es importante tener los intérpretes instalados, en Windows no vienen por defecto.
- Java/CSharp - es importante tener las máquinas virtuales respectivas instaladas (JVM o NetCore). No siempre se dá el caso que ya venga instalado por defecto en el sistema operativo.
- C/C++ - sería la opción más obvia, pero un par de problemas que encontré sería que por defecto son dinámicamente enlazados, lo cual causaría en algún caso la instalación de librerias necesarias (a menos que use el flag de estáticamente enlazados) y el segundo problema es que a pesar que se leer C/C++ no me siento en la confianza de lanzarme a hacer algo
- Go - estáticamente compilado por defecto (todo en un binario), el binario resultante no require que exista la instalación previa de alguna librería, intérprete, máquina virtual y otros.
Con esta lista —un poco parcializada 😉— Go cumple con las necesidades que tengo.
Algo no mencionado es la facilidad con la que puedo crear binarios para diferentes plataformas. Por ejemplo Gocho esta disponible para distintas plataformas sin mucho problema. Releases Gocho
Algunos Problemas que Encontré
Problema 1: Compartir Archivos
En la empresa donde trabajo existe una diversidad en cuestión a sistemas operativos. Algun@s compañer@s utilizan Windows, OSX y otr@s GNU/Linux. Para compartir archivo, existe un Active Directory o algo similar configurado, personalmente nunca logré acceder a las carpetas compartidas por otros (uso GNU/Linux). Al intentar acceder me salía la opción de insertar un dominio y credenciales; pése a que introducía los datos —que en teoría eran correctos— no lograba acceder a los archivos compartidos.
En el trabajo hay compañeros que comparten información por ejemplo videos, cursos, etc. montando un servidor httpd
en su máquina local o en mi caso ejecutaba python -m SimpleHTTPServer
en el directorio que deseaba compartir. Noté un problema con SimpleHTTPServer
, con pocas personas tratando de descargar el mismo archivo, esta pequeña utilidad solo permite manejar una descarga al mismo tiempo.
Mi segundo intento fue utilizar algo un poco más robusto que SimpleHTTPServer
pero sin la necesidad de levantar algo grande como httpd
. Tuve la suerte de chocar con un ejemplo en la documentación de Go para el modulo net/http
que justamente —con pocas líneas de código— me permitía compartir un directorio y podía soportar varias descargas simultáneas sin problemas.
package main
import (
"log"
"net/http"
)
func main() {
// Simple static webserver:
directory := "some/directory"
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/home/myuser/some/directory"))))
}
Solo tuve que compilar este archivo; poner el binario que llamé http
en algún directorio que se encuentre en mi PATH
de ejecución y voila! ya contaba con algo más robusto que pueda aguantar más descargas y no tenga que montar todo un servicio.
ya contaba con algo más robusto que pueda aguantar más descargas
Con esto me refiero a algo que aguante varias conexiones simultáneas, algo que Go lo plantea de una manera simple y en el caso del módulo
net/http
ya viene por defecto.
Hasta este punto, solamente tengo un pequeño binario que me permite compartir un directorio y pueda aguantar varias descargas simultáneas —algo no tan complejo como montar un servicio httpd
pero más robusto que SimpleHTTPServer
de Python.
La verdad si compilara este binario http
para varios sistemas operativos sería suficiente para compartir archivos, pero quería ir un poco más alla.
Problema 2: Indicar Donde se Encuentran los Archivos Compartidos
Otro problema que encontré es que cada vez que un usuario comparte algo en una red local, este debe —de algún modo— comunicar como acceder a los recursos que se comparten. Una forma común de realizar esto es compartir la url de descarga http://ip_red_local:algun_puerto
en algun grupo de chat o similar.
Ya que esta aplicación la tengo orientada para el contexto de una red local, algo que se me pasó por la cabeza son los juegos en red como StarCraft. En StarCraft cuando alguien crea una partida de red de área local, los jugadores que se unirán a una partida no especifican como tal la dirección IP de la máquina servidor a la que se conectarán. El juego simplemente muestra las partidas creadas en la red actualmente y uno puede conectarse sin problemas de forma automática.
Investigando un poco sobre cómo estos juegos hacian posible mostrar las partidas ya existentes en la red sin tener que especificar una dirección IP o algo similar, me llevó al concepto de multicast.
En palabras simples, Multicast es un método que permite enviar información a nodos interesados en una red.
Por ejemplo, si deseo enviar el mensaje “hola mundo” a computadoras interesadas en recibir este mensaje sin que yo tenga que saber a qué máquinas específicamente, la idea sería la siguiente:
- Mi Computadora: Enviar datagrama UDP con mensaje “hola mundo” a alguna dirección IP reservada para multicast ej. 239.6.6.6:1234
- Computadora Interesada 1: Escuchar por datagramas UDP en 239.6.6.6:1234
- Computadora Interesada 2: Escuchar por datagramas UDP en 239.6.6.6:1234
- Computadora Interesada n: Escuchar por datagramas UDP en 239.6.6.6:1234
De este modo multicast permite que cualquier máquina que desee compartir algo, solo debe enviar su información de nodo (identificador, dirección IP, puerto) por multicast y otras máquinas interesadas.
Sabiendo esto, Gocho además de compartir un directorio podrá saber lo que otros nodos Gocho estan compartiendo.
Ya viendo un poco la implementación de esto, podemos ver algunos trozos de código que utilicé en Gocho.
La función announceNode
básicamente envía un paquete multicast.
func announceNode(nodeInfo *NodeInfo) {
address, err := net.ResolveUDPAddr("udp", MULTICAST_ADDRESS)
// error handling
conn, err := net.DialUDP("udp", nil, address)
// error handling
for {
...
conn.Write([]byte(message))
time.Sleep(ANNOUNCE_INTERVAL_SEC * time.Second)
}
}
La función listenForNodes
que escucha los mensajes multicast.
func listenForNodes(nodeList *list.List) {
address, err := net.ResolveUDPAddr("udp", MULTICAST_ADDRESS)
// error handling
conn, err := net.ListenMulticastUDP("udp", nil, address)
// error handling
conn.SetReadBuffer(MULTICAST_BUFFER_SIZE)
for {
packet := make([]byte, MULTICAST_BUFFER_SIZE)
size, udpAddr, err := conn.ReadFromUDP(packet)
...
}
}
Entonces gran parte de la “mágia” de Gocho se encuentra en el trabajo con multicast. Con multicast una máquina puede anunciarse a sí misma y a la vez descubrir a otros nodos.
Problema 3: Formato de los Mensajes
Si bien ya tenemos una forma de comunicarnos entre nodos, ví conveniente poder identificar los paquetes que envía Gocho con otros. Básicamente el paquete (en esta versión inicial) debe seguir lo siguiente:
- Los primeros 4 bytes deben ser
0x60
,0x0d
,0xf0
,0x0d
o0x600df00d
, que es la cabecera que identifica que lo enviado es un mensaje de otro nodo de Gocho - El siguiente byte especifica el comando, actualmente solo existe un solo comando que es
0x01
que indica que un nodo se esta anunciando. La información del nodo se encuentra en el payload - Finalmente el resto es el payload. Para esto decidí utilizar el formato JSON
Un hexdump de un mensaje en el que se anuncia un nodo luce de la siguiente forma:
00000000 60 0d f0 0d 01 7b 22 6e 6f 64 65 49 64 22 3a 22 |`....{"nodeId":"|
00000010 6e 6f 64 6f 2d 73 65 72 67 69 6f 22 2c 22 69 70 |nodo-sergio","ip|
00000020 41 64 64 72 65 73 73 22 3a 22 22 2c 22 77 65 62 |Address":"","web|
00000030 50 6f 72 74 22 3a 22 35 30 30 30 22 7d |Port":"5000"}|
La decisión de tener este formato fue la de ahorrar la mayor cantidad de bytes posibles. De hecho, si no utilizara el formato JSON se ahorrarían unos cuantos bytes más.
En el futuro es posible que existan más comandos diferentes al de anunciar un nodo (0x01
). Es por eso que se dejó un byte reservado para ello.
Diseño de la aplicación
Esta sección habla un poco más de la implementación ya habiendo conocido los problemas mencionados arriba. Para poder seguir esta sección hago referencia al código fuente de la aplicación.
Estructura del Código
La estructura de código del proyecto Gocho está basada en este artículo en el que se plantea una esctructura para proyectos en Go. Esta estructura es bastante utilizada en diferentes proyectos, entre ellos algunos proyectos bastante conocidos como Kubernetes o Docker.
El proyecto incluye un archivo Makefile
el cual indica los pasos necesarios para poder construir o desarrollar el proyecto, como también crear los binarios para las plataformas soportadas.
Componentes del Servicio
En el anterior punto mencioné la estructura de código que utilizo. En este punto me enforcaré en los componentes dentro del directorio pkg
, especialmente en pkg/node
.
Componente | Descripción |
---|---|
pkg/info |
Información básica de la aplicación como nombre o versión. |
pkg/cmds |
Toda la lógica de flags de la utilidad de línea de comandos. Por ejemplo gocho start [options] o gocho configure . |
pkg/config |
Todo el código con la lógica y estructuras necesarias para representar la lógica de Gocho. Aca se encuentra la lógica de establecer valores por defecto o cargar las diferentes configuraciones de un archivo .gocho.conf o de los options establecidos por línea de comandos. |
pkg/node |
La lógica principal de la aplicación radica en este directorio. El cómo se tiene un dashboard web embebido; el formato de los paquetes; el mecanismo de auto-discovery (multicast) y el índice de archivos que muestra el contenido del directorio compartido. |
Algunas Estructuras de Datos y Lógica Utilizada
La aplicación debe guardar la información de otros nodos, para esto decidí hacer uso de una lista enlazada por la simplicidad al borrar o insertar elementos.
Es importante notar que mientras más nodos se anuncien en una red, es posible que partes del código (de cualquier nodo) tendrá que ejecutar las mismas sentencias al mismo tiempo. Para evitar problemas de concurrencia: principalmente en la lista enlazada que guarda información de otros nodos, hice uso de un Mutex con lo cual pude controlar estos comportamientos que podrían llevar a resultados inesperados.
Algo importante es notar que existen algunos timeouts por defecto que constantemente verifican la lista enlazada de nodos. Básicamente estos timeouts nos permite liberar recursos, cuando un nodo después de cierto tiempo deja de anunciarse.
El Dashboard
Para el desarrollo del dashboard de frontent hice uso de React solamente. Tal vez algunos se preguntan ¿por qué no utilicé otras librerias como Redux o React-Router?. Pues la respuesta es simple, como el bundle javascript resultante se encuentra embebido en el binario, si tenemos un bundle más grande y pesado, el binario resultante será más grande y pesado.
Los componentes y el código para la UI se encuentra en el directorio ui
. Para la estructura quise mantener las cosas cosas simple así que utilicé Create React App para este propósito.
De la misma forma para los estilos, si bien podría haber utilizado un procesador como SASS, decidí mantener los estilos y solo utilizar el estándar por defecto CSS, que como verán en el código es un archivo de 184 líneas de código.
Para generar el bundle javascript solo es necesario ejecutar el siguiente comando.
$ make dist
Internamente esto utiliza los scripts de Create React App y para poder tener el bundle embebido dentro del binario, hago uso de Go Generate.
Este archivo tiene un comentario:
package main
//go:generate go-bindata -o ../../assets/assets_gen.go -pkg assets ../../ui/build/...
import (
"github.com/donkeysharp/gocho/pkg/cmds"
"os"
)
...
donde se especifica donde se encuentra el bundle resultante que será embebido en el binario resultante.
El Índice de Archivos Compartidos
Esta fue una de las partes en las que tuve mucha diversión. Como mencioné en el Problema 1, Go presenta un ejemplo para poder compartir un directorio. El problema con esto es que no cuenta con estilos, extensibilidad, el directorio ..
para poder subir un nivel el directorio.
Para poder personalizar este código ya existente tuve que utilizar Interceptor Pattern y un middleware personalizado para poder adicionar íconos y HTML personalizados, el directorio ..
para ir un nivel hacia arriba y agrupar directorios de archivos de forma ordenada.
Toda la lógica para la personalización de net/http.FileServer
se encuentra en el archivo index.go.
Espero poder crear otra entrada en la que muestro específicamente la implementación de esta parte que puede ser reutilizada con cualquier otra aplicación.
Comentarios Finales
Hay varias cosas que deseo mejorar de Gocho. Al ser un proyecto open-source sientanse libres de abrir un issue o mejor contribuir con algo de código (bugfixing, nuevos features, documentación, etc.)
Esta es la primera vez que trabajo en una aplicación, la cual envia y recibe información de varias máquinas o nodos al mismo tiempo y hayan diferentes cosas que sincronizar. Personalmente fue —y espero siga siendo— una experiencia educativa.