Hola a tots, Avui parlarem sobre la implementació de websocket amb arquitectura sense servidor fent servir Node.js amb Typescript per a la comunicació en temps real.
Si proveu de cercar-lo a Internet, trobareu detalls d’implementació , però no hi ha res concret disponible. Per tant, aquí estic construint un ecosistema de websocket complet amb tots els detalls de codi i configuració.
Per al desenvolupament , cal tenirserverless
,npm
i/oyarn
instal·lat globalment. Vegeu com instal·lar YARN i NPM . També prefereixo utilitzar NVM per gestionar les meves versions de Node.
Desenvolupament
Pas 1. Configurar AWS
Configureu l’AWS CLI si encara no ho heu fet, aquí hi ha un article molt bo sobre crear i configurar credencials d’AWS . A continuació, hem de configurar la nostra aplicació.
Pas 2. Projecte d’instal·lació
Creeu i configureu un projecte sense servidor amb mecanografia:
$ sls create --template aws-nodejs-typescript --path <PROJECT-NAME>
On<PROJECT-NAME>
és el nom del teu projecte.
Això genera un boilerplate sense servidor per a la nostra aplicació. A continuació, hem d’anar al nostre nou projectecd <PROJECT-NAME>
i instal·leu les nostres dependències executantyarn install
.
Els fitxers més importants que anirem actualitzant a mesura que desenvolupem la nostra aplicació són elshandler.ts
iserverless.ts
. Elhandler.ts
gestiona les nostres funcions lambda o referències a les nostres funcions lambda. Hauria de ser així:
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 fitxer serverless.ts ens proporciona una plantilla per modelar i subministrar els nostres recursos d’aplicació per a AWS CloudFormation tractant la infraestructura com a codi. Això ens ajuda a crear, actualitzar i fins i tot eliminar recursos fàcilment. Hauria de ser així:
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;
A més, a causa de les obsolescències , haureu d’establir la propietat del servei directament amb el nom del servei:
{
service: 'serverless-websocket-ts',
...
}
I actualitzeu elprovider.apiGateway
objecte de la següent manera:
provider: {
...
apiGateway: {
shouldStartNameWithService: true,
...
},
...
},
Podem executar la nostra funció a l’arrel del nostre projecte$ serverless invoke local --function hello
i hauríeu de veure la resposta:
{
"statusCode": 200,
"body": "{n "message": "Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!",n "input": "n}"
}
El marc sense servidor invoca localment elhello
funció i executa l’exportathello
mètode en elhandler.ts
dossier. Elserverless invoke local
L’ordre ens permet executar les nostres funcions Lambda localment abans de desplegar-les.
A continuació, hem d’instal·lar algunes dependències per a la nostra aplicació:
$ yarn add -D serverless-offline serverless-dotenv-plugin serverless-bundle
$ yarn add aws-sdk uuid @types/uuid
- aws-sdk : ens permet comunicar-nos amb els serveis d’AWS
- uuid — genera identificadors únics per a les entrades de la nostra base de dades
- serverless-offline : aquest connector ens permet executar la nostra aplicació i les funcions Lambda localment
- serverless-dotenv-plugin — per permetre’ns la càrrega
.env
variables al nostre entorn Lambda - serverless-bundle : aquest connector empaqueta de manera òptima les nostres funcions Typescript i assegura que no ens hem de preocupar per instal·lar Babel , Typescript , Webpack , ESLint i una sèrie d’altres paquets. Això vol dir que no necessitem mantenir les nostres pròpies configuracions de paquet web. Així que podem seguir endavant i eliminar el
webpack.config.js
fitxer i la referència a ellserverless.ts
propietat personalitzada:
// FROM
custom: {
webpack: {
webpackConfig: './webpack.config.js',
includeModules: true
}
}// TO
custom: {
}
Pas 3. Recursos de configuració
Ara ens hem de posarregion
,stage
,table_throughputs
,table_throughput
i les variables connections_table . També es configuradynamodb
iserverless-offline
per al desenvolupament 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ó, hem d’actualitzar elprovider.environment
propietat a laserverless.ts
fitxer de la següent manera:
// serverless.ts
provider: {
...
environment: {
...
REGION: '${self:custom.region}',
STAGE: '${self:custom.stage}',
CONNECTIONS_TABLE: '${self:custom.connections_table}',
},
...
},
A continuació, hem d’afegirresources
que s’afegeixen a la vostra pila de CloudFormation quan es desplega la nostra aplicació. Hem de definir els recursos d’AWS en una propietat tituladaresources
.
Podeu llegir més sobre com treballar amb taules i dades a DynamoDB .
Utilitzant la potència de Javascript, podem evitar grans fitxers de definició de serveis dividint-los en fitxers i utilitzant importacions dinàmiques. Això és important perquè a mesura que la nostra aplicació creix, els fitxers de definició separats fan que sigui més fàcil de mantenir.
Per a aquest projecte, organitzarem els nostres recursos per separat i els importaremserverless.ts
. Per fer-ho, primer hem de crear unresources
directori al nostre directori arrel i després creeu undynamodb-tables.ts
fitxer per a les nostres taules DynamoDB:
// At project root
$ touch resources/dynamodb-tables.ts
A continuació, actualitzem eldynamodb-tables.ts
fitxer de la següent manera:
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
}
}
},
}
I importar aserverless.ts
— i establiu-lo a les propietats de recursos tal com es mostra a continuació,
// DynamoDB
import dynamoDbTables from './resources/dynamodb-tables';
const serverlessConfiguration: Serverless = {
service: 'serverless-todo',
...
resources: {
Resources: dynamoDbTables,
}
...
}
module.exports = serverlessConfiguration;
Per executar DynamoDB localment, primer hem d’instal·lar el fitxerserverless-dynamodb-local
connectar:
$ yarn add -D serverless-dynamodb-local
A continuació, hem d’actualitzarserverless.ts
matriu de connectors:
plugins: [
'serverless-bundle',
'serverless-dynamodb-local',
'serverless-offline',
'serverless-dotenv-plugin',
],
Per utilitzar el connector, hem d’instal·lar DynamoDB Local executant-losls dynamodb install
a l’arrel del projecte. Córrersls dynamodb start
l’iniciarà localment:
Pas 4. Integració de Websocket
En aquest pas, crearem un gestor de socket web per connectar-nos amb esdeveniments de websocket.
Podeu consultar aquí els detalls sobre els esdeveniments de websocket.
// create a websocket folder
mkdir websocket
cd websocket
touch handler.ts
touch index.ts
touch schemas.ts
touch broadcast.ts
El fitxer handler.ts serà com es mostra a continuació:
// 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);
L’API-Gateway ofereix 4 tipus de rutes relacionades amb el cicle de vida d’un client ws:
Esdeveniment | Acció |
---|---|
$connect | cridat a la connexió d’un client ws |
$desconnectar t | cridat a la desconnexió d’un client ws (és possible que no es cridi en algunes situacions) |
$per defecte | crida si no hi ha cap controlador per utilitzar per a l’esdeveniment |
crida si el nom de la ruta s’especifica per a un controlador |
A partir d’això, hem creat un cas de canvi, on cada cas farà l’acció adequada relacionada amb aquest esdeveniment.
Esdeveniment | Acció |
---|---|
$connect | crearà una entrada a connections-table , amb connectionId i ttl. |
$desconnectar t | eliminarà la connexió de dynamodb connections-table . |
$per defecte | transmetrà les dades a totes les connexions. |
schema.ts serà responsable de validar l’esquema.
// schema.ts
export default {
type: "object",
properties: {
name: { type: 'string' }
},
required: ['name']
} as const;
index.ts maparà els esdeveniments de websocket amb el controlador:
// index.ts
import { handlerPath } from '@libs/handlerResolver';
export const wsHandler = {
handler: `${handlerPath(__dirname)}/handler.wsHandler`,
events: [
{
websocket: '$connect'
},
{
websocket: '$disconnect',
},
{
websocket: '$default'
}
]
};
Hem de registrar l’exportació de wsHandler a src/functions/index.ts
// src/functions/index.ts
export { default as hello } from './hello';
export { wsHandler } from './websocket';
broadcast.ts conté 2 funcions importants com es mostren a continuació
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ó | Explicació |
---|---|
enviar missatge | accepta connectionId i body com a paràmetres. Llegeix APIG_ENDPOINT de la variable d’entorn i crea una instància d’ ApiGatewayManagementApi i després envia el cos a connectionId donat mitjançant postToConnection . |
getAllConnections | retorna totes les connexions |
Pas 4. Websocket que s’executa localment
En aquest pas, executarem el websocket localment, fent servir fora de línia sense servidor:
Abans de començar, compareu i confirmeu el fitxer serverless.ts tal com es mostra a continuació
// 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
Pas 5: demostració de Websocket
Espero que aquest post us sigui útil!
Unimedia Technology
Aquí a Unimedia Technology tenim un equip de desenvolupadors de BackEnd que us poden ajudar a desenvolupar les vostres aplicacions més complexes