当express遇见serverless——那可能是一个分布式websocket服务的诞生

253 阅读13分钟

什么是Serverless?

Serverless计算是一种云计算的执行模型,其中云提供商动态管理分配给客户端应用程序的机器资源。与传统的云服务模型不同,Serverless架构允许开发者构建和运行应用和服务而无需关心底层服务器的运行、维护、扩展和管理。这种模型让开发者可以专注于代码的编写和业务逻辑的实现,而将基础设施管理的复杂性交给云服务提供商。

Serverless计算的核心优势包括:

  1. 成本效益:在Serverless模型中,开发者只需为实际消耗的资源和执行时间支付费用,而不是为持续运行的服务器或基础设施付费。这意味着如果应用未被使用,则几乎不会产生费用。
  2. 自动扩展:Serverless平台能够根据应用的需求自动扩展资源,无论是处理几个请求还是数百万个请求。这样,应用总是有足够的资源来处理负载,而无需人工干预。
  3. 管理减轻:由于底层基础设施由云提供商管理,因此开发者不需要担心服务器的运行、安全补丁、软件更新等问题。
  4. 快速部署和更新:Serverless架构使得部署新版本的应用或服务变得更加快速和简单,因为不需要手动配置或管理服务器。

Serverless架构通常与以下概念相关联:

  • 函数即服务(FaaS) :这是Serverless计算中最常见的实现形式。在FaaS模型中,应用被分解为单一功能的函数,每个函数都是独立部署的,且仅在需要时执行。流行的FaaS平台包括AWS Lambda、Azure Functions和Google Cloud Functions。
  • 后端即服务(BaaS) :BaaS提供商提供了开发者在构建应用时所需的后端服务和管理功能,如数据库管理、用户身份验证、远程更新等,而无需开发者管理服务器或运行后端代码。

Serverless计算并不意味着“没有服务器”,而是指服务器的管理和运维工作完全由云服务提供商承担,开发者只需关注于业务逻辑的实现。

省钱、方便、易分锅

什么是Express?

Express是一个灵活的Node.js Web应用框架,提供了一系列强大的功能,帮助开发者快速构建单页、多页或混合Web应用。它被设计为简单、易用,同时又不失灵活性,让Node.js的Web应用开发变得更加高效和愉快。

Express的核心特性包括:

  1. 中间件架构:Express的一个关键特性是其使用中间件的方式处理请求。中间件是一个函数,能够访问请求对象(request)、响应对象(response),以及Web应用中处于请求-响应循环流程中的下一个中间件函数。这种机制允许开发者执行代码、修改请求和响应对象、终结请求-响应循环、调用堆栈中的下一个中间件等。
  2. 路由:Express提供了一种非常强大的路由功能,允许将请求与对应的处理函数关联起来。这不仅可以通过URL的路径部分实现,还可以通过HTTP方法(如GET、POST等)和请求路径的参数来实现更复杂的路由。
  3. 模板引擎支持:Express允许开发者使用模板引擎来生成HTML。这意味着可以将数据传递给模板,然后生成HTML视图来显示给用户。Express支持多种模板引擎,如Pug(原Jade)、Mustache、EJS等。
  4. 简化的多环境配置:Express通过中间件和设置选项,简化了在不同环境(如开发环境和生产环境)下的配置和运行。
  5. 错误处理:Express提供了一个默认的错误处理中间件,并允许开发者自定义错误处理逻辑,以便更好地控制应用的错误输出。
  6. 集成的安全功能:虽然Express本身是一个相对底层的框架,但它通过支持各种安全相关的中间件(如helmet、cors等)使得构建安全的Web应用变得更加容易。

Express的设计哲学是提供小而强大的核心功能集,同时通过丰富的中间件生态系统让开发者可以自由地扩展功能。这种设计使得Express非常适合作为构建Web应用和API的基础。

如果你在node webserver的选型中正在左右为难、不知所措那可以先无脑选Express

Serverless+Express能搞点什么飞机?

将Serverless架构与Express框架结合起来,可以创造出一种既能享受到Serverless带来的众多好处,又能利用Express强大功能和灵活性的开发模式。这种结合方式允许开发者在无需管理服务器的情况下,快速地开发和部署Web应用和API。以下是一些具体的应用场景:

  1. API开发:利用Express的路由和中间件功能,你可以快速构建RESTful API或GraphQL API,然后将其部署到Serverless平台上。这样,API可以根据请求量自动扩展,而且你只需为实际使用的计算时间付费。
  2. Web应用:使用Express,你可以构建服务端渲染(SSR)的Web应用,提供动态内容生成和展示。通过Serverless部署,这些应用可以轻松应对流量高峰,无需担心服务器负载和扩容问题。
  3. 微服务架构:将你的后端服务拆分成多个独立的微服务,每个都可以使用Express开发,然后独立部署到Serverless环境中。这种架构可以提高应用的可维护性和可扩展性,同时降低各个部分的耦合度。
  4. 事件驱动的应用:Serverless平台通常提供与事件源(如数据库更改、文件上传、队列消息等)的集成。你可以使用Express来处理这些事件触发的逻辑,开发出高度响应的应用。
  5. 定时任务:结合Serverless的定时触发功能,你可以使用Express来开发执行定时任务的逻辑,如数据备份、清理、批量处理等操作,而无需担心服务器和计划任务的管理。

要实现这种结合,通常需要使用一些适配器或框架来帮助将Express应用适配到Serverless环境中。例如,AWS上的Serverless Express或者Vercel等平台,它们允许你直接在Serverless环境中运行Express应用,几乎不需要修改原有代码。

总之,通过将Serverless架构与Express框架结合,开发者可以在保留Express开发体验的同时,享受到Serverless带来的伸缩性、成本效益和管理便利性。这种模式适合快速开发和部署现代Web应用和服务。

Serverless快速开始(以阿里云为例)

传送门:serverless-dev快速入门 原文写的很清晰,可以去看看。看下可以跳过下面这些

这为使用Serverless Devs和阿里云函数计算(FC)进行项目开发提供了一个简明的步骤指南。以下是几个主要部分:工具安装、配置阿里云密钥、初始化函数示例、部署函数、调用函数、删除函数。

工具安装

  1. 安装 Node.js 和 npm:确保安装了 Node.js (版本 14.14.0 以上) 和 npm 包管理工具。

  2. 安装 Serverless Devs 开发者工具

    npm install @serverless-devs/s -g
    
  3. 验证安装:使用以下命令检查Serverless Devs工具是否安装成功:

    s -v
    

    成功安装后会显示版本信息,如@serverless-devs/s: 3.0.0

配置阿里云密钥

  1. 获取密钥信息:访问阿里云管理控制台获取AccessKey。

  2. 配置密钥:通过以下命令添加密钥:

    s config add
    

    根据提示选择云服务提供商(例如,选择Alibaba Cloud (alibaba)),然后输入AccessKeyIDAccessKeySecret,最后设置密钥别名。

大写的注意

这个秘钥应该找你们公司运维或者相关人员要,如果是自己的账号或者自己就是管理员。那可以

传送门:阿里云访问控制 自行新建一个或者用已有的

提示!

阿里云等公有云子账号权限(包括密钥对)应该遵循最小权限原则:能不给就不给,能拆分就拆分

初始化函数示例

  1. 创建项目:使用s init命令,根据提示创建一个新的Serverless项目,例如一个Python语言的Hello World项目:

    s init start-fc3-python
    
  2. 进入项目目录:使用cd命令进入项目目录:

    cd start-fc3-python
    

部署函数

在项目目录下,使用s deploy命令一键部署函数。部署成功后,会显示函数的详细信息,如函数名称、内存大小、运行时等。

这个我一般不用,都是用的仓库push触发应用版本更新

调用函数

在项目目录下,使用s invoke命令调用已部署的函数。例如,使用以下命令发送"test"作为事件数据:

s invoke -e "test"

删除函数

在项目目录下,使用s remove命令删除已部署的函数。根据提示确认删除操作。

这个快速入门指南提供了使用Serverless Devs工具和阿里云函数计算进行项目开发和管理的基本步骤,从项目初始化到部署、调用,最后到资源删除,为开发者提供了一个简单明了的操作流程。

从阿里云控制台创建serverless应用

打开阿里云函数计算3.0——应用——创建应用——选择express——立即创建

image-20240508132728534.png

仓库github、gitee、gitlab、阿里云效有什么就可以用什么,其他默认拉到底选创建并部署默认环境

image-20240508132932843.png

耐心等待一会儿,会自动跳转到部署页面

image-20240508133219905.png

部署完成之后会有提醒,点开网址可以看一下默认项目长什么样子

image-20240508133307286.png

示例项目长这样

image-20240508104412045.png 从你上面步骤中选择的仓库克隆代码到本地可以看到这样的结构

express-ws
├── code
| ├── bootstrap
| ├── build.sh
| ├── index.js
| └── package.json
├── readme.md
└── s.yaml

下面是对每个文件和文件夹的简要说明:

  • code文件夹: 包含项目的主要代码。

    • bootstrap: 这个文件用于项目的启动。
    • build.sh: 一个Shell脚本,通常用于自动化构建过程,比如安装依赖、编译源代码或打包应用。
    • index.js: Node.js项目的入口文件,上图中看到的示例项目就是这个文件中代码渲染出来的。
    • package.json: Node.js项目的配置文件,定义了项目的依赖、脚本和其他配置信息。
  • readme.md: 一个Markdown文件,包含示例项目的说明、安装指南和使用方法等。
  • s.yaml: 这是Serverless Framework的配置文件。Serverless Framework是一个无服务器应用框架,允许你定义你的无服务器应用,然后轻松地在云提供商上部署它。s.yaml文件定义了服务的名称、提供者、函数、触发器和其他资源。

添加业务代码

就不重头写业务了,HTML5的新特性(三)——Websocket (附一个ws聊天室demo)示例代码拿来用吧

安装依赖

npm i #安装依赖
npm i ws #安装ws包

入口文件index.js

有一点小改动,进行了express改造以及心跳以及使用express.staticsendFile托管静态文件

const express = require("express");
const http = require("http");
const WebSocket = require("ws");
​
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
​
// 设置静态文件目录
app.use(express.static("static"));
​
const rooms = {}; // 用于存储房间信息的对象
​
wss.on("connection", function connection(ws) {
  console.log("一个新的客户端已连接");
​
  ws.on("message", function incoming(data) {
    const message = JSON.parse(data);
    console.log("接收到消息:", message);
​
    switch (message.type) {
      case "join":
        if (!rooms[message.room]) {
          rooms[message.room] = new Set();
        }
        rooms[message.room].add(ws);
        ws.room = message.room;
        ws.userId = message.userId;
        ws.send(`你好,你已经加入房间 ${message.room}`);
        rooms[message.room].forEach((client) => {
          if (client !== ws && client.readyState === WebSocket.OPEN) {
            client.send(`欢迎 用户 ${message.userId} 加入房间 ${message.room}`);
          }
        });
        break;
      case "message":
        if (rooms[message.room]) {
          rooms[message.room].forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
              client.send(
                `${message.userId} [${new Date()}] : ${message.content}`
              );
            }
          });
        }
        break;
      case "heartbeat":
        ws.send(JSON.stringify({ type: "heartbeat-ack" }));
        break;
    }
  });
​
  ws.on("close", function close() {
    if (ws.room && rooms[ws.room]) {
      rooms[ws.room].delete(ws);
      if (rooms[ws.room].size === 0) {
        delete rooms[ws.room];
      }
    }
    console.log("客户端已断开连接");
  });
});
​
// 设置express路由
app.get("/", (req, res) => {
  res.send("Hello, Express with WebSocket!");
});
​
app.get("/html", (req, res) => {
  // 发送HTML文件到客户端
  res.sendFile(path.join(__dirname, "/static/html/index.html"));
});
​
server.listen(9000, function listening() {
  console.log("服务器启动在 9000 端口");
});
​

前端文件code/static/html/index.html

这个就加了一个5s的心跳发起和ws地址自适应,其他的没有变化

注意,这里要和前面静态文件目录对应上

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket客户端</title>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        // 创建WebSocket连接到本地服务器
        const socket = new WebSocket(
          `ws${location.protocol === "https:" ? "s" : ""}://${location.host}`
        );

        // 设置默认房间号为1001
        document.getElementById("room").value = "1001";

        // 生成随机用户ID
        function generateUserId() {
          return "user_" + Math.random().toString(36).substr(2, 9);
        }

        // 加入房间按钮点击事件
        document.getElementById("joinRoom").addEventListener("click", () => {
          let room = document.getElementById("room").value; // 获取房间号
          let userId = document.getElementById("userId").value; // 获取用户ID
          // 如果用户ID未填写,则生成随机用户ID
          if (!userId) {
            userId = generateUserId();
            document.getElementById("userId").value = userId; // 显示生成的用户ID
          }
          // 发送加入房间的消息到服务器
          socket.send(JSON.stringify({ type: "join", room, userId }));
          setInterval(
            () => socket.send(JSON.stringify({ type: "heartbeat" })),
            5000
          );
        });

        // 发送消息按钮点击事件
        document.getElementById("sendMessage").addEventListener("click", () => {
          const message = document.getElementById("message").value; // 获取消息内容
          const room = document.getElementById("room").value; // 获取房间号
          let userId = document.getElementById("userId").value; // 获取用户ID
          // 如果用户ID未填写,则生成随机用户ID
          if (!userId) {
            userId = generateUserId();
            document.getElementById("userId").value = userId; // 显示生成的用户ID
          }
          // 发送消息到服务器
          socket.send(
            JSON.stringify({ type: "message", room, userId, content: message })
          );
          // 清空消息输入框
          document.getElementById("message").value = "";
        });

        // 接收到服务器消息时的处理
        socket.onmessage = function (event) {
          // 将接收到的消息显示在页面上
          document.getElementById("serverMessages").textContent +=
            event.data + "\n";
        };
      });
    </script>
    <style type="text/css">
      main {
        display: flex;
        justify-content: space-between;
        width: 100vw;
        height: 100vh;
      }
      .message {
        flex: 0 0 75vw;
        height: 100vh;
        position: relative;
      }
      .message-main {
        flex: 0 0 75vw;
        height: 70vh;
        min-height: 100px;
      }
      .send-message {
        flex: 0 0 75vw;
        height: 300px;
        position: absolute;
        bottom: 0;
      }
      .send-message input {
        width: calc(75vw - 100px);
      }
      .login {
        flex: 0 0 20vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <h1>WebSocket客户端</h1>
    <main>
      <div class="message">
        <div
          class="message-main"
          id="serverMessages"
          style="white-space: pre"
        ></div>
        <div class="send-message">
          <input type="text" id="message" />
          <button id="sendMessage">发送消息</button>
        </div>
      </div>
      <div class="login">
        <p>
          <label for="userId">用户ID:</label>
          <input type="text" id="userId" placeholder="留空将自动生成" />
        </p>
        <p>
          <label for="room">房间号:</label>
          <input type="text" id="room" />
        </p>
        <p><button id="joinRoom">加入房间</button></p>
      </div>
    </main>
  </body>
</html>

提交代码,等待部署完成之后再访问serverless默认的网址看看

image-20240508134820968.png

写在后面

本文深入探讨了Serverless计算和Express框架的结合应用,通过详细的介绍和实践指南,展示了如何利用这两种技术构建高效、可扩展的现代Web应用和服务。从Serverless计算的定义、优势,到Express框架的核心特性,再到二者结合的具体应用场景,文章提供了一系列的步骤和示例,帮助开发者快速上手。

通过对Serverless Devs工具和阿里云函数计算的使用,文章详细说明了如何配置环境、部署函数、调用和管理Serverless应用。此外,通过一个WebSocket聊天室的示例,演示了如何在Serverless环境中添加业务逻辑和处理实时通信。

总的来说,结合Serverless计算和Express框架,开发者可以在无需深入管理底层服务器的情况下,快速开发和部署功能丰富、响应迅速的Web应用和服务。这不仅可以提高开发效率,还能根据应用需求自动扩展资源,优化成本。通过本文的引导,开发者可以掌握将Serverless技术和Express框架结合使用的方法,为构建下一代Web应用奠定基础。