”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 Socket.io 和 Redis 构建和部署聊天应用程序。

使用 Socket.io 和 Redis 构建和部署聊天应用程序。

发布于2024-11-07
浏览:483

Build and deploy a chat application using Socket.io and Redis.

在本教程中,我们将使用 Web 套接字构建一个聊天应用程序。当您想要构建需要实时传输数据的应用程序时,Web 套接字非常有用。

在本教程结束时,您将能够设置自己的套接字服务器、实时发送和接收消息、在 Redis 中存储数据以及在渲染和 google cloud run 上部署您的应用程序。

我们将建造什么?

我们将构建一个聊天应用程序。为了简单起见,我们将只设置服务器。您可以使用自己的前端框架并遵循。

在这个聊天应用程序中,会有房间,用户可以加入房间并开始聊天。为了让一切简单,我们假设用户名不是唯一的。然而,每个房间只能有一个具有特定用户名的用户。

设置套接字服务器。

首先我们需要安装所需的依赖项。

npm i express cors socket.io -D @types/node

我们将使用 http 模块来设置我们的套接字服务器。由于我们的应用程序将在终端中运行,因此我们必须允许所有来源。

import express from "express";
import cors from "cors"
import { Server } from "socket.io";
import { createServer } from "http"

const app = express();
const server = createServer(app);

// create a socket server.
const io = new Server(server, {
  cors: {
    origin: "*",
    credentials: true,
  }
});

// listen to connections errors
io.engine.on("connection_error", console.log)

app.use(cors())

const PORT = 3000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));

设置 Redis。

我们将使用 Redis 来存储我们的消息以及房间和用户信息。您可以使用 upstash redis (免费)。在 upstash 仪表板中创建一个新的 Redis 实例。创建后,您将收到一个 redis url,您可以使用它来连接到您的 redis 实例。

安装您选择的任何 Redis 客户端。我将使用 ioredis。

npm i ioredis

接下来,我们将初始化我们的redis客户端并使用我们获得的连接url将其连接到我们的redis服务器。

/** /src/index.ts */
import { Redis } from "ioredis"

if (!process.env.REDIS_URL) throw new Error("REDIS_URL env variable is not set");
const redis = new Redis(process.env.REDIS_URL);

// listen to connection events.
redis.on("connect", () => console.log("Redis connected"))
redis.on("error", console.log)

处理事件。

用户可以创建房间或加入现有房间。房间由唯一的房间 ID 标识。每个成员都有一个用户名,该用户名在房间内是唯一的,而不是全局的。

我们可以通过将房间 ID 存储在 Redis 集中来跟踪服务器中的所有活动房间。

出于我们的目的,用户名仅在房间内是唯一的。因此,我们将它们与房间 ID 一起存储在一个集合中。这确保了房间 ID 与会员 ID 的组合在全球范围内是唯一的。

我们可以设置套接字事件来创建房间。当我们创建房间时,我们还将请求创建房间的成员添加到房间中。

io.on("connection", () => {
    // ...
    socket.on("create:room", async (message) => {
        console.log("create:room", message)

        const doesRoomExist = await redis.sismember("rooms", message.roomId)
        if (doesRoomExist === 1) return socket.emit("error", { message: "Room already exist."})

        const roomStatus = await redis.sadd("rooms", message.roomId)
        const memStatus = await redis.sadd("members", message.roomId   "::"   message.username)

        if (roomStatus === 0 || memStatus === 0) return socket.emit("error", { message: "Room creation failed." })

        socket.join(message.roomId)
        io.sockets.in(message.roomId).emit("create:room:success", message)
        io.sockets.in(message.roomId).emit("add:member:success", message)
  })
}

要将新成员添加到现有房间,我们首先需要检查该成员是否已存在于该房间中。

io.on("connection", () => {
    // ...
    socket.on("add:member", async (message) => {
        console.log("add:member", message)

        const doesRoomExist = await redis.sismember("rooms", message.roomId)
        if (doesRoomExist === 0) return socket.emit("error", { message: "Room does not exist." })

        const doesMemExist = await redis.sismember("members", message.roomId   "::"   message.username)
        if (doesMemExist === 1) return socket.emit("error", { message: "Username already exists, please choose another username." })

        const memStatus = await redis.sadd("members", message.roomId   "::"   message.username)
        if (memStatus === 0) return socket.emit("error", { message: "User creation failed." })

        socket.join(message.roomId)
        io.sockets.in(message.roomId).emit("add:member:success", message)
  })

    socket.on("remove:member", async (message) => {
        console.log("remove:member", message)

        const doesRoomExist = await redis.sismember("rooms", message.roomId)
        if (doesRoomExist === 0) return socket.emit("error", { message: "Room does not exist." })

        await redis.srem("members", message.roomId   "::"   message.username)

        socket.leave(message.roomId)
        io.sockets.in(message.roomId).emit("remove:member:success", message)
      })
}

最后,我们创建聊天事件。

io.on("connection", () => {
    socket.on("create:chat", (message) => {
    console.log("create:chat", message)
    redis.lpush("chat::"   message.roomId, message.username   "::"   message.message)
    io.sockets.in(message.roomId).emit("create:chat:success", message)
  })
}

部署。

Socket 服务器需要持久连接,它在无服务器环境中无法工作。所以你不能在 vercel 中部署你的套接字服务器。

您可以将其部署在许多地方,例如 Render、fly.io 或 Google Cloud Run。

使成为

在渲染上部署很简单。如果您有 dockerfile,它将自动从该 dockerfile 构建您的项目。渲染有一个免费层,但请记住免费层中会有冷启动。

这是我的 dockerfile。

# syntax=docker/dockerfile:1
ARG NODE_VERSION=20.13.1
ARG PNPM_VERSION=9.4.0

FROM node:${NODE_VERSION}-bookworm AS base

## set shell to bash
SHELL [ "/usr/bin/bash", "-c" ]
WORKDIR /usr/src/app

## install pnpm.
RUN --mount=type=cache,target=/root/.npm \
    npm install -g pnpm@${PNPM_VERSION}

# ------------
FROM base AS deps
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds.
# Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them
# into this layer.
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
    --mount=type=cache,target=/root/.local/share/pnpm/store \
    pnpm install --prod --frozen-lockfile

# -----------
FROM deps AS build
## downloading dev dependencies.
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
    --mount=type=cache,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile

COPY . .
RUN pnpm run build

# -------------
FROM base AS final
ENV NODE_ENV=production
USER node
COPY package.json .

# Copy the production dependencies from the deps stage and also
# the built application from the build stage into the image.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist

EXPOSE 3000
ENTRYPOINT [ "pnpm" ]
CMD ["run", "start"]

谷歌云运行

如果您想要一个免费的替代方案来渲染并避免冷启动,您应该使用 Google Cloud Run。在云运行上部署的步骤超出了本文的范围,但这里有您需要执行的操作的简短列表。

  1. 从下面提供的 dockerfile 构建 docker 镜像。

  2. 使用 Google Artifact Registry 服务创建工件存储库。

  3. 将 docker 镜像重命名为 -docker.pkg.dev//:

  4. 将您的图像推送到您的工件存储库。

  5. 将映像部署到 Google Cloud Run。确保将最小活动实例设置为 1,以避免冷启动。

本教程就是这样。

感谢您的阅读❣️

版本声明 本文转载于:https://dev.to/sammaji/build-and-deploy-a-chat-application-using-socketio-and-redis-438f?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 哪种方法更有效地用于点 - 填点检测:射线跟踪或matplotlib \的路径contains_points?
    哪种方法更有效地用于点 - 填点检测:射线跟踪或matplotlib \的路径contains_points?
    在Python 射线tracing方法 matplotlib路径对象表示多边形。它检查给定点是否位于定义路径内。 This function is often faster than the ray tracing approach, as seen in the code snippet pr...
    编程 发布于2025-02-19
  • 如何在JavaScript对象中动态设置键?
    如何在JavaScript对象中动态设置键?
    如何为JavaScript对象变量创建动态键,尝试为JavaScript对象创建动态键,使用此Syntax jsObj['key' i] = 'example' 1;将不起作用。正确的方法采用方括号:他们维持一个长度属性,该属性反映了数字属性(索引)和一个数字属性的数量。标准对象没有模仿这...
    编程 发布于2025-02-19
  • 如何可靠地检查MySQL表中的列存在?
    如何可靠地检查MySQL表中的列存在?
    在mySQL中确定列中的列存在,验证表中的列存在与与之相比有点困惑其他数据库系统。常用的方法:如果存在(从信息_schema.columns select * * where table_name ='prefix_topic'和column_name =&...
    编程 发布于2025-02-19
  • 如何为PostgreSQL中的每个唯一标识符有效地检索最后一行?
    如何为PostgreSQL中的每个唯一标识符有效地检索最后一行?
    [2最后一行与数据集中的每个不同标识符关联。考虑以下数据: 1 2014-02-01 kjkj 1 2014-03-11 ajskj 3 2014-02-01 sfdg 3 2014-06-12 fdsa 为了检索数据集中每个唯一ID的最后一行信息,您可以在操作员上使用Postgres的有效效...
    编程 发布于2025-02-19
  • 如何使用char_length()在mySQL中按字符串长度对数据进行排序?
    如何使用char_length()在mySQL中按字符串长度对数据进行排序?
    [2使用内置的char_length()函数。:返回字符串中的字符数,考虑多BYTE字符encoding(例如UTF-8)。 ] :返回字符串占用的字节数,该字符的数量可能无法准确反映字符计数多字节编码。 [&& && && && && && &&华从指定的表格中的所有行,并根据指定列的字符长度按升...
    编程 发布于2025-02-19
  • 如何检查对象是否具有Python中的特定属性?
    如何检查对象是否具有Python中的特定属性?
    方法来确定对象属性存在寻求一种方法来验证对象中特定属性的存在。考虑以下示例,其中尝试访问不确定属性会引起错误: >>> a = someClass() >>> A.property Trackback(最近的最新电话): 文件“ ”,第1行, AttributeError:SomeClass实...
    编程 发布于2025-02-19
  • 在映射到MySQL枚举列时,如何确保冬眠保留值?
    在映射到MySQL枚举列时,如何确保冬眠保留值?
    在hibernate中保存枚举值:故障排除错误的列type ,他们各自的映射至关重要。在Java中使用枚举类型时,至关重要的是,建立冬眠的方式如何映射到基础数据库。在您的情况下,您已将MySQL列定义为枚举,并在Java中创建了相应的枚举代码。但是,您遇到以下错误:“ MyApp中的错误列类型。...
    编程 发布于2025-02-19
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    克服go mod中的模块路径差异 github.com/coreos/etcd/integration imports :解析GO.mod:模块将其路径声明为: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&&&&&&&&&&&&& github.com/coreos/b...
    编程 发布于2025-02-19
  • 如何使用PHP从XML文件中有效地检索属性值?
    如何使用PHP从XML文件中有效地检索属性值?
    从php 您的目标可能是检索“ varnum”属性值,其中提取数据的传统方法可能会使您感到困惑。 - > attributes()为$ attributeName => $ attributeValue){ echo $ attributeName,'=“',$ at...
    编程 发布于2025-02-19
  • Java是否允许多种返回类型:仔细研究通用方法?
    Java是否允许多种返回类型:仔细研究通用方法?
    在java中的多个返回类型:一个误解介绍,其中foo是自定义类。该方法声明似乎拥有两种返回类型:列表和E。但是,情况确实如此吗?通用方法:拆开神秘 [方法仅具有单一的返回类型。相反,它采用机制,如钻石符号“ ”。分解方法签名: :本节定义了一个通用类型参数,E。它表示该方法接受扩展FOO类的任何...
    编程 发布于2025-02-19
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-02-19
  • 在没有密码提示的情况下,如何在Ubuntu上安装MySQL?
    在没有密码提示的情况下,如何在Ubuntu上安装MySQL?
    在ubuntu 使用debconf-set-selections 在安装过程中避免密码提示mysql root用户。这需要以下步骤: sudo debconf-set-selections
    编程 发布于2025-02-19
  • 如何以不同的频率控制Android设备振动?
    如何以不同的频率控制Android设备振动?
    控制使用频率变化的Android设备振动是否想为您的Android应用程序添加触觉元素?了解如何触发设备的振动器至关重要。您可以做到这一点:生成基本振动以生成简单的振动,使用振动器对象:这将导致设备在指定的持续时间内振动。许可要求通过上述技术,您可以创建在您的Android应用程序中自定义振动,以增...
    编程 发布于2025-02-19
  • 如何克服PHP的功能重新定义限制?
    如何克服PHP的功能重新定义限制?
    克服PHP的函数重新定义限制在PHP中,多次定义一个相同名称的函数是一个no-no。尝试这样做,如提供的代码段所示,将导致可怕的“不能重新列出”错误。 //错误:“ cance redeclare foo()” 但是,PHP工具腰带中有一个隐藏的宝石:runkit扩展。它使您能够灵活地重新定义...
    编程 发布于2025-02-19

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3