Hola a todos, Así que hoy vamos a discutir sobre la implementación de websocket con serverless mediante el uso de Node.js con Typescript para la comunicación en tiempo real.
Si intenta buscarlo en Internet, encontrará fragmentos de detalles de aplicación , pero no hay nada concreto. Así que, aquí estoy construyendo un ecosistema websocket completo con todo el código y detalles de configuración.
Para el desarrollo, necesita tener serverless
, npm
y/o yarn
instalados globalmente. Vea cómo instalar YARN y NPM. También prefiero usar NVM para gestionar mis versiones de Node.
Desarrollo
Paso 1. Configurar AWS
Configurar AWS CLI si aún no lo ha hecho, hay un artículo muy bueno aquí en crear y configurar las credenciales de AWS. A continuación, tenemos que configurar nuestra aplicación.
Segundo paso. Configurar proyecto
Crear y configurar un proyecto serverless con typescript:
$ sls create --template aws-nodejs-typescript --path <PROJECT-NAME>
Donde <PROJECT-NAME>
es el nombre de su proyecto.
Esto genera un serverless boilerplate para nuestra aplicación. A continuación, tenemos que navegar a nuestro nuevo proyecto cd <PROJECT-NAME>
e instalar nuestras dependencias ejecutando yarn install
.
Los archivos más importantes que actualizaremos a medida que desarrollemos nuestra aplicación son handler.ts
y serverless.ts
. El archivo handler.ts
maneja nuestras funciones lambda o referencias a nuestras funciones lambda. Debería verse así:
import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';
export const hello: APIGatewayProxyHandler = async (event, _context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!',
input: event,
}, null, 2),
};
}
El archivo serverless.ts nos proporciona una plantilla para modelar y aprovisionar los recursos de nuestra aplicación para AWS CloudFormation tratando la infraestructura como código. Esto nos ayuda a crear, actualizar e incluso eliminar recursos fácilmente. Debería verse así:
import type { Serverless } from 'serverless/aws';
const serverlessConfiguration: Serverless = {
service: {
name: 'serverless-websocket-ts',
// app and org for use with dashboard.serverless.com
// app: your-app-name,
// org: your-org-name,
},
frameworkVersion: '>=1.72.0',
custom: {
webpack: {
webpackConfig: './webpack.config.js',
includeModules: true
}
},
// Add the serverless-webpack plugin
plugins: ['serverless-webpack'],
provider: {
name: 'aws',
runtime: 'nodejs12.x',
apiGateway: {
minimumCompressionSize: 1024,
},
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
},
},
functions: {
hello: {
handler: 'handler.hello',
events: [
{
http: {
method: 'get',
path: 'hello',
}
}
]
}
}
}
module.exports = serverlessConfiguration;
Además, debido a las depreciaciones, tendrá que establecer la propiedad de servicio directamente al nombre del servicio:
{
service: 'serverless-websocket-ts',
...
}
Y actualice el objeto provider.apiGateway
de la siguiente manera:
provider: {
...
apiGateway: {
shouldStartNameWithService: true,
...
},
...
},
Podemos ejecutar nuestra función en la raíz de nuestro proyecto $ serverless invoke local --function hello
y deberías ver la respuesta:
{
"statusCode": 200,
"body": "{n "message": "Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!",n "input": "n}"
}
El framework sin servidor invoca localmente la función hello
y ejecuta el método exportado hello
en el archivo handler.ts
. El comando serverless invoke local
nos permite ejecutar nuestras funciones Lambda localmente antes de desplegarlas.
A continuación, tenemos que instalar algunas dependencias para nuestra aplicación:
$ yarn add -D serverless-offline serverless-dotenv-plugin serverless-bundle
$ yarn add aws-sdk uuid @types/uuid
- aws-sdk – nos permite comunicarnos con los servicios de AWS
- uuid – genera identificadores únicos para las entradas de nuestra base de datos
- serverless-offline – este plugin nos permite ejecutar nuestra aplicación y funciones Lambda localmente
-
serverless-dotenv-plugin
– para permitirnos cargar variables
.env
en nuestro entorno Lambda - serverless-bundle – este plugin empaqueta de forma óptima nuestras funciones Typescript y garantiza que no tengamos que preocuparnos de instalar
Babel
,
Typescript
,
Webpack
,
ESLint
y muchos otros paquetes. Esto significa que no necesitamos mantener nuestras propias configuraciones de webpack. Así que podemos seguir adelante y eliminar el archivo
webpack.config.js
y la referencia a ella enserverless.ts
propiedad personalizada:
// FROM
custom: {
webpack: {
webpackConfig: './webpack.config.js',
includeModules: true
}
}// TO
custom: {
}
Paso 3. Recursos de configuración
Ahora necesitamos configurar las variables region
, stage
, table_throughputs
, table_throughput
y connections_table . También configura dynamodb
y serverless-offline
para el desarrollo local.
import type { Serverless } from 'serverless/aws';
const serverlessConfiguration: Serverless = {
service: 'serverless-todo',
frameworkVersion: '>=1.72.0',
custom: {
region: '${opt:region, self:provider.region}',
stage: '${opt:stage, self:provider.stage}',
connections_table: '${self:service}-connections-table-${opt:stage, self:provider.stage}',
table_throughputs: {
prod: 5,
default: 1,
},
table_throughput: '${self:custom.TABLE_THROUGHPUTS.${self:custom.stage}, self:custom.table_throughputs.default}',
dynamodb: {
stages: ['dev'],
start: {
port: 8008,
inMemory: true,
heapInitial: '200m',
heapMax: '1g',
migrate: true,
seed: true,
convertEmptyValues: true,
// Uncomment only if you already have a DynamoDB running locally
// noStart: true
}
},
['serverless-offline']: {
httpPort: 3000,
babelOptions: {
presets: ["env"]
}
}
},
plugins: [
'serverless-bundle',
'serverless-offline',
'serverless-dotenv-plugin',
],
package: {
individually: true,
},
provider: {
name: 'aws',
runtime: 'nodejs12.x',
stage: 'dev',
region: 'eu-west-1',
apiGateway: {
shouldStartNameWithService: true,
minimumCompressionSize: 1024,
},
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
},
},
functions: {
hello: {
handler: 'handler.hello',
events: [
{
http: {
method: 'get',
path: 'hello',
}
}
]
}
},
}
module.exports = serverlessConfiguration;
A continuación, tenemos que actualizar la propiedad provider.environment
en el archivoserverless.ts
de la siguiente manera:
// serverless.ts
provider: {
...
environment: {
...
REGION: '${self:custom.region}',
STAGE: '${self:custom.stage}',
CONNECTIONS_TABLE: '${self:custom.connections_table}',
},
...
},
A continuación, tenemos que añadir resources
que se añaden a su pila CloudFormation cuando se despliega nuestra aplicación. Necesitamos definir los recursos de AWS en una propiedad titulada resources
.
Puede obtener más información sobre cómo trabajar con tablas y datos en DynamoDB.
Utilizando el poder de Javascript, podemos evitar grandes archivos de definición de servicios dividiéndolos en archivos y utilizando importaciones dinámicas. Esto es importante porque a medida que nuestra aplicación crece, los archivos de definición separados facilitan el mantenimiento.
Para este proyecto, organizaremos nuestros recursos por separado y los importaremos a serverless.ts
. Para ello, primero debemos crear un directorio resources
en nuestro directorio raíz y, a continuación, crear un archivo dynamodb-tables.ts
para nuestras tablas de DynamoDB:
// At project root
$ touch resources/dynamodb-tables.ts
A continuación, actualizamos el archivo dynamodb-tables.ts
del siguiente modo:
export default {
ConnectionsTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: '${self:provider.environment.CONNECTIONS_TABLE}',
AttributeDefinitions: [
{ AttributeName: 'connectionId', AttributeType: 'S' }
],
KeySchema: [
{ AttributeName: 'connectionId', KeyType: 'HASH' }
],
ProvisionedThroughput: {
ReadCapacityUnits: '${self:custom.table_throughput}',
WriteCapacityUnits: '${self:custom.table_throughput}'
},
SSESpecification: {
SSEEnabled: true
},
TimeToLiveSpecification: {
AttributeName: 'ttl',
Enabled: true
}
}
},
}
E importar en serverless.ts
– y establecer en las propiedades de los recursos como se indica a continuación,
// DynamoDB
import dynamoDbTables from './resources/dynamodb-tables';
const serverlessConfiguration: Serverless = {
service: 'serverless-todo',
...
resources: {
Resources: dynamoDbTables,
}
...
}
module.exports = serverlessConfiguration;
Para ejecutar DynamoDB localmente, primero debemos instalar el complemento serverless-dynamodb-local
:
$ yarn add -D serverless-dynamodb-local
A continuación, debemos actualizar la matriz de plugins serverless.ts
:
plugins: [
'serverless-bundle',
'serverless-dynamodb-local',
'serverless-offline',
'serverless-dotenv-plugin',
],
Para utilizar el complemento, debemos instalar DynamoDB Local ejecutando sls dynamodb install
en la raíz del proyecto. Ejecutandosls dynamodb start
se iniciará localmente:
Paso 4. Integración de Websocket
En este paso, vamos a crear un manejador de socket web, para conectar con los eventos de websocket.
Puedes consultar aquí en detalle los eventos websocket.
// create a websocket folder
mkdir websocket
cd websocket
touch handler.ts
touch index.ts
touch schemas.ts
touch broadcast.ts
handler.ts será como se indica a continuación:
// handler.ts
import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/apiGateway';
import { formatJSONResponse } from '@libs/apiGateway';
import { middyfy } from '@libs/lambda';
import * as AWS from 'aws-sdk';
import { getAllConnections, sendMessage } from './broadcast';
import schema from './schema';
const config: any = { region: "us-east-1" };
if (process.env.STAGE === process.env.DYNAMODB_LOCAL_STAGE) {
config.accessKeyId = process.env.DYNAMODB_LOCAL_ACCESS_KEY_ID;
config.secretAccessKey = process.env.DYNAMODB_LOCAL_SECRET_ACCESS_KEY;
config.endpoint = process.env.DYNAMODB_LOCAL_ENDPOINT;
}
AWS.config.update(config);
const dynamodb = new AWS.DynamoDB.DocumentClient();
const connectionTable = process.env.CONNECTIONS_TABLE;
const websocketHandler: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (event) => {
console.log(event);
const { body, requestContext: { connectionId, routeKey }} = event;
console.log(body, routeKey, connectionId);
switch (routeKey) {
case '$connect':
await dynamodb.put({
TableName: connectionTable,
Item: {
connectionId,
// Expire the connection an hour later. This is optional, but recommended.
// You will have to decide how often to time out and/or refresh the ttl.
ttl: parseInt((Date.now() / 1000).toString() + 3600)
}
}).promise();
break;
case '$disconnect':
await dynamodb.delete({
TableName: connectionTable,
Key: { connectionId }
}).promise();
break;
case '$default':
default:
const connections = await getAllConnections();
await Promise.all(
connections.map(connectionId => sendMessage(connectionId, body))
);
break;
}
return formatJSONResponse({ statusCode: 200 });
}
export const wsHandler = middyfy(websocketHandler);
La API-Gateway proporciona 4 tipos de rutas relacionadas con el ciclo de vida de un cliente ws:
Evento | Acción |
---|---|
Conectar | llamada a la conexión de un cliente ws |
$desconectar | se ejecuta al desconectar un cliente ws (puede que no se ejecute en algunas situaciones) |
$por defecto | si no hay ningún controlador para el evento |
si se especifica el nombre de la ruta para un gestor |
Basándonos en eso, hemos creado un switch case, donde cada caso hará la acción apropiada relacionada con ese evento.
Evento | Acción |
---|---|
Conectar | creará una entrada en connections-table, con connectionId & ttl. |
$desconectar | eliminará la conexión de la tabla de conexiones de dynamodb. |
$por defecto | transmitirá los datos a todas las conexiones. |
schema. ts se encargará de validar el esquema.
// schema.ts
export default {
type: "object",
properties: {
name: { type: 'string' }
},
required: ['name']
} as const;
index. ts mapeará los eventos websocket con handler:
// index.ts
import { handlerPath } from '@libs/handlerResolver';
export const wsHandler = {
handler: `${handlerPath(__dirname)}/handler.wsHandler`,
events: [
{
websocket: '$connect'
},
{
websocket: '$disconnect',
},
{
websocket: '$default'
}
]
};
Necesitamos registrar wsHandler export en src/functions/index.ts
// src/functions/index.ts
export { default as hello } from './hello';
export { wsHandler } from './websocket';
broadcast.ts contiene 2 funciones importantes como se indica a continuación
import * as AWS from 'aws-sdk';
const config: any = { region: "us-east-1" };
if (process.env.STAGE === process.env.DYNAMODB_LOCAL_STAGE) {
config.accessKeyId = process.env.DYNAMODB_LOCAL_ACCESS_KEY_ID;
config.secretAccessKey = process.env.DYNAMODB_LOCAL_SECRET_ACCESS_KEY;
config.endpoint = process.env.DYNAMODB_LOCAL_ENDPOINT;
}
AWS.config.update(config);
const dynamodb = new AWS.DynamoDB.DocumentClient();
const connectionTable = process.env.CONNECTIONS_TABLE;
export async function sendMessage(connectionId, body) {
try {
const endpoint = process.env.APIG_ENDPOINT;
const apig = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint
});
await apig.postToConnection({
ConnectionId: connectionId,
Data: JSON.stringify(body)
}).promise();
} catch (err) {
// Ignore if connection no longer exists
if (err.statusCode !== 400 && err.statusCode !== 410) {
throw err;
}
}
}
export async function getAllConnections() {
const { Items, LastEvaluatedKey } = await dynamodb.scan({
TableName: connectionTable,
AttributesToGet: ['connectionId']
}).promise();
const connections = Items.map(({ connectionId }) => connectionId);
if (LastEvaluatedKey) {
connections.push(...await getAllConnections());
}
return connections;
}
Función | Explicación |
---|---|
enviarMensaje | acepta connectionId & cuerpo como parámetros. Lee APIG_ENDPOINT de la variable de entorno y crea una instancia de ApiGatewayManagementApi y luego envía el cuerpo al connectionId dado usando postToConnection. |
getAllConnections | devuelve todas las conexiones |
Paso 4. Ejecución local de Websocket
En este paso, ejecutaremos websocket localmente, usando serverless offline:
Antes de empezar, compare y confirme el archivo serverless.ts como se indica a continuación
// serverless.ts
import type { AWS } from '@serverless/typescript';
import hello from '@functions/hello';
import { wsHandler } from '@functions/websocket';
import dynamoDbTables from './dynamodb-tables';
const serverlessConfiguration: AWS = {
service: 'serverless-websocket-ts',
frameworkVersion: '2',
custom: {
region: '${opt:region, self:provider.region}',
stage: '${opt:stage, self:provider.stage}',
prefix: '${self:service}-${self:custom.stage}',
connections_table: '${self:service}-connections-table-${opt:stage, self:provider.stage}',
['serverless-offline']: {
httpPort: 3000,
},
['bundle']: {
linting: false
},
table_throughputs: {
prod: 5,
default: 1,
},
table_throughput: '${self:custom.TABLE_THROUGHPUTS.${self:custom.stage}, self:custom.table_throughputs.default}',
dynamodb: {
stages: ['dev'],
start: {
port: 8008,
inMemory: true,
heapInitial: '200m',
heapMax: '1g',
migrate: true,
seed: true,
convertEmptyValues: true,
// Uncomment only if you already have a DynamoDB running locally
// noStart: true
}
}
},
plugins: [
'serverless-bundle',
'serverless-dynamodb-local',
'serverless-offline',
'serverless-dotenv-plugin',
],
package: {
individually: true,
},
provider: {
name: 'aws',
runtime: 'nodejs14.x',
apiGateway: {
minimumCompressionSize: 1024,
shouldStartNameWithService: true,
},
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
REGION: '${self:custom.region}',
STAGE: '${self:custom.stage}',
APIG_ENDPOINT: 'http://localhost:3001',
CONNECTIONS_TABLE: '${self:custom.connections_table}',
},
lambdaHashingVersion: '20201221',
},
// import the function via paths
functions: {
hello,
wsHandler
},
resources: {
Resources: dynamoDbTables,
}
};
module.exports = serverlessConfiguration;
$ serverless offline start
Serverless: Bundling with Webpack...
Serverless: Watching for changes...
Dynamodb Local Started, Visit: http://localhost:8008/shell
Issues checking in progress...
No issues found.
Serverless: DynamoDB - created table serverless-websocket-ts-connections-table-dev
offline: Starting Offline: dev/us-east-1.
offline: Offline [http for lambda] listening on http://localhost:3002
offline: Function names exposed for local invocation by aws-sdk:
* hello: serverless-websocket-ts-dev-hello
* wsHandler: serverless-websocket-ts-dev-wsHandler
offline: route '$connect'
offline: route '$disconnect'
offline: route '$default'
offline: Offline [websocket] listening on ws://localhost:3001
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ POST | http://localhost:3000/dev/hello │
│ POST | http://localhost:3000/2015-03-31/functions/hello/invocations │
│ │
└─────────────────────────────────────────────────────────────────────────┘
offline: Offline [http for websocket] listening on http://localhost:3001
offline: [HTTP] server ready: http://localhost:3000
offline:
offline: Enter "rp" to replay the last request
Paso 5: Demostración de Websocket
Espero que este post le resulte útil.
Unimedia Technology
En Unimedia Technology contamos con un equipo de Desarrolladores BackEnd que pueden ayudarle a desarrollar sus Aplicaciones más complejas