Hallo zusammen, heute werden wir über die Implementierung von Websocket mit Serverless diskutieren, indem wir Node.js mit Typescript für die Echtzeitkommunikation verwenden.
Wenn Sie versuchen, im Internet danach zu suchen, werden Sie zwar einige Details zur Umsetzung finden, aber nichts Konkretes. Hier baue ich also ein vollwertiges Websocket-Ökosystem mit allen Code- und Konfigurationsdetails auf.
Für die Entwicklung müssen Sie serverless
, npm
und/oder yarn
global installiert haben. Lesen Sie, wie Sie YARN und NPM installieren. Ich verwende auch lieber NVM, um meine Node-Versionen zu verwalten.
Entwicklung
Schritt 1. AWS konfigurieren
Richten Sie AWS CLI ein, falls Sie das noch nicht getan haben. Hier finden Sie einen sehr guten Artikel über das Erstellen und Konfigurieren von AWS-Anmeldeinformationen. Als nächstes müssen wir unsere Anwendung einrichten.
Schritt 2. Projekt einrichten
Erstellen und Einrichten eines serverlosen Projekts mit Typescript:
$ sls create --template aws-nodejs-typescript --path <PROJECT-NAME>
Dabei ist <PROJECT-NAME>
der Name Ihres Projekts.
Dies erzeugt eine serverlose Boilerplate für unsere Anwendung. Als nächstes müssen wir zu unserem neuen Projekt cd <PROJECT-NAME>
navigieren und unsere Abhängigkeiten installieren, indem wir yarn install
ausführen.
Die wichtigsten Dateien, die wir im Laufe der Entwicklung unserer Anwendung aktualisieren werden, sind die handler.ts
und serverless.ts
. Die Datei handler.ts
behandelt unsere Lambda-Funktionen oder Verweise auf unsere Lambda-Funktionen. Sie sollte folgendermaßen aussehen:
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),
};
}
Die Datei serverless.ts bietet uns eine Vorlage zum Modellieren und Bereitstellen unserer Anwendungsressourcen für AWS CloudFormation, indem die Infrastruktur als Code behandelt wird. Auf diese Weise können wir Ressourcen leicht erstellen, aktualisieren und sogar löschen. Sie sollte folgendermaßen aussehen:
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;
Außerdem müssen Sie aufgrund von Verwerfungen die Eigenschaft service direkt auf service name setzen:
{
service: 'serverless-websocket-ts',
...
}
Und aktualisieren Sie das Objekt provider.apiGateway
wie folgt:
provider: {
...
apiGateway: {
shouldStartNameWithService: true,
...
},
...
},
Wir können unsere Funktion in unserem Projektstamm $ serverless invoke local --function hello
ausführen und Sie sollten die Antwort sehen:
{
"statusCode": 200,
"body": "{n "message": "Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!",n "input": "n}"
}
Das serverlose Framework ruft lokal die Funktion hello
auf und führt die exportierte Methode hello
in der Datei handler.ts
aus. Mit dem Befehl serverless invoke local
können wir unsere Lambda-Funktionen lokal ausführen, bevor sie bereitgestellt werden.
Als nächstes müssen wir ein paar Abhängigkeiten für unsere Anwendung installieren:
$ yarn add -D serverless-offline serverless-dotenv-plugin serverless-bundle
$ yarn add aws-sdk uuid @types/uuid
- aws-sdk – ermöglicht uns die Kommunikation mit AWS-Diensten
- uuid – erzeugt eindeutige IDs für unsere Datenbankeinträge
- serverlos-offline – dieses Plugin ermöglicht es uns, unsere Anwendung und Lambda-Funktionen lokal auszuführen
-
serverless-dotenv-plugin
– damit wir
.env
Variablen in unsere Lambda-Umgebung laden können - serverless-bundle – verpackt dieses Plugin unsere Typescript-Funktionen optimal und sorgt dafür, dass wir uns nicht um die Installation von
Babel
,
Typescript
,
Webpack
,
ESLint
und eine Vielzahl anderer Pakete. Das bedeutet, dass wir unsere eigenen Webpack-Konfigurationen nicht pflegen müssen. Wir können also fortfahren und die Datei
webpack.config.js
und den Verweis auf sie in der benutzerdefinierten Eigenschaftserverless.ts
löschen:
// FROM
custom: {
webpack: {
webpackConfig: './webpack.config.js',
includeModules: true
}
}// TO
custom: {
}
Schritt 3. Ressourcen einrichten
Jetzt müssen wir die Variablen region
, stage
, table_throughputs
, table_throughput
und connections_table setzen. Sie konfiguriert auch dynamodb
und serverless-offline
für die lokale Entwicklung.
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;
Als nächstes müssen wir die Eigenschaft provider.environment
in der Dateiserverless.ts
wie folgt aktualisieren:
// serverless.ts
provider: {
...
environment: {
...
REGION: '${self:custom.region}',
STAGE: '${self:custom.stage}',
CONNECTIONS_TABLE: '${self:custom.connections_table}',
},
...
},
Als Nächstes müssen wir resources
hinzufügen, das bei der Bereitstellung unserer Anwendung zu Ihrem CloudFormation-Stack hinzugefügt wird. Wir müssen AWS-Ressourcen in einer Eigenschaft mit dem Titel resources
definieren.
Sie können mehr über die Arbeit mit Tabellen und Daten in DynamoDB lesen.
Mit der Kraft von Javascript können wir große Dienstdefinitionsdateien vermeiden, indem wir sie in Dateien aufteilen und dynamische Importe verwenden. Das ist wichtig, denn wenn unsere Anwendung wächst, ist sie durch getrennte Definitionsdateien leichter zu pflegen.
Für dieses Projekt werden wir unsere Ressourcen separat organisieren und in serverless.ts
importieren. Dazu müssen wir zunächst ein Verzeichnis resources
in unserem Stammverzeichnis anlegen und dann eine Datei dynamodb-tables.ts
für unsere DynamoDB-Tabellen erstellen:
// At project root
$ touch resources/dynamodb-tables.ts
Als nächstes aktualisieren wir die Datei dynamodb-tables.ts
wie folgt:
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
}
}
},
}
Und importieren Sie in serverless.ts
– und stellen Sie es in den Ressourceneigenschaften wie unten angegeben ein,
// DynamoDB
import dynamoDbTables from './resources/dynamodb-tables';
const serverlessConfiguration: Serverless = {
service: 'serverless-todo',
...
resources: {
Resources: dynamoDbTables,
}
...
}
module.exports = serverlessConfiguration;
Um DynamoDB lokal auszuführen, müssen wir zunächst das Plugin serverless-dynamodb-local
installieren:
$ yarn add -D serverless-dynamodb-local
Als nächstes müssen wir das Array serverless.ts
plugins aktualisieren:
plugins: [
'serverless-bundle',
'serverless-dynamodb-local',
'serverless-offline',
'serverless-dotenv-plugin',
],
Um das Plugin zu verwenden, müssen wir DynamoDB Local installieren, indem wir sls dynamodb install
im Stammverzeichnis des Projekts ausführen. Wenn Siesls dynamodb start
ausführen, wird es lokal gestartet:
Schritt 4. Websocket-Integration
In diesem Schritt werden wir einen Websocket-Handler erstellen, um eine Verbindung mit Websocket-Ereignissen herzustellen.
Sie können hier Details über Websocket-Ereignisse nachlesen.
// create a websocket folder
mkdir websocket
cd websocket
touch handler.ts
touch index.ts
touch schemas.ts
touch broadcast.ts
handler.ts-Datei wird wie unten angegeben aussehen:
// 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);
Das API-Gateway bietet 4 Arten von Routen, die sich auf den Lebenszyklus eines WS-Clients beziehen:
Veranstaltung | Aktion |
---|---|
$verbinden | Aufruf beim Verbinden eines ws-Clients |
$Trennung der Verbindung | wird beim Trennen eines WS-Clients aufgerufen (kann in manchen Situationen nicht aufgerufen werden) |
$Standard | aufgerufen, wenn es keinen Handler für das Ereignis gibt |
wird aufgerufen, wenn der Routenname für einen Handler angegeben ist |
Auf dieser Grundlage haben wir einen Switch-Case erstellt, bei dem jeder Fall eine entsprechende Aktion in Bezug auf dieses Ereignis ausführt.
Veranstaltung | Aktion |
---|---|
$verbinden | erstellt einen Eintrag in der Verbindungstabelle mit connectionId und ttl. |
$Trennung der Verbindung | wird die Verbindung aus der dynamodb-Verbindungstabelle entfernt. |
$Standard | sendet die Daten an alle Verbindungen. |
schema.ts wird für die Validierung des Schemas zuständig sein.
// schema.ts
export default {
type: "object",
properties: {
name: { type: 'string' }
},
required: ['name']
} as const;
index.ts wird die Websocket-Ereignisse mit Handlern abbilden:
// index.ts
import { handlerPath } from '@libs/handlerResolver';
export const wsHandler = {
handler: `${handlerPath(__dirname)}/handler.wsHandler`,
events: [
{
websocket: '$connect'
},
{
websocket: '$disconnect',
},
{
websocket: '$default'
}
]
};
Wir müssen registrieren wsHandler exportieren in src/functions/index.ts
// src/functions/index.ts
export { default as hello } from './hello';
export { wsHandler } from './websocket';
broadcast.ts enthält 2 wichtige Funktionen (siehe unten)
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;
}
Funktion | Erläuterung |
---|---|
sendMessage | akzeptiert connectionId & Körper als Parameter. Es liest APIG_ENDPOINT aus der Umgebungsvariablen und erstellt eine Instanz von ApiGatewayManagementApi und sendet dann den Body mit postToConnection an die angegebene connectionId . |
getAllConnections | gibt alle Verbindungen zurück |
Schritt 4. Lokale Websocket-Ausführung
In diesem Schritt werden wir Websocket lokal ausführen, indem wir Serverless Offline verwenden:
Bevor wir beginnen, vergleichen und bestätigen Sie die Datei serverless.ts wie unten angegeben
// 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
Schritt 5: Websocket-Demo
Ich hoffe, Sie finden diesen Beitrag nützlich!
Unimedia Technology
Hier bei Unimedia Technology haben wir ein Team von BackEnd-Entwicklern, das Ihnen bei der Entwicklung Ihrer komplexesten Anwendungen helfen kann