发布于 2026-01-05 1 阅读
0

精通微服务:Node.js、RabbitMQ、Nginx 和 Docker 实战教程;DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

精通微服务:Node.js、RabbitMQ、Nginx 和 Docker 实战教程

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

微服务架构是一种巧妙的应用程序设计方式,它将应用程序拆分成更小、更独立的组件——微服务,每个微服务专注于特定的任务。这些微服务自主运行,支持独立的开发、部署和扩展。这种灵活性使开发人员能够调整或更新特定组件,而无需对整个应用程序进行彻底的重构。这与传统的单体架构截然不同,在单体架构中,整个应用程序被视为一个不可分割的整体,这通常会导致灵活性不足和可扩展性问题。

在深入学习本教程之前,如果您觉得微服务很神秘,可以先阅读我之前的文章,其中有详细的解释。在本实践教程中,我们将使用 Node.js、Socket.io、RabbitMQ 和 Docker 构建一个实时聊天服务器。准备好开启一段微服务世界的实践之旅吧!让我们开始!

先决条件

在开始开发过程之前,请确保您的计算机上已安装并准备以下必备软件:

1. Node.js 和 npm

请确保您已安装Node.js和npm(Node包管理器)。您可以从Node.js官方网站下载它们。

# Check Node.js and npm are installed correctly
node -v
npm -v
Enter fullscreen mode Exit fullscreen mode

2. TypeScript

由于我们将使用 TypeScript 进行增强开发,请使用 npm 全局安装它。

# Install TypeScript globally
npm install -g typescript

# Check TypeScript version
tsc -v
Enter fullscreen mode Exit fullscreen mode

3. Docker

我们将使用 Docker 进行容器化。请从Docker 官方网站下载并安装 Docker 。

4. MongoDB

请确保您已安装 MongoDB 用于数据存储。您可以从MongoDB 官方网站下载 MongoDB 社区服务器,或使用云集群。

5. Nginx

要将 Nginx 设置为反向代理,您可以按照Nginx 官方文档中的安装说明进行操作。

6. 邮递员(可选)

要测试 API 端点,您可以使用 Postman。请从Postman 官方网站下载并安装 Postman 。

完成这些前提条件后,您就可以开始搭建和开发基于微服务的聊天服务器了。让我们进入下一步!

搭建微服务

在开始编写代码之前,让我们先了解一下项目需求和目标。我们的目标是使用微服务架构构建一个实时聊天服务器。主要需求包括:

  • 实时通信:聊天服务器应促进用户之间的即时通讯,确保流畅的实时体验。
  • 可扩展性:该架构应支持轻松扩展各个组件,以适应不断增长的用户群,而不会影响整个系统。
  • 模块化:我们希望我们的系统是模块化的,以便我们可以独立地更新、部署和扩展每个微服务。

聊天服务器结构

为实现我们的目标,我们将利用以下技术和工具:

  • 用于后端服务的 Node.js

    Node.js 是一个功能强大且高效的 JavaScript 运行时环境,以其非阻塞、事件驱动的架构而闻名。它是构建可扩展、响应迅速的后端服务的绝佳选择。

  • TypeScript 增强开发

    我们将使用 TypeScript 引入静态类型来增强开发过程,使我们的代码更加健壮和易于维护。

  • Socket.io 用于实时通信

    Socket.io 是一个库,它支持客户端和服务器之间的实时双向通信。它是构建我们微服务实时聊天功能的关键组件。

  • Docker 用于容器化

    Docker 通过将每个微服务封装到容器中,简化了部署流程。容器确保了不同环境之间的一致性,从而使我们的服务易于部署和扩展。

  • 用于数据存储的 MongoDB

    MongoDB 是一款 NoSQL 数据库,为存储聊天数据提供了一种灵活且可扩展的解决方案。其面向文档的结构与聊天消息的动态特性非常契合。

  • Nginx 作为反向代理

    Nginx 作为反向代理,处理传入的请求并将其分发给相应的微服务。这增强了安全性和负载均衡,并简化了整体架构。

通过结合这些技术,我们为基于微服务的聊天服务器构建了一个强大的基础。接下来的章节将深入探讨实现细节。让我们开始吧!

父文件夹结构

在继续之前,请确保项目结构清晰有序。创建一个名为 `<household_name>` 的文件夹chat-server,用于存放所有微服务。在该文件夹内,添加一个.gitignore文件,将不必要的文件从版本控制中排除。此外,还要添加一个docker-compose.yml文件,用于管理微服务的 Docker 配置。

导航至您首选的目录,然后使用以下命令来实现此操作:

mkdir chat-server && cd chat-server && touch .gitignore && touch docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

现在,打开.gitignore文件并添加以下内容:

**/node_modules
**/.env
**/build
Enter fullscreen mode Exit fullscreen mode

这些条目确保 `<username> node_modules`、.env`<username>` 和build`<username>` 文件夹不会被推送到 GitHub。这种有序的结构为我们构建微服务时简化开发流程奠定了基础。现在,让我们继续开发用户服务。

用户服务

在本节中,我们将深入探讨用户服务的开发,它是我们微服务架构的基础组件。用户服务负责处理用户注册、身份验证以及将用户相关数据存储在 MongoDB 中。

用户服务设计

我们将首先设计用户服务的结构,创建构成微服务骨干的基本文件夹和文件。这包括配置设置、用户身份验证控制器、MongoDB 用户模型、Express 路由、用于 JWT 和密码处理的实用函数以及主服务器配置。

以下是我们的用户服务文件夹结构:

用户服务结构

现在,使用以下命令设置用户服务项目结构,确保您位于项目的根目录中:

创建并导航至您的user-service文件夹:

mkdir user-service && cd user-service
Enter fullscreen mode Exit fullscreen mode

接下来,创建src文件夹并导航到该文件夹​​:

mkdir -p src/config src/controllers src/services src/database/models src/middleware src/routes src/utils
cd src
Enter fullscreen mode Exit fullscreen mode

创建单个文件:

touch config/config.ts controllers/AuthController.ts services/RabbitMQService.ts database/models/UserModel.ts database/connection.ts database/index.ts middleware/index.ts routes/authRoutes.ts utils/index.ts server.ts
Enter fullscreen mode Exit fullscreen mode

创建Dockerfile.dockerignore.env文件:

cd ..
touch Dockerfile .dockerignore .env
Enter fullscreen mode Exit fullscreen mode

以下是每个文件和文件夹的功能描述:

  • user-service文件夹:
    • 描述:用户服务的主文件夹,包含所有相关文件和文件夹。
  • src文件夹:
    • 描述:包含用户服务的源代码,按不同功能组织成子文件夹。
  • config文件夹:
    • 描述:存储用户服务的配置设置。
  • controllers文件夹:
    • 描述:包含 AuthController 文件并处理注册和登录逻辑。
  • services文件夹:
    • 描述:包含我们的 RabbitMQService 文件,并负责处理 RabbitMQ 交互。
  • database文件夹:
    • models文件夹:
      • 描述:包含 UserModel 文件,定义了 Mongoose 用户模型。
    • connection.ts文件:
      • 描述:管理与 MongoDB 数据库的连接。
    • index.ts文件:
      • 描述:作为数据库相关功能的入口点。
  • middleware文件夹:
    • index.ts文件:
      • 描述:作为中间件相关功能的入口点。
  • routes文件夹:
    • authRoutes.ts文件:
      • 描述:定义用于用户身份验证的 Express 路由。
  • utils文件夹:
    • index.ts文件:
      • 描述:包含实用函数,例如自定义错误处理程序和密码相关函数。
  • server.ts文件:
    • 描述:作为 Express 应用设置的入口点。
  • Dockerfile
    • 描述:指定用于容器化用户服务的 Docker 指令。
  • .dockerignore文件:
    • 描述:列出要从 Docker 构建上下文中排除的文件和目录。
  • .env文件:
    • 描述:存储用户服务的环境变量。

接下来,我们来设置环境并安装用户服务所需的依赖项。请使用以下命令,并确保您位于user-service根目录中:

初始化 Node.js 项目

npm init -y
Enter fullscreen mode Exit fullscreen mode

安装依赖项

npm install amqplib bcryptjs dotenv express jsonwebtoken mongoose nodemon ts-node typescript validator @types/express @types/validator @types/amqplib @types/bcryptjs @types/jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

上述命令序列将创建一个package.json文件并安装用户服务所需的 Node.js 包。现在,让我们继续设置 TypeScript:

如果尚未安装 TypeScript,请全局安装它。

npm install -g typescript
Enter fullscreen mode Exit fullscreen mode

初始化 TypeScript 配置

tsc --init
Enter fullscreen mode Exit fullscreen mode

打开生成的tsconfig.json文件,并使用以下配置进行更新:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "tests"
  ]
}
Enter fullscreen mode Exit fullscreen mode

.env现在,让我们通过添加以下内容来设置文件:

NODE_ENV="development"
PORT=8081
JWT_SECRET="{{YOUR_SECRET_KEY}}"
MONGO_URI="{{YOUR_MONGODB_URI}}"
MESSAGE_BROKER_URL="{{YOUR_MESSAGE_BROKER_URL}}"
Enter fullscreen mode Exit fullscreen mode

请将YOUR_SECRET_KEY`<key>` 替换为强密钥,YOUR_MONGODB_URI`<URI>` 替换为你的 MongoDB URI,` <RabbitMQ 消息代理 URI>` 替换为你的 RabbitMQ 消息代理 URI。你可以按照以下步骤从其官方网站YOUR_MESSAGE_BROKER_URL获取这些信息:

  1. 访问CloudAMQP 网站,登录或注册帐户。
  2. 创建一个名为新实例的名称,选择一个区域,然后创建实例。
  3. 从实例详细信息中复制消息代理 URL。

RabbitMQ 网站

最后,我们来设置config.ts文件——该文件位于该src/config文件夹中。复制并粘贴以下代码:

import { config } from "dotenv";

const configFile = `./.env`;
config({ path: configFile });

const { MONGO_URI, PORT, JWT_SECRET, NODE_ENV, MESSAGE_BROKER_URL } =
    process.env;

export default {
    MONGO_URI,
    PORT,
    JWT_SECRET,
    env: NODE_ENV,
    msgBrokerURL: MESSAGE_BROKER_URL,
};
Enter fullscreen mode Exit fullscreen mode

完成这些设置后,我们就可以开始用 TypeScript 编写用户服务代码了。让我们开始吧!

用户模型

要设置我们的 User 模型,请打开UserModel.ts位于src/database/models文件夹中的文件,然后复制并粘贴以下代码:

import mongoose, { Schema, Document } from "mongoose";
import validator from "validator";

export interface IUser extends Document {
    name: string;
    email: string;
    password: string;
    createdAt: Date;
    updatedAt: Date;
}

const UserSchema: Schema = new Schema(
    {
        name: {
            type: String,
            trim: true,
            required: [true, "Name must be provided"],
            minlength: 3,
        },
        email: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
            trim: true,
            validate: [validator.isEmail, "Please provide a valid email."],
        },
        password: {
            type: String,
            trim: false,
            required: [true, "Password must be provided"],
            minlength: 8,
        },
    },
    {
        timestamps: true,
    }
);

const User = mongoose.model<IUser>("User", UserSchema);
export default User;
Enter fullscreen mode Exit fullscreen mode

以下是对上述文件内容的详细分析:

  • IUser界面概述了用户文档的结构,包括诸如name、、、、和之email的字段passwordcreatedAtupdatedAt
  • UserSchema利用 Mongoose 的Schema类,为每个用户文档字段指定类型和约束。
  • name该字段最小长度为 3 个字符,且必须进行截断。
  • email字段为必填项,必须唯一,并且使用验证码验证为有效的电子邮件地址validator.isEmail
  • password该字段为必填项,最少长度为 8 个字符。
  • timestamps: true架构中添加了自动createdAt字段updatedAt来跟踪创建和更新时间。
  • User该模型是使用 Mongoose 的model函数创建的,并将其与IUser接口关联起来UserSchema
  • 最后一步是将User模型导出,以便在应用程序的其他部分中使用。

我们的用户模型定义了 MongoDB 中用户文档的结构和验证规则,为用户服务中处理用户数据奠定了基础。

数据库连接

打开文件夹connection.ts中的文件src/database,并添加以下代码:

import mongoose from "mongoose";
import config from "../config/config";

export const connectDB = async () => {
    try {
        console.info("Connecting to database..." + config.MONGO_URI);
        await mongoose.connect(config.MONGO_URI!);
        console.info("Database connected");
    } catch (error) {
        console.error(error);
        process.exit(1);
    }
};
Enter fullscreen mode Exit fullscreen mode

在上述文件中,我们定义了一个异步connectDB函数,用于建立与 MongoDB 数据库的连接。它使用文件mongoose.connect中指定的 MongoDB URI 来连接到数据库config

接下来,打开文件夹index.ts中的文件src/database,并通过添加以下代码导出所有内容:

import User, { IUser } from "./models/UserModel";
import { connectDB } from "./connection";

export { User, IUser, connectDB };
Enter fullscreen mode Exit fullscreen mode

身份验证控制器

数据库设置完成后,下一步是定义位于AuthController.ts该文件夹中的文件中的身份验证方法src/controllers。复制并粘贴以下代码:

import express, { Request, Response } from "express";
import jwt from "jsonwebtoken";
import { User } from "../database";
import { ApiError, encryptPassword, isPasswordMatch } from "../utils";
import config from "../config/config";
import { IUser } from "../database";

const jwtSecret = config.JWT_SECRET as string;
const COOKIE_EXPIRATION_DAYS = 90; // cookie expiration in days
const expirationDate = new Date(
    Date.now() + COOKIE_EXPIRATION_DAYS * 24 * 60 * 60 * 1000
);
const cookieOptions = {
    expires: expirationDate,
    secure: false,
    httpOnly: true,
};

const register = async (req: Request, res: Response) => {
    try {
        const { name, email, password } = req.body;
        const userExists = await User.findOne({ email });
        if (userExists) {
            throw new ApiError(400, "User already exists!");
        }

        const user = await User.create({
            name,
            email,
            password: await encryptPassword(password),
        });

        const userData = {
            id: user._id,
            name: user.name,
            email: user.email,
        };

        return res.json({
            status: 200,
            message: "User registered successfully!",
            data: userData,
        });
    } catch (error: any) {
        return res.json({
            status: 500,
            message: error.message,
        });
    }
};

const createSendToken = async (user: IUser, res: Response) => {
    const { name, email, id } = user;
    const token = jwt.sign({ name, email, id }, jwtSecret, {
        expiresIn: "1d",
    });
    if (config.env === "production") cookieOptions.secure = true;
    res.cookie("jwt", token, cookieOptions);

    return token;
};

const login = async (req: Request, res: Response) => {
    try {
        const { email, password } = req.body;
        const user = await User.findOne({ email }).select("+password");
        if (
            !user ||
            !(await isPasswordMatch(password, user.password as string))
        ) {
            throw new ApiError(400, "Incorrect email or password");
        }

        const token = await createSendToken(user!, res);

        return res.json({
            status: 200,
            message: "User logged in successfully!",
            token,
        });
    } catch (error: any) {
        return res.json({
            status: 500,
            message: error.message,
        });
    }
};

export default {
    register,
    login,
};
Enter fullscreen mode Exit fullscreen mode

上述文件包含用户注册和登录函数。该register函数检查用户是否已存在,如果存在则创建新用户,并返回成功或错误信息。该login函数验证用户凭据,生成 JWT 令牌,并在登录成功后将其作为 cookie 发送。代码确保了安全高效的身份验证过程,并实现了清晰的职责分离。

路线

现在,让我们定义使用控制器方法的路由。在文件夹authRoutes.ts中的文件中src/routes,添加以下代码:

import { Router } from "express";
import AuthController from "../controllers/AuthController";

const userRouter = Router();

userRouter.post("/register", AuthController.register);
userRouter.post("/login", AuthController.login);

export default userRouter;
Enter fullscreen mode Exit fullscreen mode

此文件设置了用户注册(/register)和用户登录(/login)的路由。这些路由与在相应方法中定义的路由相关联AuthController。ExpressRouter的路由管理工具用于创建和管理这些路由。完成此设置后,我们的 Express 应用现在可以处理发送到这些路由的 HTTP 请求,并调用相应的控制器方法来进行用户注册和登录。

工具

路由设置完毕后AuthController,我们来编写工具类。打开index.ts位于该src/utils目录下的文件,并添加以下代码:

import bcrypt from "bcryptjs";

class ApiError extends Error {
    statusCode: number;
    isOperational: boolean;

    constructor(
        statusCode: number,
        message: string | undefined,
        isOperational = true,
        stack = ""
    ) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        if (stack) {
            this.stack = stack;
        } else {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

const encryptPassword = async (password: string) => {
    const encryptedPassword = await bcrypt.hash(password, 12);
    return encryptedPassword;
};

const isPasswordMatch = async (password: string, userPassword: string) => {
    const result = await bcrypt.compare(password, userPassword);
    return result;
};

export { ApiError, encryptPassword, isPasswordMatch };
Enter fullscreen mode Exit fullscreen mode

在这个实用模块中,我们定义了三个关键要素:

  1. ApiError:此自定义错误类旨在处理与 API 相关的错误。它接受状态码、消息、运行状态和堆栈跟踪等参数。
  2. encryptPassword功能:此异步函数使用 bcrypt 算法对密码进行哈希处理。它接受一个明文密码作为输入,并返回哈希后的密码。
  3. isPasswordMatch函数:另一个异步函数,用于检查提供的密码是否与存储的哈希值匹配。它接受明文密码和存储的哈希密码作为输入,并返回一个布尔值结果。

这些实用函数在增强用户服务的安全性和错误处理能力方面发挥着至关重要的作用。借助这些实用函数,我们的应用程序能够实现强大的密码加密和一致的错误管理。

中间件

index.ts在文件夹中的文件src/middleware,添加以下代码:

import { ErrorRequestHandler } from "express";
import { ApiError } from "../utils";

export const errorConverter: ErrorRequestHandler = (err, req, res, next) => {
    let error = err;
    if (!(error instanceof ApiError)) {
        const statusCode =
            error.statusCode ||
            (error instanceof Error
                ? 400 // Bad Request
                : 500); // Internal Server Error
        const message =
            error.message ||
            (statusCode === 400 ? "Bad Request" : "Internal Server Error");
        error = new ApiError(statusCode, message, false, err.stack.toString());
    }
    next(error);
};

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
    let { statusCode, message } = err;
    if (process.env.NODE_ENV === "production" && !err.isOperational) {
        statusCode = 500; // Internal Server Error
        message = "Internal Server Error";
    }

    res.locals.errorMessage = err.message;

    const response = {
        code: statusCode,
        message,
        ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
    };

    if (process.env.NODE_ENV === "development") {
        console.error(err);
    }

    res.status(statusCode).json(response);
    next();
};
Enter fullscreen mode Exit fullscreen mode

在这些中间件函数中:

  1. errorConverter:将非 API 错误转换为 API 错误,确保错误处理的一致性。它会检查错误是否为 API 实例ApiError,如果不是,则创建一个ApiError包含相关详细信息的新实例。
  2. errorHandler:处理 API 错误。它会为生产环境中的非操作性错误设置相应的状态码和消息。响应负载包含错误代码、消息,以及(在开发模式下)堆栈跟踪。在开发模式下,错误会记录在控制台中。此中间件确保 API 错误得到标准化且信息丰富的响应。

RabbitMQ 服务

现在,让我们来设置 RabbitMQ 服务。打开RabbitMQService.ts位于src/services文件夹中的文件,并添加以下代码:

import amqp, { Channel, Connection } from "amqplib";
import config from "../config/config";
import { User } from "../database";
import { ApiError } from "../utils";

class RabbitMQService {
    private requestQueue = "USER_DETAILS_REQUEST";
    private responseQueue = "USER_DETAILS_RESPONSE";
    private connection!: Connection;
    private channel!: Channel;

    constructor() {
        this.init();
    }

    async init() {
        // Establish connection to RabbitMQ server
        this.connection = await amqp.connect(config.msgBrokerURL!);
        this.channel = await this.connection.createChannel();

        // Asserting queues ensures they exist
        await this.channel.assertQueue(this.requestQueue);
        await this.channel.assertQueue(this.responseQueue);

        // Start listening for messages on the request queue
        this.listenForRequests();
    }

    private async listenForRequests() {
        this.channel.consume(this.requestQueue, async (msg) => {
            if (msg && msg.content) {
                const { userId } = JSON.parse(msg.content.toString());
                const userDetails = await getUserDetails(userId);

                // Send the user details response
                this.channel.sendToQueue(
                    this.responseQueue,
                    Buffer.from(JSON.stringify(userDetails)),
                    { correlationId: msg.properties.correlationId }
                );

                // Acknowledge the processed message
                this.channel.ack(msg);
            }
        });
    }
}

const getUserDetails = async (userId: string) => {
    const userDetails = await User.findById(userId).select("-password");
    if (!userDetails) {
        throw new ApiError(404, "User not found");
    }

    return userDetails;
};
export const rabbitMQService = new RabbitMQService();
Enter fullscreen mode Exit fullscreen mode

在我们的 RabbitMQ 服务中:

  • 该类RabbitMQService初始化 RabbitMQ 连接和通道。它建立与 RabbitMQ 服务器的连接,创建通道,并确认请求队列和响应队列的存在。
  • listenForRequests方法监听请求队列中的传入消息,处理用户详细信息请求,向响应队列发送响应,并确认已处理的消息。
  • getUserDetails函数根据提供的用户 ID 从 MongoDB 中获取用户详细信息。
  • RabbitMQService最后,当文件导入时,会创建一个实例来初始化 RabbitMQ 连接。

RabbitMQ 的工作原理

把 RabbitMQ 想象成应用程序的邮局。它帮助软件的不同部分之间传递消息,从而实现通信。

  1. 消息代理(邮局): RabbitMQ 就像中央邮局,位于中心位置,随时准备接收、存储和发送消息。在我们的代码中,该类RabbitMQService代表这个中央邮局,它负责初始化与 RabbitMQ 的连接并设置必要的队列。
  2. 发送方(生产者):发送方就像应用程序中想要发送消息的部分。在我们的代码中,发送方没有被显式地展示,而是通过生产消息的概念来表示。消息会被发送到 RabbitMQ 交换机。
  3. 消息(信函):消息是我们想要分享的信息。在代码中,消息是包含一个userId字段的 JSON 对象。这些消息会被发送到 RabbitMQ 请求队列。
  4. 交换中心(邮箱):发送邮件之前,需要先将其放入邮箱。在 RabbitMQ 中,这个邮箱被称为交换中心。根据不同的用途,可以有不同类型的邮箱。在我们的代码中,它requestQueue充当邮箱(交换中心),用于接收和处理消息。
  5. 路由(投递指令):有时,您希望邮件发送到特定部门或收件人。在 RabbitMQ 中,路由指定消息的发送目的地。虽然我们的代码中没有明确显示路由,但它的requestQueue作用是将消息定向到正确的目的地。
  6. 接收者(消费者):在另一端,有人正在等待接收信件。接收者就像应用程序中等待接收和处理消息的部分。在我们的代码中,该listenForRequests函数充当消费者的角色,等待消息requestQueue
  7. 邮件分拣队列:邮件在到达收件人之前,会先按类别分拣到不同的队列中。每种类型的邮件(信息)都有其对应的队列。邮件requestQueue在处理之前会被放入队列中。
  8. 送达消费者:信件分类完成后,将送达相应的收件人(消费者),以便他们处理信息。该listenForRequests功能处理来自用户的消息requestQueue,并将用户详细信息回复发送给用户responseQueue
  9. 确认(已接收确认):在 RabbitMQ 中,确认机制用于确认消息已成功处理。我们的代码使用this.channel.ack(msg)确认机制来确认已收到已处理的消息。

总而言之,这段RabbitMQService.ts代码模拟了这样一个场景:消息(用户详情请求)被发送到中央邮件服务器(RabbitMQ),在邮箱(队列)中进行分类,然后由接收方(消费者)进行处理。确认机制确保处理状态能够反馈给 RabbitMQ。这种设置使得使用 RabbitMQ 的应用程序不同部分之间能够高效通信。

服务器设置和初始化

一切就绪后RabbitMQService.ts,最后一块拼图就是这个server.ts文件,它负责协调 Express 服务器的设置、连接数据库并初始化 RabbitMQ 客户端。以下是代码:

import express, { Express } from "express";
import { Server } from "http";
import userRouter from "./routes/authRoutes";
import { errorConverter, errorHandler } from "./middleware";
import { connectDB } from "./database";
import config from "./config/config";
import { rabbitMQService } from "./services/RabbitMQService";

const app: Express = express();
let server: Server;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(userRouter);
app.use(errorConverter);
app.use(errorHandler);

connectDB();

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

const initializeRabbitMQClient = async () => {
    try {
        await rabbitMQService.init();
        console.log("RabbitMQ client initialized and listening for messages.");
    } catch (err) {
        console.error("Failed to initialize RabbitMQ client:", err);
    }
};

initializeRabbitMQClient();

const exitHandler = () => {
    if (server) {
        server.close(() => {
            console.info("Server closed");
            process.exit(1);
        });
    } else {
        process.exit(1);
    }
};

const unexpectedErrorHandler = (error: unknown) => {
    console.error(error);
    exitHandler();
};

process.on("uncaughtException", unexpectedErrorHandler);
process.on("unhandledRejection", unexpectedErrorHandler);
Enter fullscreen mode Exit fullscreen mode

server.ts文件执行以下操作:

  1. Express 服务器设置:初始化 Express 应用程序,设置 JSON 和表单数据解析的中间件,添加路由,并包含错误处理中间件。
  2. 数据库连接:使用该函数连接到 MongoDB 数据库connectDB
  3. 服务器启动:启动 Express 服务器,监听config文件中指定的端口。
  4. RabbitMQ 客户端初始化:调用函数initializeRabbitMQClient使用我们的RabbitMQService.
  5. 优雅关闭:实现了优雅关闭机制。当服务器关闭时(无论是主动关闭还是由于错误),它会记录关闭事件并退出进程。
  6. 错误处理:注册未捕获异常和未处理拒绝的事件监听器,确保在发生意外错误时进行正确的错误处理和优雅关闭。

有了这个文件,我们的用户服务微服务架构就完成了,可以处理用户注册、登录和 RabbitMQ 通信了。

脚本更新

服务器搭建完成后,务必更新配置package.json文件,添加便于服务器开发、构建和启动的脚本。打开配置package.json文件并添加以下代码:

"scripts": {
    "dev": "NODE_ENV=development nodemon src/server.ts",
    "build": "rm -rf build/ && tsc -p .",
    "start": "NODE_ENV=production nodemon build/server.js"
},
Enter fullscreen mode Exit fullscreen mode

然后,确保"main"标签已设置"src/server.ts"。这些脚本为开发过程的不同阶段提供了便捷的命令:

  • "dev":使用 . 以开发模式启动服务器nodemon
  • "build":清除现有的构建文件夹并使用 TypeScript 编译器编译 TypeScript 文件(tsc)。
  • "start":使用已编译的 JavaScript 文件以生产模式启动服务器。

这种设置可以优化开发工作流程,并便于在生产环境中部署。

现在,让我们看看用户服务是如何运行的。打开终端,确保你位于user-service根目录。运行以下命令,根据你的包管理器启动开发服务器:

npm run dev
Enter fullscreen mode Exit fullscreen mode

或者

yarn dev
Enter fullscreen mode Exit fullscreen mode

此命令会将服务器启动到开发模式,并启用实时重载nodemon。服务器运行后,您可以使用 Postman 或任何 API 工具来测试 API 端点,确保一切运行正常。测试 `add_register`/register和 ` /loginadd_login` 端点,以验证用户注册和登录功能。

聊天服务

聊天服务支持用户间的实时沟通,从而在我们的应用程序中营造动态且互动性强的用户体验。我们将构建项目结构,设置依赖关系,并创建必要的文件和文件夹,为构建聊天服务奠定坚实的基础。请按照分步指南,将此服务无缝集成到我们的微服务生态系统中。让我们开始吧!

以下是我们聊天服务的结构:

聊天服务结构

现在,请使用以下命令设置聊天服务项目结构,确保您位于项目的根目录中:

创建并导航至您的chat-service文件夹:

mkdir chat-service && cd chat-service
Enter fullscreen mode Exit fullscreen mode

创建src文件夹并进入该文件夹:

mkdir -p src/config src/controllers src/services src/database/models src/middleware src/routes src/utils
cd src
Enter fullscreen mode Exit fullscreen mode

创建单个文件:

touch config/config.ts controllers/MessageController.ts services/RabbitMQService.ts database/models/MessageModel.ts database/connection.ts database/index.ts middleware/index.ts routes/messageRoutes.ts utils/apiError.ts utils/messageHandler.ts utils/userStatusStore.ts utils/index.ts app.ts server.ts
Enter fullscreen mode Exit fullscreen mode

创建Dockerfile.dockerignore.env文件:

cd ..
touch Dockerfile .dockerignore .env
Enter fullscreen mode Exit fullscreen mode

以下是每个文件和文件夹的描述:

  • config包含我们聊天服务的配置文件。该config.ts文件包含环境变量和配置信息。
  • controllers这里,我们管理处理文件中消息的业务逻辑MessageController.ts。消息操作也在这里定义。
  • services此文件夹中的文件RabbitMQService.ts处理与 RabbitMQ 消息代理的交互,从而促进微服务之间的通信
  • database/models在这个文件夹中,我们使用以下方式定义消息的数据模型MessageModel.ts
  • database:connection.ts文件index.ts处理数据库连接和导出模型。
  • middleware此文件夹中的文件index.ts整合了我们稍后将使用的中间件函数
  • routes此文件夹中的文件messageRoutes.ts定义了消息相关操作的路由及其相应的处理程序
  • utils此文件夹包含实用工具文件,例如用于apiError.ts处理 API 错误、messageHandler.ts管理传入消息和userStatusStore.ts跟踪用户状态的文件。该index.ts文件整合了来自 utils 文件夹的导出文件。
  • app.ts此文件是我们聊天服务应用程序的入口点
  • server.ts这里,我们搭建 Express 服务器,配置中间件,并建立路由。

这些文件和文件夹共同构成了我们聊天服务的基础结构,从而可以有条不紊地开发各种特性和功能。

接下来,我们来设置环境并安装必要的依赖项。请使用以下命令,并确保您位于chat-service根目录中:

初始化Node.js项目:

npm init -y
Enter fullscreen mode Exit fullscreen mode

安装依赖项

npm install amqplib dotenv express jsonwebtoken mongoose nodemon ts-node typescript socket.io uuid @types/express @types/amqplib @types/jsonwebtoken @types/uuid
Enter fullscreen mode Exit fullscreen mode

此命令序列将创建一个package.json文件并安装聊天服务所需的 Node.js 包。

接下来我们进行 TypeScript 配置。运行以下命令:

tsc --init
Enter fullscreen mode Exit fullscreen mode

打开生成的tsconfig.json文件,并使用以下配置进行更新:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "tests"
  ]
}
Enter fullscreen mode Exit fullscreen mode

将以下代码添加到您的.env文件中:

NODE_ENV="development"
PORT=8082
JWT_SECRET="{{YOUR_SECRET_KEY}}"
MONGO_URI="{{YOUR_MONGODB_URI}}"
MESSAGE_BROKER_URL="{{YOUR_MESSAGE_BROKER_URL}}"
Enter fullscreen mode Exit fullscreen mode

请将密钥替换YOUR_SECRET_KEY为强密钥,将 MongoDB URI 替换YOUR_MONGODB_URI为你的 MongoDB URI,并将YOUR_MESSAGE_BROKER_URLRabbitMQ 消息代理 URL 替换为你在用户服务中使用的相同 URL。

现在,让我们来设置config.ts位于该src/config文件夹中的文件。打开该文件并添加以下代码:

import { config } from "dotenv";

const configFile = `./.env`;
config({ path: configFile });

const { MONGO_URI, PORT, JWT_SECRET, NODE_ENV, MESSAGE_BROKER_URL } =
    process.env;

const queue = { notifications: "NOTIFICATIONS" };

export default {
    MONGO_URI,
    PORT,
    JWT_SECRET,
    env: NODE_ENV,
    msgBrokerURL: MESSAGE_BROKER_URL,
    queue,
};
Enter fullscreen mode Exit fullscreen mode

在这个config.ts文件中,我们配置聊天服务所需的环境变量。首先,我们使用库从指定文件中加载这些变量dotenv。加载完成后,我们提取关键信息,例如 MongoDB URI、端口、JWT 密钥、环境类型和消息代理 URL。此外,我们还定义了一个queue对象(具体来说是一个notifications队列),它将在消息代理中使用。这与我们在用户服务中所做的类似。

这种集中式配置方法使我们能够根据需要轻松管理和修改设置。它通过从环境变量加载敏感信息(例如 MongoDB URI 或 JWT 密钥)来确保其安全。该queue对象代表消息代理内部通信使用的特定队列,使我们的消息传递设置更加清晰。

消息模型

一切准备就绪,接下来我们来设置消息模型。打开文件夹MessageModel.ts中的文件src/database/models,并添加以下代码:

import mongoose, { Schema, Document } from "mongoose";

enum Status {
    NotDelivered = "NotDelivered",
    Delivered = "Delivered",
    Seen = "Seen",
}

export interface IMessage extends Document {
    senderId: string;
    receiverId: string;
    message: string;
    status: Status;
    createdAt: Date;
    updatedAt: Date;
}

const MessageSchema: Schema = new Schema(
    {
        senderId: {
            type: String,
            required: true,
        },
        receiverId: {
            type: String,
            required: true,
        },
        message: {
            type: String,
            required: true,
        },
        status: {
            type: String,
            enum: Object.values(Status),
            default: Status.NotDelivered,
        },
    },
    {
        timestamps: true,
    }
);

const Message = mongoose.model<IMessage>("Message", MessageSchema);
export default Message;
Enter fullscreen mode Exit fullscreen mode

MessageModel.ts文件定义了消息的模式。每条消息包含发送者 ID、接收者 ID、消息内容、状态(可以是“未送达”、“已送达”或“已读”)以及创建时间戳和最后更新时间戳。使用 TypeScript 接口确保文档遵循特定的结构。enum状态选项提供了清晰的选项和默认值,从而提高了代码的可读性。此模式将用于与 MongoDB 数据库交互,以存储和检索消息。

数据库连接

打开文件夹connection.ts中的文件src/database,并添加以下代码,就像我们在用户服务中所做的那样:

import mongoose from "mongoose";
import config from "../config/config";

export const connectDB = async () => {
    try {
        console.info("Connecting to database..." + config.MONGO_URI);
        await mongoose.connect(config.MONGO_URI!);
        console.info("Database connected");
    } catch (error) {
        console.error(error);
        process.exit(1);
    }
};
Enter fullscreen mode Exit fullscreen mode

然后,将以下代码添加到index.ts位于该src/database文件夹中的文件中,即可导出:

import Message from "./models/MessageModel";
import { connectDB } from "./connection";

export { Message, connectDB };
Enter fullscreen mode Exit fullscreen mode

消息控制器

现在数据库已经设置好了。接下来我们来编写消息控制器,打开MessageController.ts文件并添加以下代码:

import { Request, Response } from "express";
import { AuthRequest } from "../middleware";
import { Message } from "../database";
import { ApiError, handleMessageReceived } from "../utils";

const send = async (req: AuthRequest, res: Response) => {
    try {
        const { receiverId, message } = req.body;
        const { _id, email, name } = req.user;

        validateReceiver(_id, receiverId);

        const newMessage = await Message.create({
            senderId: _id,
            receiverId,
            message,
        });

        await handleMessageReceived(name, email, receiverId, message);

        return res.json({
            status: 200,
            message: "Message sent successfully!",
            data: newMessage,
        });
    } catch (error: any) {
        return res.json({
            status: 500,
            message: error.message,
        });
    }
};

const validateReceiver = (senderId: string, receiverId: string) => {
    if (!receiverId) {
        throw new ApiError(404, "Receiver ID is required.");
    }

    if (senderId == receiverId) {
        throw new ApiError(400, "Sender and receiver cannot be the same.");
    }
};

const getConversation = async (req: AuthRequest, res: Response) => {
    try {
        const { receiverId } = req.params;
        const senderId = req.user._id;

        const messages = await Message.find({
            $or: [
                { senderId, receiverId },
                { senderId: receiverId, receiverId: senderId },
            ],
        });

        return res.json({
            status: 200,
            message: "Messages retrieved successfully!",
            data: messages,
        });
    } catch (error: any) {
        return res.json({
            status: 500,
            message: error.message,
        });
    }
};

export default {
    send,
    getConversation,
};
Enter fullscreen mode Exit fullscreen mode

MessageController.ts文件定义了处理消息的方法。该send方法允许用户向指定的接收者发送消息,并执行验证以确保提供的接收者 ID 与发送者的 ID 不同。此外,还有一个handleMessageReceived函数用于处理新消息的接收情况,并在接收者收到新消息时通知他/她,RabbitMQService该函数我们将在稍后定义。

getConversation方法根据用户 ID 检索两个用户之间的消息。同时,该方法包含错误处理机制,用于管理操作过程中可能出现的问题。

消息路由

将以下代码添加到messageRoutes.ts位于该src/routes文件夹中的文件中:

import { Router } from "express";
import MessageController from "../controllers/MessageController";
import { authMiddleware } from "../middleware";

const messageRoutes = Router();

// @ts-ignore
messageRoutes.post("/send", authMiddleware, MessageController.send);
messageRoutes.get(
    "/get/:receiverId",
    // @ts-ignore
    authMiddleware,
    MessageController.getConversation
);

export default messageRoutes;
Enter fullscreen mode Exit fullscreen mode

messageRoutes.ts文件设置了发送消息(/send)和检索对话(/get/:receiverId)的路由。这些路由与在 中定义的相应方法相关联MessageControllerauthMiddleware确保只有经过身份验证的用户才能访问这些路由。通过这些路由,用户可以发送消息和检索他们的对话。

RabbitMQ 服务

src/services文件夹中,打开RabbitMQService.ts文件并添加以下代码:

import amqp, { Channel } from "amqplib";
import { v4 as uuidv4 } from "uuid";
import config from "../config/config";

class RabbitMQService {
    private requestQueue = "USER_DETAILS_REQUEST";
    private responseQueue = "USER_DETAILS_RESPONSE";
    private correlationMap = new Map();
    private channel!: Channel;

    constructor() {
        this.init();
    }

    async init() {
        const connection = await amqp.connect(config.msgBrokerURL!);
        this.channel = await connection.createChannel();
        await this.channel.assertQueue(this.requestQueue);
        await this.channel.assertQueue(this.responseQueue);

        this.channel.consume(
            this.responseQueue,
            (msg) => {
                if (msg) {
                    const correlationId = msg.properties.correlationId;
                    const user = JSON.parse(msg.content.toString());

                    const callback = this.correlationMap.get(correlationId);
                    if (callback) {
                        callback(user);
                        this.correlationMap.delete(correlationId);
                    }
                }
            },
            { noAck: true }
        );
    }

    async requestUserDetails(userId: string, callback: Function) {
        const correlationId = uuidv4();
        this.correlationMap.set(correlationId, callback);
        this.channel.sendToQueue(
            this.requestQueue,
            Buffer.from(JSON.stringify({ userId })),
            { correlationId }
        );
    }

    async notifyReceiver(
        receiverId: string,
        messageContent: string,
        senderEmail: string,
        senderName: string
    ) {
        await this.requestUserDetails(receiverId, async (user: any) => {
            const notificationPayload = {
                type: "MESSAGE_RECEIVED",
                userId: receiverId,
                userEmail: user.email,
                message: messageContent,
                from: senderEmail,
                fromName: senderName,
            };

            try {
                await this.channel.assertQueue(config.queue.notifications);
                this.channel.sendToQueue(
                    config.queue.notifications,
                    Buffer.from(JSON.stringify(notificationPayload))
                );
            } catch (error) {
                console.error(error);
            }
        });
    }
}

export const rabbitMQService = new RabbitMQService();
Enter fullscreen mode Exit fullscreen mode

RabbitMQService.ts文件设置了一个 RabbitMQ 服务,用于处理不同微服务之间的通信。它使用amqplibRabbitMQ 库与 RabbitMQ 进行交互。该服务通过连接到 RabbitMQ 服务器并声明必要的队列来初始化。`getUserDetails` 和 `newMessages` 方法requestUserDetails分别notifyReceiver演示了如何使用 RabbitMQ 请求用户详细信息和通知接收者有关新消息的信息。该服务利用关联 ID 将请求与响应进行匹配,从而确保服务之间的有效通信。

工具

服务和控制器都已准备就绪,接下来我们来编写工具类。在 `<utils>`文件中src/utils,添加以下代码apiError.ts

class ApiError extends Error {
    statusCode: number;
    isOperational: boolean;

    constructor(
        statusCode: number,
        message: string | undefined,
        isOperational = true,
        stack = ""
    ) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        if (stack) {
            this.stack = stack;
        } else {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

export { ApiError };
Enter fullscreen mode Exit fullscreen mode

这个ApiError类与我们用户服务中的类类似,它们的功能相同。

接下来,在userStatusStore.ts文件中,以下代码定义了该类UserStatusStore

export class UserStatusStore {
    private static instance: UserStatusStore;
    private userStatuses: Record<string, boolean>;

    private constructor() {
        this.userStatuses = {};
    }

    public static getInstance(): UserStatusStore {
        if (!UserStatusStore.instance) {
            UserStatusStore.instance = new UserStatusStore();
        }
        return UserStatusStore.instance;
    }

    setUserOnline(userId: string) {
        this.userStatuses[userId] = true;
    }

    setUserOffline(userId: string) {
        this.userStatuses[userId] = false;
    }

    isUserOnline(userId: string): boolean {
        return !!this.userStatuses[userId];
    }
}
Enter fullscreen mode Exit fullscreen mode

该类UserStatusStore是一个单例类,负责管理用户的在线/离线状态。它提供了设置用户在线或离线状态以及检查用户当前是否在线的方法。

最后,在messageHandler.ts文件中,包含以下代码:

import { UserStatusStore } from "./userStatusStore";
import { rabbitMQService } from "../services/RabbitMQService";

const userStatusStore = UserStatusStore.getInstance();

export const handleMessageReceived = async (
    senderName: string,
    senderEmail: string,
    receiverId: string,
    messageContent: string
) => {
    const receiverIsOffline = !userStatusStore.isUserOnline(receiverId);

    if (receiverIsOffline) {
        await rabbitMQService.notifyReceiver(
            receiverId,
            messageContent,
            senderEmail,
            senderName
        );
    }
};
Enter fullscreen mode Exit fullscreen mode

handleMessageReceived功能使用 来检查接收者是否在线UserStatusStore。如果接收者离线,则使用RabbitMQService来通知接收者有新消息。这样即使用户没有主动使用聊天服务,也能收到通知。

现在,将以下代码添加到index.ts文件中,即可导出该实用程序:

import { ApiError } from "./apiError";
import { UserStatusStore } from "./userStatusStore";
import { handleMessageReceived } from "./messageHandler";

export { ApiError, UserStatusStore, handleMessageReceived };
Enter fullscreen mode Exit fullscreen mode

这段代码从工具集中导出 `<string> ApiError`、` UserStatusStore<string>` 和handleMessageReceived`<string>`,使它们可供应用程序的其他部分使用。工具集准备就绪后,让我们定义中间件。

中间件

打开文件夹index.ts中的文件src/middleware,并添加以下代码:

import { Request, Response, NextFunction, ErrorRequestHandler } from "express";
import jwt from "jsonwebtoken";
import { ApiError } from "../utils";
import config from "../config/config";

interface TokenPayload {
    id: string;
    name: string;
    email: string;
    iat: number;
    exp: number;
}

interface IUser {
    _id: string;
    name: string;
    email: string;
    password: string;
    createdAt: Date;
    updatedAt: Date;
}

export interface AuthRequest extends Request {
    user: IUser;
}

const jwtSecret = config.JWT_SECRET as string;

const authMiddleware = async (
    req: AuthRequest,
    res: Response,
    next: NextFunction
) => {
    const authHeader = req.headers.authorization;
    if (!authHeader) {
        return next(new ApiError(401, "Missing authorization header"));
    }

    const [, token] = authHeader.split(" ");
    try {
        const decoded = jwt.verify(token, jwtSecret) as TokenPayload;

        req.user = {
            _id: decoded.id,
            email: decoded.email,
            createdAt: new Date(decoded.iat * 1000),
            updatedAt: new Date(decoded.exp * 1000),
            name: decoded.name,
            password: "",
        };
        return next();
    } catch (error) {
        console.error(error);
        return next(new ApiError(401, "Invalid token"));
    }
};

const errorConverter: ErrorRequestHandler = (err, req, res, next) => {
    let error = err;
    if (!(error instanceof ApiError)) {
        const statusCode =
            error.statusCode ||
            (error instanceof Error
                ? 400 // Bad Request
                : 500); // Internal Server Error
        const message =
            error.message ||
            (statusCode === 400 ? "Bad Request" : "Internal Server Error");
        error = new ApiError(statusCode, message, false, err.stack.toString());
    }
    next(error);
};

const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
    let { statusCode, message } = err;
    if (process.env.NODE_ENV === "production" && !err.isOperational) {
        statusCode = 500; // Internal Server Error
        message = "Internal Server Error";
    }

    res.locals.errorMessage = err.message;

    const response = {
        code: statusCode,
        message,
        ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
    };

    if (process.env.NODE_ENV === "development") {
        console.error(err);
    }

    res.status(statusCode).json(response);
    next();
};

export { authMiddleware, errorConverter, errorHandler };
Enter fullscreen mode Exit fullscreen mode

这段代码在文件夹中定义了三个中间件src/middleware: `<middleware_name> authMiddleware`、 `<middleware_name>`errorConverter和 `<middleware_name> errorHandler`。`<middleware_name>`authMiddleware处理用户身份验证、errorConverter将错误转换为标准化格式并errorHandler发送相应的错误响应。此中间件将在 Express 应用程序中用于处理身份验证和错误。

服务器设置和初始化

现在是时候设置服务器了。首先,打开文件夹app.ts中的文件src,并添加以下代码:

import express, { Express } from "express";
import userRouter from "./routes/messageRoutes";
import { errorConverter, errorHandler } from "./middleware";

const app: Express = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(userRouter);
app.use(errorConverter);
app.use(errorHandler);

export default app;
Enter fullscreen mode Exit fullscreen mode

这段代码搭建了一个 Express 应用程序,其中包含用于处理 JSON 和 URL 编码数据的中间件。它还包括messageRouter用于消息路由的中间件以及错误处理中间件。

然后,将以下代码添加到server.ts同一文件夹中的文件中:

import { Server } from "http";
import { Socket, Server as SocketIOServer } from "socket.io";
import app from "./app";
import { Message, connectDB } from "./database";
import config from "./config/config";

let server: Server;
connectDB();

server = app.listen(config.PORT, () => {
    console.log(`Server is running on port ${config.PORT}`);
});
const io = new SocketIOServer(server);
io.on("connection", (socket: Socket) => {
    console.log("Client connected");
    socket.on("disconnect", () => {
        console.log("Client disconnected", socket.id);
    });

    socket.on("sendMessage", (message) => {
        io.emit("receiveMessage", message);
    });

    socket.on("sendMessage", async (data) => {
        const { senderId, receiverId, message } = data;
        const msg = new Message({ senderId, receiverId, message });
        await msg.save();

        io.to(receiverId).emit("receiveMessage", msg); // Assuming receiverId is socket ID of the receiver
    });
});

const exitHandler = () => {
    if (server) {
        server.close(() => {
            console.info("Server closed");
            process.exit(1);
        });
    } else {
        process.exit(1);
    }
};

const unexpectedErrorHandler = (error: unknown) => {
    console.error(error);
    exitHandler();
};

process.on("uncaughtException", unexpectedErrorHandler);
process.on("unhandledRejection", unexpectedErrorHandler);
Enter fullscreen mode Exit fullscreen mode

这段代码初始化服务器,建立 Socket.IO 连接,并设置事件监听器来处理客户端的连接、断开连接和消息发送。此外,它还包含服务器关闭和意外错误的处理机制。

脚本更新

请记得更新"scripts"我们聊天服务的标签,打开package.json文件,并按照用户服务的更新方式,将以下内容更新到标签中:

"scripts": {
    "dev": "NODE_ENV=development nodemon src/server.ts",
    "build": "rm -rf build/ && tsc -p .",
    "start": "NODE_ENV=production nodemon build/server.js"
},
Enter fullscreen mode Exit fullscreen mode

此外,"main"请将标签更新为"src/server.ts"。完成这些步骤后,我们的聊天服务就准备就绪了。运行以下命令启动开发服务器:

npm run dev
Enter fullscreen mode Exit fullscreen mode

这将启动聊天服务的开发服务器。此外,您可以使用 Postman 或您选择的任何 API 工具测试聊天服务端点。

通知服务

恭喜您完成聊天服务器微服务的搭建!聊天服务运行顺畅后,现在是时候通过实现通知服务来提升用户体验了。

在本节中,我们将重点介绍如何创建一个通知服务,用于处理聊天应用程序的实时通知。通知在让用户了解新消息、确保及时沟通以及提供流畅的聊天体验方面发挥着至关重要的作用。

在本节结束时,我们将拥有一个功能齐全的通知服务,它与我们现有的聊天服务器微服务无缝集成,通过实时通知提升整体用户体验。让我们开始吧!

以下是我们的通知服务的文件和文件夹结构:

通知结构

现在,请使用以下命令设置通知服务项目结构,并确保您位于项目的根目录中:

创建并导航至您的notification-service文件夹:

mkdir notification-service && cd notification-service
Enter fullscreen mode Exit fullscreen mode

接下来,创建src文件夹并导航到该文件夹​​:

mkdir -p src/config src/services src/middleware src/utils
cd src
Enter fullscreen mode Exit fullscreen mode

现在,创建各个文件:

touch config/config.ts services/RabbitMQService.ts services/EmailService.ts services/FCMService.ts services/index.ts middleware/index.ts utils/apiError.ts utils/userStatusStore.ts utils/index.ts server.ts
Enter fullscreen mode Exit fullscreen mode

Finally, create Dockerfile, .dockerignore, and .env files:

cd ..
touch Dockerfile .dockerignore .env
Enter fullscreen mode Exit fullscreen mode

Here's a brief description of each file and folder created for the Notification Service:

  1. config/config.ts:
    • This file contains configurations related to the Notification Service, such as RabbitMQ connection details and other environment variables.
  2. services/RabbitMQService.ts:
    • This file implements the RabbitMQ service, which handles message queueing and communication with other microservices via RabbitMQ.
  3. services/EmailService.ts:
    • This file defines the Email service, responsible for sending email notifications to users based on certain events or triggers within the application.
  4. services/FCMService.ts:
    • This file contains the Firebase Cloud Messaging (FCM) service implementation, allowing the Notification Service to send push notifications to mobile devices.
  5. services/index.ts:
    • This file exports all service modules, facilitating easy import and access to service functionalities from other parts of the application.
  6. middleware/index.ts:
    • This file houses and exports middleware functions used within the Notification Service, such as authentication middleware or error handling middleware.
  7. utils/apiError.ts:
    • This file defines the ApiError class, which represents custom error objects used throughout the application to handle API-related errors.
  8. utils/userStatusStore.ts:
    • This file contains the UserStatusStore class, which manages the online/offline status of users, facilitating real-time communication and notification delivery.
  9. utils/index.ts:
    • This file exports utility functions and classes for easy access and use within the Notification Service.
  10. server.ts:
    • This file initializes and starts the Express server for the Notification Service, defining routes, middleware, and server setup logic.

Next, let’s set up our environment and install the necessary dependencies. Use the following command, ensuring you’re in the notification-service root directory:

Initialize Node.js project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Install dependencies

npm install amqplib dotenv express firebase-admin nodemon ts-node typescript nodemailer sib-api-v3-typescript @types/express @types/amqplib @types/nodemailer
Enter fullscreen mode Exit fullscreen mode

This command sequence will create a package.json file and install the required Node.js packages for the Notification Service.

Now, let's proceed with the TypeScript configuration. Run the following command:

tsc --init
Enter fullscreen mode Exit fullscreen mode

Open the generated tsconfig.json file and update it with the following configuration:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "tests"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Next, add the following code to your .env file:

# Environment variables for Notification Service

# Set the environment to development
NODE_ENV="development"

# Port for the Notification Service
PORT=8083

# Replace 'YOUR_MESSAGE_BROKER_URL' with the RabbitMQ message broker URL used in the User and Chat Service
MESSAGE_BROKER_URL="{{YOUR_MESSAGE_BROKER_URL}}"

# SMTP configuration for sending emails using SendInBlue (Brevo)
SMTP_HOST="smtp-relay.brevo.com"
SMTP_PORT=587

# Replace placeholders with your SendInBlue (Brevo) account details
SMTP_USER="{{YOUR_SENDINBLUE_ACCOUNT_EMAIL}}"
SMTP_PASS="{{YOUR_SENDINBLUE_MASTER_PASSWORD}}"

# Replace placeholders with your SendInBlue (Brevo) API key and email source
SENDINBLUE_APIKEY="{{YOUR_SENDINBLUE_APIKEY}}"
EMAIL_FROM="{{YOUR_EMAIL_SOURCE}}"
Enter fullscreen mode Exit fullscreen mode

To set up the .env file correctly:

  1. Replace {{YOUR_MESSAGE_BROKER_URL}} with the RabbitMQ message broker URL you used in the User and Chat Service.
  2. Visit Brevo's website and log in or create an account if you don't have one.
  3. Navigate to the SMTP & API key page and create an SMTP key. Copy the value and replace {{YOUR_SENDINBLUE_MASTER_PASSWORD}} with this value.
  4. Click on the API KEYS tab, create an API key, and copy the value. Replace {{YOUR_SENDINBLUE_APIKEY}} with this value.
  5. Replace {{YOUR_SENDINBLUE_ACCOUNT_EMAIL}} with your Brevo account's email.
  6. Replace {{YOUR_EMAIL_SOURCE}} with the email address you want the emails to be sent from.

Now, let’s set up the config.ts file in the src/config folder. It manages the configuration variables used throughout the Notification Service. Let's dive into the code:

import { config } from "dotenv";

const configFile = `./.env`;
config({ path: configFile });

const {
    PORT,
    JWT_SECRET,
    NODE_ENV,
    MESSAGE_BROKER_URL,
    SENDINBLUE_APIKEY,
    EMAIL_FROM,
    SMTP_HOST,
    SMTP_PORT = 587,
    SMTP_USER,
    SMTP_PASS,
} = process.env;

const queue = { notifications: "NOTIFICATIONS" };

export default {
    PORT,
    JWT_SECRET,
    env: NODE_ENV,
    msgBrokerURL: MESSAGE_BROKER_URL,
    SENDINBLUE_APIKEY,
    EMAIL_FROM,
    queue,
    smtp: {
        host: SMTP_HOST,
        port: SMTP_PORT as number,
        user: SMTP_USER,
        pass: SMTP_PASS,
    },
};
Enter fullscreen mode Exit fullscreen mode

In our config.ts file, we handle the configuration settings for our Notification Service. Using the dotenv library, we load environment variables from the .env file, which contains crucial settings like the server port, JWT secret key, message broker URL, SMTP settings, and more. These variables are then deconstructed for convenient access within the configuration object. Within this object, we define the notification queue and SMTP settings, ensuring seamless integration with external services such as RabbitMQ and SendInBlue (Brevo) for email delivery. Ultimately, we export this configuration object, making it available for use throughout the Notification Service, ensuring consistent and reliable behavior across the application.

Utils

With our configurations in place, let’s work on our utils. Add the following code to the apiError.ts in the src/utils file:

class ApiError extends Error {
    statusCode: number;
    isOperational: boolean;

    constructor(
        statusCode: number,
        message: string | undefined,
        isOperational = true,
        stack = ""
    ) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        if (stack) {
            this.stack = stack;
        } else {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

export { ApiError };
Enter fullscreen mode Exit fullscreen mode

This ApiError class performs the same function as in our User and Chat services.

Next, add the following code to the userStatusStore.ts file:

export class UserStatusStore {
    private static instance: UserStatusStore;
    private userStatuses: Record<string, boolean>;

    constructor() {
        this.userStatuses = {};
    }

    public static getInstance(): UserStatusStore {
        if (!UserStatusStore.instance) {
            UserStatusStore.instance = new UserStatusStore();
        }
        return UserStatusStore.instance;
    }

    setUserOnline(userId: string) {
        this.userStatuses[userId] = true;
    }

    setUserOffline(userId: string) {
        this.userStatuses[userId] = false;
    }

    isUserOnline(userId: string): boolean {
        return !!this.userStatuses[userId];
    }
}
Enter fullscreen mode Exit fullscreen mode

This performs the same function as it does in our Chat Service.

Lastly, export the utils by adding the code below to the index.ts file:

import { UserStatusStore } from "./userStatusStore";
import { ApiError } from "./apiError";

export { UserStatusStore, ApiError };
Enter fullscreen mode Exit fullscreen mode

This code exports the ApiError and UserStatusStore from the utils, making them available for use in other parts of the application. With our utils in place, let’s define our middleware.

Email Service

Add the following code to the EmailService.ts file located in the src/services folder:

import nodemailer from "nodemailer";
import config from "../config/config";

export class EmailService {
    private transporter;

    constructor() {
        this.transporter = nodemailer.createTransport({
            host: config.smtp.host,
            port: config.smtp.port,
            secure: false,
            auth: {
                user: config.smtp.user,
                pass: config.smtp.pass,
            },
        });
    }

    async sendEmail(to: string, subject: string, content: string) {
        const mailOptions = {
            from: config.EMAIL_FROM,
            to: to,
            subject: subject,
            html: content,
        };

        try {
            const info = await this.transporter.sendMail(mailOptions);
            console.log("Email sent: %s", info.messageId);
        } catch (error) {
            console.error("Error sending email:", error);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the EmailService.ts file, we define the EmailService class responsible for handling email delivery. Upon instantiation, the constructor initializes a Nodemailer transporter instance using SMTP settings retrieved from the configuration file. The sendEmail method accepts the recipient's email address, subject, and content as parameters and constructs the email message options accordingly. Finally, it attempts to send the email using the transporter instance, logging success or error messages appropriately. This service ensures efficient and reliable email communication within our application.

Firebase Cloud Messaging (FCM) Services

In our Notification Service, we're integrating Firebase Cloud Messaging (FCM) Services to enable push notification delivery to mobile devices. The FCMService.ts file contains the code responsible for sending push notifications using the Firebase Admin SDK.

import admin from "firebase-admin";

admin.initializeApp({
    credential: admin.credential.applicationDefault(),
});

export class FCMService {
    async sendPushNotification(token: string, message: string) {
        const payload = {
            notification: {
                title: "New Message",
                body: message,
            },
            token: token,
        };

        try {
            await admin.messaging().send(payload);
            console.log("Notification sent successfully");
        } catch (error) {
            console.error("Error sending notification", error);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The FCMService class initializes the Firebase Admin SDK with default application credentials upon instantiation. It provides a sendPushNotification method, which accepts a device token and message as parameters to construct the notification payload. The method then sends the notification using the Firebase Admin SDK's messaging functionality. Any errors encountered during the process are logged for further investigation. This service ensures seamless push notification delivery to mobile devices, enhancing user engagement and real-time interaction within our application.

RabbitMQ Services

The RabbitMQService.ts file orchestrates the communication between the Notification Service and RabbitMQ message broker, enabling efficient message exchange for real-time notifications. This service integrates our Email and FCM (Firebase Cloud Messaging) services to deliver notifications via email or push notifications based on the user's online status.

import amqp, { Channel } from "amqplib";
import config from "../config/config";
import { FCMService } from "./FCMService";
import { EmailService } from "./EmailService";
import { UserStatusStore } from "../utils";

class RabbitMQService {
    private channel!: Channel;
    private fcmService = new FCMService();
    private emailService = new EmailService();
    private userStatusStore = new UserStatusStore();

    constructor() {
        this.init();
    }

    async init() {
        const connection = await amqp.connect(config.msgBrokerURL!);
        this.channel = await connection.createChannel();
        await this.consumeNotification();
    }

    async consumeNotification() {
        await this.channel.assertQueue(config.queue.notifications);
        this.channel.consume(config.queue.notifications, async (msg) => {
            if (msg) {
                const {
                    type,
                    userId,
                    message,
                    userEmail,
                    userToken,
                    fromName,
                } = JSON.parse(msg.content.toString());

                if (type === "MESSAGE_RECEIVED") {
                    // Check if the user is online
                    const isUserOnline =
                        this.userStatusStore.isUserOnline(userId);

                    if (isUserOnline && userToken) {
                        // User is online, send a push notification
                        await this.fcmService.sendPushNotification(
                            userToken,
                            message
                        );
                    } else if (userEmail) {
                        // User is offline, send an email
                        await this.emailService.sendEmail(
                            userEmail,
                            `New Message from ${fromName}`,
                            message
                        );
                    }
                }

                this.channel.ack(msg); // Acknowledge the message after processing
            }
        });
    }
}

export const rabbitMQService = new RabbitMQService();
Enter fullscreen mode Exit fullscreen mode

The RabbitMQService class establishes a connection with the RabbitMQ message broker and consumes notifications from a designated queue. Upon receiving a notification, it parses the message content and determines the appropriate notification delivery method based on the user's online status. If the user is online and has a valid FCM token, the service sends a push notification using the FCMService. Otherwise, if the user is offline and an email address is provided, it dispatches an email notification using the EmailService. This integration ensures seamless and reliable delivery of notifications to users across different channels, enhancing the overall user experience.

Next, export the services:

import { FCMService } from "./FCMService";
import { EmailService } from "./EmailService";

export { FCMService, EmailService };
Enter fullscreen mode Exit fullscreen mode

Middleware

With our services ready, open the index.ts file in the src/middleware folder and add the following code:

import { ErrorRequestHandler } from "express";
import { ApiError } from "../utils";

export const errorConverter: ErrorRequestHandler = (err, req, res, next) => {
    let error = err;
    if (!(error instanceof ApiError)) {
        const statusCode =
            error.statusCode ||
            (error instanceof Error
                ? 400 // Bad Request
                : 500); // Internal Server Error
        const message =
            error.message ||
            (statusCode === 400 ? "Bad Request" : "Internal Server Error");
        error = new ApiError(statusCode, message, false, err.stack.toString());
    }
    next(error);
};

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
    let { statusCode, message } = err;
    if (process.env.NODE_ENV === "production" && !err.isOperational) {
        statusCode = 500; // Internal Server Error
        message = "Internal Server Error";
    }

    res.locals.errorMessage = err.message;

    const response = {
        code: statusCode,
        message,
        ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
    };

    if (process.env.NODE_ENV === "development") {
        console.error(err);
    }

    res.status(statusCode).json(response);
    next();
};
Enter fullscreen mode Exit fullscreen mode

The errorConverter and errorHandler functions perform the same functions as in our User and Chat Services.

Server Setup and Initialization

Well done so far. It’s time to set up our server. Open the server.ts file located in the src folder and add the following code:

import express, { Express } from "express";
import { Server } from "http";
import { errorConverter, errorHandler } from "./middleware";
import config from "./config/config";
import { rabbitMQService } from "./services/RabbitMQService";

const app: Express = express();
let server: Server;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(errorConverter);
app.use(errorHandler);

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

const initializeRabbitMQClient = async () => {
    try {
        await rabbitMQService.init();
        console.log("RabbitMQ client initialized and listening for messages.");
    } catch (err) {
        console.error("Failed to initialize RabbitMQ client:", err);
    }
};

initializeRabbitMQClient();

const exitHandler = () => {
    if (server) {
        server.close(() => {
            console.info("Server closed");
            process.exit(1);
        });
    } else {
        process.exit(1);
    }
};

const unexpectedErrorHandler = (error: unknown) => {
    console.error(error);
    exitHandler();
};

process.on("uncaughtException", unexpectedErrorHandler);
process.on("unhandledRejection", unexpectedErrorHandler);
Enter fullscreen mode Exit fullscreen mode

This code initializes the Express application, sets up middleware for error handling, and starts the server to listen on the specified port. Additionally, it initializes the RabbitMQ client to handle messaging functionality and sets up handlers for process exits and unexpected errors to ensure graceful shutdown and error handling within the application.

Script Update

To ensure proper execution of the Notification service, update the "scripts" tag in the package.json file as follows:

"scripts": {
    "dev": "NODE_ENV=development nodemon src/server.ts",
    "build": "rm -rf build/ && tsc -p .",
    "start": "NODE_ENV=production nodemon build/server.js"
},
Enter fullscreen mode Exit fullscreen mode

Additionally, update the "main" tag by setting it to "src/server.ts". This change ensures that the main entry point of the service is correctly identified.

To initiate the development server for your Notification service, run the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Executing this command will start the development server, allowing it to listen to incoming messages and handle notifications effectively.

API Gateway

Congratulations on completing the setup of the Notification service! Now that our microservices are ready, let’s create our API Gateway. This will connect all our microservices ports to one gateway.

  1. Navigate to the chat-server root directory
  2. Create and navigate to the gateway folder
mkdir gateway && cd gateway
Enter fullscreen mode Exit fullscreen mode
  1. Set up the development server:
npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies
npm install nodemon express ts-node express-http-proxy @types/express @types/express-http-proxy
Enter fullscreen mode Exit fullscreen mode
  1. Create the index.ts file:
touch index.ts
Enter fullscreen mode Exit fullscreen mode
  1. Add the following code to the index.ts file:
import express from "express";
import proxy from "express-http-proxy";

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const auth = proxy("http://localhost:8081");
const messages = proxy("http://localhost:8082");
const notifications = proxy("http://localhost:8083");

app.use("/api/auth", auth);
app.use("/api/messages", messages);
app.use("/api/notifications", notifications);

const server = app.listen(8080, () => {
    console.log("Gateway is Listening to Port 8080");
});

const exitHandler = () => {
    if (server) {
        server.close(() => {
            console.info("Server closed");
            process.exit(1);
        });
    } else {
        process.exit(1);
    }
};

const unexpectedErrorHandler = (error: unknown) => {
    console.error(error);
    exitHandler();
};

process.on("uncaughtException", unexpectedErrorHandler);
process.on("unhandledRejection", unexpectedErrorHandler);
Enter fullscreen mode Exit fullscreen mode

In the above file, we create an Express application to serve as our API Gateway. This application is responsible for routing incoming HTTP requests to the appropriate microservices based on the request path. We begin by importing the necessary modules: express for creating the server and express-http-proxy for proxying requests to other services.

Next, we instantiate an Express application and configure it to parse JSON and URL-encoded data from incoming requests using express.json() and express.urlencoded() middleware.

Then, we create proxy middleware instances for each of our microservices: auth, messages, and notifications. These proxy middleware are configured to forward incoming requests to the corresponding microservice running on different ports: 8081 for user, 8082 for chat, and 8083 for notification service.

After defining the proxy middleware, we mount them on specific routes using app.use(). For example, requests to /api/auth are forwarded to the User microservice, requests to /api/messages are forwarded to the Chat microservice, and requests to /api/notifications are forwarded to the Notification microservice.

Finally, we start the Express server on port 8080 to listen for incoming requests. We also define error handlers to gracefully handle uncaught exceptions and unhandled rejections to ensure the stability of our application. This completes the setup of our API Gateway, which now acts as a central entry point for accessing the functionalities provided by our microservices.

Next, update the package.json file. Update the "script" tag with the following:

"scripts": {
    "dev": "nodemon index.ts",
},
Enter fullscreen mode Exit fullscreen mode

and the "main" tag with "index.ts".

Now, run the following command to start the Gateway server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Testing and Debugging

With the setup of our microservices and API Gateway complete, it's crucial to conduct thorough testing to ensure flawless functionality.

  1. Microservice Verification:

    Confirm that each microservice is up and running. The user service should be accessible at [http://localhost:8081](http://localhost:8081), the chat service at [http://localhost:8082](http://localhost:8082/), and the notification service at [http://localhost:8083](http://localhost:8083/).

  2. With your Postman or any other API tool of your choice. We’d be using Postman in this tutorial.

  3. User Registration and Authentication:

    Register a new user by sending a POST request to http://localhost:8080/user/register with the following JSON data:

    {
        "name": "Test User",
        "email": "{{REAL_EMAIL_ADDRESS}}",
        "password": "password"
    }
    

    Replace REAL_EMAIL_ADDRESS with a valid email address (we’ll need this to send emails). Upon successful registration, proceed to log in by sending a POST request to [http://localhost:8080/user/login](http://localhost:8080/user/login) with the registered email and password.

  4. Message Sending:

    Test the message-sending functionality by making a POST request to [http://localhost:8080/chat/send](http://localhost:8080/chat/send) with the following JSON data:

    {
        "receiverId": "{{RECEIVER_ID}}",
        "message": "Hello there!"
    }
    

    Replace RECEIVER_ID with the ID of another registered user obtained from the registration response. Upon sending the message, an email notification will be dispatched to the recipient's email address.

By meticulously executing these testing steps, we ensure that our microservices operate seamlessly and deliver the desired functionality. Any encountered issues or discrepancies will be addressed promptly, ensuring the reliability and robustness of our system.

Nginx Configuration

Configure Nginx by following these steps:

  1. Create the necessary files and folders by running the following code in the chat-server root folder:
mkdir nginx && cd nginx
touch Dockerfile nginx.conf
Enter fullscreen mode Exit fullscreen mode

This command creates the nginx folder to house our Nginx configurations and creates both the Dockerfile and nginx.conf files.

  1. Add the following code to the Dockerfile:
FROM nginx:latest

RUN rm /etc/nginx/nginx.conf

COPY nginx.conf /etc/nginx/nginx.conf
Enter fullscreen mode Exit fullscreen mode
  1. Add the following code to the nginx.conf file:
http {
    upstream user {
        server user:8081;
    }
    upstream chat {
        server chat:8082;
    }
    upstream notification {
        server notification:8083;
    }

    server {
        listen 85;

        location /user/ {
            proxy_pass http://user/;
        }

        location /chat/ {
            proxy_pass http://chat/;
        }

        location /notification/ {
            proxy_pass http://notification/;
        }
    }
}
events {}
Enter fullscreen mode Exit fullscreen mode

This configuration uses Nginx as a reverse proxy to route requests to the appropriate microservices based on the URL path. Each microservice is defined as an upstream server, and Nginx listens on port 85 for incoming requests. Requests to /user/, /chat/, and /notification/ are proxied to the respective microservices running on ports 8081, 8082, and 8083.

Docker and Containerization

With everything working perfectly, it’s time to containerize our microservices. Containerization is pivotal for deploying and managing our services efficiently. Let's proceed with containerizing each microservice using Docker. Follow these steps:

  1. Open the Dockerfile located in the user-service folder, and add the following code:
FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 8081

CMD [ "npm", "start" ]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile configures a lightweight Node.js environment, copies the necessary files, installs dependencies, builds the application, exposes port 8081, and starts the user service upon container initialization.

  1. Open the Dockerfile located in the chat-service folder, and append the following code:
FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 8082

CMD [ "npm", "start" ]
Enter fullscreen mode Exit fullscreen mode

Similarly, this exposes port 8082 and starts the chat service upon container initialization.

  1. Open the Dockerfile located in the notification-service folder, and add the following code:
FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 8083

CMD [ "npm", "start" ]
Enter fullscreen mode Exit fullscreen mode

This also exposes port 8083 and starts the notification service upon container initialization.

  1. Update the .dockerignore file of every microservice with the following code:
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
Enter fullscreen mode Exit fullscreen mode

This excludes the above-mentioned files from being included in the Docker image creation process for each microservice. This helps reduce the size of the Docker image and ensures that only necessary files are included.

  1. Open the docker-compose.yml file in the chat-server folder (the parent folder) and add the following code:
version: '3.8'
services:
  mongodb:
    image: mongo:latest
    restart: unless-stopped
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

  user:
    build:
      context: ./user-service
      dockerfile: Dockerfile
    ports:
      - "8081:8081"
    restart: always
    depends_on:
      - "mongodb"
    environment:
      - NODE_ENV=production

  chat:
    build:
      context: ./chat-service
      dockerfile: Dockerfile
    ports:
      - "8082:8082"
    depends_on:
      - "mongodb"
    environment:
      - NODE_ENV=production

  notification:
    build:
      context: ./notification-service
      dockerfile: Dockerfile
    ports:
      - "8083:8083"
    depends_on:
      - "mongodb"
    environment:
      - NODE_ENV=production

  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    ports:
      - "85:85"
    depends_on:
      - user
      - chat
      - notification

volumes:
  mongo-data:
Enter fullscreen mode Exit fullscreen mode

This Docker Compose configuration defines several services:

  • mongodb: Runs a MongoDB container.
  • user, chat, and notification: Build and run containers for the user service, chat service, and notification service, respectively.
  • nginx: Configures an NGINX server for routing requests to the appropriate microservices.

Each service has its configuration for building the Docker image, specifying ports to expose, and setting environment variables. Additionally, dependencies are defined to ensure that the services start-up in the correct order. Finally, a volume is specified to persist MongoDB data between container restarts.

  1. To start up our application in a containerized environment, run the following command:
docker-compose up --build 
Enter fullscreen mode Exit fullscreen mode

This command will build and start all the services defined in the docker-compose.yml file. It will also ensure that any changes made to the Dockerfiles or other configurations are applied during the build process.

Docker 日志

Conclusion

We have successfully designed, developed, and containerized a microservices-based chat application server. Through a step-by-step approach, we tackled each component of the system, including the User Service, Chat Service, Notification Service, API Gateway, Docker, and Nginx configuration.

We began by structuring our project directory and setting up each microservice individually. The User Service facilitated user registration and authentication, while the Chat Service enabled real-time communication between users. The Notification Service handled email notifications and push notifications using RabbitMQ for message queuing.

To ensure seamless communication between microservices, we implemented an API Gateway using Express.js, which served as a single entry point for our application. This allowed us to centralize access to our microservices and manage routing efficiently.

Following the development phase, we containerized our microservices using Docker, ensuring consistent deployment across different environments. We configured Nginx as a reverse proxy to route incoming requests to the appropriate microservice based on the URL path.

Throughout the tutorial, we emphasized the importance of testing and debugging to ensure the reliability and functionality of our application. By conducting manual testing and leveraging tools like Postman, we verified the behavior of each microservice and API endpoint, addressing any issues that arose along the way.

总之,本教程旨在作为微服务入门指南,提供构建和部署基于微服务的应用程序的基础知识和实践经验。虽然它涵盖了基本概念和实现步骤,但需要注意的是,微服务涉及一个庞大而复杂的生态系统,超出了本教程的范围。通过学习本教程,初学者可以深入了解微服务架构、容器化以及开发可扩展、高弹性应用程序的最佳实践。对于经验丰富的开发人员,本教程可以帮助他们回顾关键原则,并有机会在微服务开发的背景下探索现代技术。要访问本教程的完整代码,请访问此代码库

文章来源:https://dev.to/davydocsurg/mastering-microservices-a-hands-on-tutorial-with-nodejs-rabbitmq-nginx-and-docker-m4f