Vapor 4. WebSockets

Bienvenidos a lo que espero sea una serie de artículos sobre Vapor, el framework Web escrito en Swift que nos ha traído el desarrollo servidor al ecosistema Apple.

En el momento de escribir el artículo el equipo de Vapor está a punto de lanzar la versión 4 que trae, además de mejoras de rendimiento, nuevas característica como son el uso de SwiftNIO 2, el uso de Leaf como render de presentación por defecto, rediseño del Model API y soporte de HTTP/2 por defecto y como novedad la integración de APNS (Apple Push Notification Server) mediante el paquete APNSwift.

Instalando Vapor 4 (beta)

Lo primero que necesitamos es tener Vapor en nuestro equipo y para hacerlo vamos a recurrir a Homebrew, un gestor de paquetes para Mac.

Si lo tienes en tu equipo puedes saltar al siguiente paso, pero si no lo tienes basta con que abras la app Terminal y ejecutes el siguiente comando

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

Ahora que tienes instalado Homebrew tienes que comprobar si Xcode está presente en tu sistema. Si no lo tienes puedes descargarlo desde la Mac App Store

Ya tenemos Homebrew y Xcode, así que ya podemos instalar Vapor, y lo haremos abriendo nuevamente la app Terminal y ejecutando este comando

brew install vapor/tap/vapor-beta

# En caso de que de un error puedes probar con...

brew install vapor/tap/vapor-beta --HEAD

Cuando finalice el proceso comprobamos que Vapor esté instalado correctamente ejecutando el siguiente comando

vapor-beta --help

Si todo ha ido bien veremos algo como esto…

Creando el proyecto web

Una vez instalado Vapor vamos a crear una nueva app usando el comando

vapor-beta new TalWS

Los parámetros que pasamos son new, que indica que queremos crear una nueva app y el segundo parámetro es el nombre de la app, que además hace las veces de nombre de la carpeta donde se genera la solución.

Durante la creación se nos pregunta si queremos usar Fluent, que es la capa de acceso a datos que incorpora Vapor. Permite acceder a bases de datos PostgreSQL, MySQL, SQLite y MongoDB.

Ahora ya podemos empezar a añadir código al proyecto, pero, dónde se debe añadir cada componente…

Estructura de carpetas de un proyecto Vapor

En principio, se podría añadir cualquier archivo de código fuente donde queremos dentro de la carpeta App, pero esto no es nada recomendable, lo mejor es seguir la estructura de carpetas que nos proponen desde el equipo de Vapor.

.
├── Public
├── Sources
│   ├── App
│   │   ├── Controllers
│   │   ├── Migrations
│   │   ├── Models
│   │   ├── app.swift
│   │   ├── configure.swift
│   │   └── routes.swift
│   └── Run
│       └── main.swift
├── Tests
│   └── AppTests
└── Package.swift

Public

En esta carpeta situamos todos archivos que serán accesibles al público en general, como páginas web HTML, hojas de estilo CSS, imágenes, etc. Por ejemplo, si el usuario solicita el recurso http://localhost:8080/login.html Vapor buscará el archivo login.html en la carpeta Public.

Para habilitarlo tenemos que añadir el FileMiddleware

Sources

Todos los archivos de código fuente van en esta carpeta que se organiza de la siguiente forma

App

Toda la lógica de la aplicación se sitúa dentro de esta carpeta.

  • Controllers. Aquí las clases o estructuras que gestionan los distintos procesos de nuestra aplicación. Reciben datos de las peticiones y devuelve toda, o parte, de la respuesta.
  • Migrations. Aquí van las sucesivas migraciones que hagamos de nuestro modelo de datos si usamos Fluent.
  • Models. Los modelos de datos se situan aquí., sean de una bases de datos Fluent o no.
  • app.swift. Desde donde se inicia la app. También es el punto de entrada para los test que podemos programar.
  • configure.swift Si te preguntas donde tienes que inicializar la base de datos, preparar el enrutamiento, etc. la respuesta es en este archivo.
  • routes.swift. Todas las URL y protocolos que soporte nuestra app se definen aquí. Es el punto de entrada junto con los archivos albergados en la carpeta Public.
Run

Sólo esta el archivo main.swift

Test

Todos los test unitarios que escribas van dentro de estar carpeta. Los test se desarrollan usando el framework XCTest que ya conocemos de nuestros proyectos de iOS, macOS, etc…

AppTests

Todos los test que tengan como objetivo probar el código contenido en la carpeta App deben situarse aquí

Se ejecutan con el comando

swift test

Package.swift

El archivo de definición del paquete Swift según el Swift Package Manager.

Definiendo la aplicación

Para mostrar en que consisten los WebSockets y como funcionan voy a desarrollar un chat como los de los inicios de IRC.

La aplicación tendrá un servicio HTTP que soporta con los siguientes endpoints

  • POST /login Identifica a los usuarios en el chat
  • GET /rooms Lista todas las salas creadas hasta ese momento
  • GET /rooms/:name Muestra todos los usuario de la sala que se pasa como parámetro en la URL

Además tendrá el endpoint /chat para el servicio de WebSockets

Sobre peticiones HTTP en Vapor…

El punto de entrada a los distintos endpoints lo definimos dentro del archivo routes.swift mediante los helpers de la clase Applications

Estos helpers coinciden con las distintas operaciones soportadas por el protocolo HTTP

  • GET = app.get
  • POST = app.post
  • DELETE = app.delete
  • PATCH = app.patch
  • PUT = app.put

Además pueden recibir una serie de parámetros que sirven para establecer la ruta al recursos que queremos capturar así como la opción de recoger las variables incluidas en la ruta.

De esta manera como tengo definido app.get("rooms", ":name) lo que consigo es capturar y procesar cualquier petición cuya url sea, por ejemplo /rooms/vapor

De esta manera la variable :name equivale a vapor y para recuperar debemos hacerlo de la siguiente manera

Sesiones HTTP

Además del enrutamiento, otra de las necesidad básicas en el desarrollo HTTP son las sesiones.

Recordemos que el protocolo HTTP no maneja el concepto de sesión, fue pensado para servir recursos de forma atómica, sin guardar ninguna información durante la navegación por parte de los usuarios.

Con el tiempo de acabo desarrollando el concepto de sesión entre los distintos frameworks de desarrollo web lo que permitía almacenar objetos y/o valores en un diccionario de datos que el servidor asociaba a cada usuario y permanecía mientras el usuario navegaba por la aplicación web.

Como no podía ser otra manera Vapor da soporte a las sesiones, y se puede configurar para que se almacenen en memoria, por defecto, o en base de datos.

Para activar las sesiones tenemos que abrir el archivo configure.swift y añadir el middleware de control de sesiones.

Y ahora sí, ya podemos guardar y consultar valores almacenados en las sesiones de usuario.

En este caso guardo el nick del usuario y la sala a la que accede en sendas variables de sesión para poder compartirlas entre las operaciones de HTTP y las de WebSocket.

Vapor WebSockets

Lo primero que tenemos que tener claro es qué son los WebSockets. Según la definición oficial…

WebSockets es una tecnología basada en el protocolo ws, este hace posible establecer una conexión continua  full-duplex, entre un cliente y servidor.

MDN web docs

Esto quiere decir que existe un canal de comunicación abierto de forma permanente entre el cliente y el servidor, un comportamiento diametralmente opuesto al del protocolo HTTP 1.1.

Para hacernos una idea de como funciona en realidad vamos a implementar un Chat que permita conversaciones entre varios usuarios. La parte web estará desarrollada con Swift, no podía ser de otra manera, y el cliente será web/javascript.

Antes de empezar…

Debo decirte que este artículo está centrado en los WebSockets, no verás nada sobre Fluent, los controladores son muy básicos y tampoco hay autenticación.

Todo esto lo veremos en próximos artículos de esta serie, usando esta misma aplicación como base sobre la que iremos haciendo cambios para añadir las características sobre las que traten los diferentes artículos

Servidor WebSockets

Los endpoints de WebSockets se añaden usando el API de Routing, exactamente igual que si un endpoint HTTP se tratase, pero en lugar de indicar un método get o post hay que usar el método websocket.

Para que el servidor Vapor ponga a la escucha los endpoints WebSockets debemos añadirlos en el archivo configure.swift, junto a la llamada a routes.

La función websockets la podemos encontrar en el archivo routes.swift

Recibir mensajes de texto o binarios de los cliente

La función websockets que acabamos de ver recibe como parámetro un objeto requesto y otro websocket, siendo este último el que nos va a permitir recoger los mensajes y eventos por parte de los clientes mediante callbacks

Usaremos onText para los mensajes de texto, onBinary para la recepción de datos binarios y para saber cuando se ha cerrado la conexión por parte del cliente tenemos el callback onClose.whenComplete

¿Y cómo sé cuándo un cliente abre la conexión? Simplemente cuando se invoca a la función websockets, ese el momento en que un cliente abre la conexión, y es justo cuando tenemos que implementar los callbacks que acabamos de ver.

Cliente WebSockets

Vapor también soporte la conexión a servidores WebSockets y el envío de mensajes mediante la función send.

Para enviar mensajes de texto basta con pasar un String como parámetros a la función y si lo que queremos en enviar datos binarios debemos pasar un array de UInt8

Entrando en el cliente web

Si tenemos iniciado la aplicación Vapor debemos abrir Safari y apuntar a la URL http://localhost:8080/login.html. En esa página elegimos un nick y el nombre de la sala a la nos queremos conectar.

Esta primera parte se resuelve mediante una llamada HTTP usando el método POST. La aplicación comprueba que no exista un usuario con ese mismo nick y almacena en nuestra sesión el nick y la sala.

En la siguiente pantalla entramos a la sala de chat, donde iremos viendo los mensaje que van enviando otros usuarios. Para simular a presencia de usuario basta con abrir otra pestaña de tu navegador y repetir el proceso de login.

Al final veremos algo como esto…

Cambiar Host y Puerto

Si en lugar de ejecutar Vapor sobre el host y puerto por defecto quieres usar otros sólo debes abrir el archivo configure.swift y añadir las siguiente dos líneas

Compilar e iniciar la aplicación

Para compilar el proyecto y, si todo ha ido bien, arrancarlo debemos volver a Terminal

La compilación se hace mediante la opción build

vapor-beta build

En caso de que finalice de forma satisfactoria podemos lanzar la app con..

vapor-beta run

También puede ser que en algún momento necesites eliminar los archivos temporales que se van generando.

vapor-beta clean

Material del artículo

En este repositorio de GitHub podéis descargar el proyecto que acompaña a este articulo.

Enlaces de interés