Temps real sense servidor mitjançant Websocket a AWS

Taula de continguts

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 elwebpack.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:

EsdevenimentAcció
$connectcridat a la connexió d’un client ws
$desconnectar tcridat a la desconnexió d’un client ws (és possible que no es cridi en algunes situacions)
$per defectecrida 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.

EsdevenimentAcció
$connectcrearà una entrada a connections-table , amb connectionId i ttl.
$desconnectar teliminarà la connexió de dynamodb connections-table .
$per defectetransmetrà 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 missatgeaccepta 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 .
getAllConnectionsretorna 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

Recorda que a Unimedia som experts en tecnologies emergents, així que no dubtis a contactar amb nosaltres si necessites assessorament o serveis. Estarem encantats d’ajudar-te.

Unimedia Technology

El teu soci de desenvolupament de software

Som una consultoria tecnològica d’avantguarda especialitzada en arquitectura i desenvolupament de programari personalitzat.

Els nostres serveis

Registra't per rebre les nostres actualitzacions

Estigueu al dia, estigueu informat i donem forma junts al futur de la tecnologia!

Lectures relacionades

Aprofundeix amb aquests articles

Exploreu més coneixements experts d’Unimedia i anàlisis en profunditat en l’àmbit del desenvolupament de programari i la tecnologia.

Let’s make your vision a reality!

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

Fem realitat la teva visió!

Només has d’omplir aquest formulari per començar el teu viatge cap a la innovació i l’eficiència.