Echtzeit mit Serverless unter Verwendung von Websocket in AWS

Inhaltsübersicht

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 Eigenschaft serverless.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:

VeranstaltungAktion
$verbindenAufruf beim Verbinden eines ws-Clients
$Trennung der Verbindungwird beim Trennen eines WS-Clients aufgerufen (kann in manchen Situationen nicht aufgerufen werden)
$Standardaufgerufen, 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.

VeranstaltungAktion
$verbindenerstellt einen Eintrag in der Verbindungstabelle mit connectionId und ttl.
$Trennung der Verbindungwird die Verbindung aus der dynamodb-Verbindungstabelle entfernt.
$Standardsendet 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;
}
FunktionErläuterung
sendMessageakzeptiert 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 .
getAllConnectionsgibt 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

Vergessen Sie nicht, dass wir bei Unimedia Experten für neue Technologien sind. Wenden Sie sich an uns, wenn Sie Beratung oder Dienstleistungen benötigen. Wir helfen Ihnen gerne weiter.

Unimedia Technology

Ihr Software-Entwicklungspartner

Wir sind ein innovatives Technologieberatungsunternehmen, das sich auf kundenspezifische Softwarearchitektur und -entwicklung spezialisiert hat.

Unsere Dienstleistungen

Registrieren Sie sich für unsere Updates

Bleiben Sie auf dem Laufenden, bleiben Sie informiert, und lassen Sie uns gemeinsam die Zukunft der Technik gestalten!

Verwandte Lektüre

Tiefer eintauchen mit diesen Artikeln

Entdecken Sie mehr von Unimedia’s Expertenwissen und tiefgreifenden Analysen im Bereich der Softwareentwicklung und Technologie.

Let’s make your vision a reality!

Simply fill out this form to begin your journey towards innovation and efficiency.

Lassen Sie uns Ihre Vision Wirklichkeit werden!

Füllen Sie einfach dieses Formular aus, um Ihre Reise in Richtung Innovation und Effizienz zu beginnen.