使用 Amazon AppSync Events 的 WebSocket 发布功能构建实时应用程序

作者: 布莱斯·佩莱 |

实时功能已成为现代应用程序中必不可少的功能。无论你是在构建协作工具、实时仪表板还是互动游戏,用户在与应用程序交互时都会期待即时和无缝的更新。Amazon AppSync Events 是一项针对无服务器 WebSocket API 的完全托管服务,一直在帮助开发者为其应用添加实时功能,使他们能够大规模构建响应式和引人入胜的体验。

今天,我很高兴地宣布对 Amazon AppSync Events 进行了增强:除了通过 API 的 HTTP 终端节点发布消息外,还能够直接通过 WebSocket 连接发布消息。此更新允许开发人员使用单个 WebSocket 连接来发布和接收事件,从而简化了实时功能的开发并降低了实现复杂性。这对于以前面临为每个消息发布建立新的 HTTP 连接的开销的聊天应用程序尤其有利。开发人员现在拥有更大的灵活性:他们可以通过 HTTP 端点进行发布,例如:用于从后端发布,或者通过 WebSocket 发布,这可能是 Web 和移动应用程序客户端的首选。在这篇文章中,我将介绍新的增强功能,并向你展示如何开始将通过 WebSocket 发布功能集成到应用程序中。

入门

正如我在宣布 Amazon AppSync Events 的帖子中提到的那样,AppSync Events 的入门很简单,现在你可以直接从控制台通过 WebSocket 或 HTTP 进行发布。在 AppSync 控制台中,您可以创建 API 并自动获取default频道命名空间和 API 密钥。在 Pub/Sub 编辑器上,您可以立即试用 API。如下图所示,选择 "发布" 按钮,然后在下拉列表中选择 "WebSocket"。你的活动是通过 WebSocket 发布的。您会收到publish_success一条确认请求的消息。

AppSync Pub/Sub 编辑器显示了带有

在 AppSync 的控制台 Pub/Sub 编辑器中通过 WebSocket 发布

消息格式

AppSync 现在支持新的 "发布" WebSocket 操作来发布事件。连接到 WebSocket 后,您的客户端可以开始将事件发布到已配置的频道命名空间中的频道。请注意,在发布频道之前,您无需订阅该频道。要发布事件,您只需创建数据消息,指定唯一标识消息的 ID、发送消息的频道、发送事件的列表(最多 5 个)以及允许请求的授权标头即可。您可以在文档中找到有关授权标头格式的更多信息。事件数组中的每个事件都必须是有效的 JSON 字符串。这是一个例子。

{
  "type": "publish",
  "id": "an-identifier-for-this-request",
  "channel": "/namespace/my/path",
  "events": [ "{ \"msg\": \"Hello World!\" }" ],
  "authorization": {
    "x-api-key": "da2-12345678901234567890123456-example",
   }
}
JSON

发布后,您将收到 "publish_success" 响应,其中包含发送的每个事件的详细信息,如果操作不成功,则会收到 "publish_error" 响应。

与您的应用程序集成

在上一篇文章中,我向你展示了如何实现一个使用浏览器的 Web API WebSocket 的简单客户端。我将再次这样做,以便在事件 API WebSocket 端点上进行连接和发布。在此示例中,我将构建一个实时演示聊天应用程序,在该应用程序中,客户端可以通过 WebSocket 即时发送和接收消息。消息发布到/default/messages频道,客户端订阅/default/*以接收默认命名空间中的所有消息。

一个名为

演示聊天应用程序

我将使用适用于 AppSync Events 的新的亚马逊云科技云开发套件 (CDK) L2 结构来配置和部署 AppSync Events API,其中包含一个名为 "默认" 的单一频道命名空间和一个 API 密钥。了解有关亚马逊云科技 CDK 入门的更多信息。如果需要,使用节点包管理器安装 CDK CLI。

$ npm install -g aws-cdk
Shell

我首先初始化一个新的文件夹结构并创建我的 CDK 应用程序。

$ mkdir -p events-app/cdk-events-publish
$ cd events-app/cdk-events-publish
$ cdk init app --language javascript
Shell

接下来,我用这个代码更新lib/cdk-events-publish-stack.js文件:

const { Stack, CfnOutput } = require('aws-cdk-lib');
const { EventApi, AppSyncAuthorizationType } = require('aws-cdk-lib/aws-appsync');

class CdkEventsPublishStack extends Stack {
  constructor(scope, id, props) {
    super(scope, id, props);
    const apiKeyProvider = { authorizationType: AppSyncAuthorizationType.API_KEY };

    // create an API called `my-event-api` that uses API Key authorization
    const api = new EventApi(this, 'api', {
      apiName: 'my-event-api',
      authorizationConfig: { authProviders: [apiKeyProvider] }
    });

    // add a channel namespace called `default`
    api.addChannelNamespace('default');

    // output configuration properties
    new CfnOutput(this, 'apiKey', { value: api.apiKeys['Default'].attrApiKey });
    new CfnOutput(this, 'httpDomain', { value: api.httpDns });
    new CfnOutput(this, 'realtimeDomain', { value: api.realtimeDns });
  }
}

module.exports = { CdkEventsPublishStack }
JavaScript

然后我部署堆栈并将输出保存到 JSON 文件中。

$ npm run cdk deploy -- -O output.json
Shell

我得到的输出如下所示:

Outputs:
CdkStack.apiKey = da2-12345678901234567890123456-example
CdkStack.httpDomain = a12345678901234567890123456.appsync-api.us-east-2.amazonaws.com
CdkStack.realtimeDomain = a12345678901234567890123456.appsync-realtime-api.us-east-2.amazonaws.com
Plain text

接下来,我使用 vite(前端构建工具)和 vite 的原始 JavaScript 模板创建 Web 应用程序。在events-app文件夹中,我运行:

$ npm create vite@latest app -- --template vanilla
$ cd app
$ npm install
$ ln -s ../../cdk-events-publish/output.json src
Shell

这会创建指向该output.json文件的链接,我可以在应用程序中引用。在新app文件夹中,我src/main.js用下面的代码替换。请注意,该代码使用中的 CDK 输出。output.json

import './style.css'
import output from './output.json'

// use the output from the CDK stack deployment
const HTTP_DOMAIN = output.CdkEventsPublishStack.httpDomain
const REALTIME_DOMAIN = output.CdkEventsPublishStack.realtimeDomain
const API_KEY = output.CdkEventsPublishStack.apiKey 
     
const authorization = { 'x-api-key': API_KEY, host: HTTP_DOMAIN }

document.querySelector('#app').innerHTML = `
    <style>
      #top{width: calc(100vw - 8rem); max-width: 1280px; background: oklch(0.977 0.013 236.62); margin: 2rem 0; height: calc(100vh - 8rem); box-shadow: inset 0 0 20px rgba(0,0,0,0.1); position: relative; margin-inline: auto;}
      #container{display: flex;flex-direction: column;height: calc(100% - 6.5rem);}
      h2{color: oklch(0.3 0.013 236.62); margin-bottom: 1.5em; font-size: 1.8rem;}
      #messages{display: flex; flex-direction: column-reverse; gap: 1em; max-height: calc(100vh - 200px); flex: 1; min-height: 0; overflow-y: auto; text-align: left;}
      form{position: absolute; bottom: 2rem; left: 2rem; right: 2rem; padding: 1rem 0;}
      input{width: 90%; padding: 0.8em 1.2em; border: 1px solid oklch(0.8 0.013 236.62); border-radius: 25px; font-size: 1rem; outline: none; transition: all 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.05);}
      .msg{padding: 0 1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;}
    </style>
    <div id="top">
      <div id="container"><h2>Messages</h2><div id="messages"></div></div>
      <form id="form"> <input id="messageInput" name="message" type="text" autocomplete="off" /></form>
    </div>`

// construct the protocol header for the connection
function getAuthProtocol() {
  const header = btoa(JSON.stringify(authorization))
    .replace(/\+/g, '-') // Convert '+' to '-'
    .replace(/\//g, '_') // Convert '/' to '_'
    .replace(/=+$/, '') // Remove padding `=`
  return `header-${header}`
}

const socket = await new Promise((resolve, reject) => {
  const socket = new WebSocket(`wss://${REALTIME_DOMAIN}/event/realtime`, [
    'aws-appsync-event-ws',
    getAuthProtocol(),
  ])
  socket.onopen = () => resolve(socket)
  socket.onclose = (event) => reject(new Error(event.reason))
  socket.onmessage = (_evt) => {
    const data = JSON.parse(_evt.data)
    // if this is a `data` event, add the content to the list of messages
    if (data.type === 'data') {
      const event = JSON.parse(data.event)
      const div = document.createElement('div')
      div.className = 'msg'
      div.innerHTML = `${event.time} | ↓ ${new Date().toISOString().split('T')[1]} | ${event.message}`
      messages.prepend(div)
    }
  }
})

// subscribe to `/default/*`
socket.send(
  JSON.stringify({
    type: 'subscribe',
    id: crypto.randomUUID(),
    channel: '/default/*',
    authorization,
  }),
)

const form = document.querySelector('#form')
const messageInput = document.querySelector('#messageInput')
const messages = document.querySelector('#messages')

// when the form is submitted, send an event to `/default/messages`
form.addEventListener('submit', (e) => {
  e.preventDefault()
  const message = new FormData(e.currentTarget).get('message')
  messageInput.value = ''
  socket.send(
    JSON.stringify({
      type: 'publish',
      id: crypto.randomUUID(),
      channel: '/default/messages',
      events: [JSON.stringify({ message, time: new Date().toISOString().split('T')[1] })],
      authorization,
    }),
  )
})
JavaScript

现在在app目录中,我启动网络服务器:

$ npm run dev
Shell

我可以在多个浏览器上使用提供的地址打开网站来发送和接收消息。

正在清理

完成示例后,我可以删除使用 CDK 部署的资源。

$ cd ../cdk-events-publish
$ npm run cdk destroy
Shell

结论

在这篇文章中,我介绍了 Amazon AppSync Events 的新的 "通过 WebSocket 发布" 功能,并展示了如何开始使用该功能。此增强功能提供了多项好处:

  • 使用用于发布和订阅的单一连接简化了实施
  • 减少了聊天应用程序的连接开销
  • 根据您的用例,可以灵活地在 WebSocket 和 HTTP 发布之间进行选择

现在,所有提供 AppSync 的地区都支持通过 WebSocket 进行发布。客户端可以以每个客户端 WebSocket 连接每秒 25 个请求的速度发布请求。您可以继续使用 API 的 HTTP 终端节点以更高的速率进行发布(可调整的默认值为每秒 10,000 个事件)。访问 AppSync 的终端节点和配额页面,了解更多详情。我们很高兴看到你用这项新功能创造了什么。今天就开始试试这篇文章中的示例,或者在现有的 AppSync Events 应用程序中添加 WebSocket 发布功能。要了解有关 AppSync Events 的更多信息,请访问文档。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。