Mô tả dự án
Trong bài đăng trên blog này, chúng tôi sẽ tạo một ứng dụng nhắn tin cho phép người dùng tạo ứng dụng nhắn tin và phòng trò chuyện. Ngoài ra, người dùng sẽ có thể truy cập các tin nhắn trước đây.
Dự án bao gồm hai trang. Trang đầu tiên dành riêng cho việc đăng ký khách hàng, nơi bạn có thể tạo nhiều khách hàng với những tên duy nhất.

Khi bạn nhấp vào tên người dùng của khách hàng, bạn sẽ được chuyển đến ứng dụng khách phòng trò chuyện được liên kết với người dùng cụ thể đó.

Logic của ứng dụng chat như sau:
Người dùng có thể tạo nhiều ứng dụng khách trên trang chỉ mục, mỗi ứng dụng khách có một tên người dùng duy nhất. Nhấp vào tên người dùng của khách hàng sẽ mở một tab mới với một khách hàng riêng biệt có đường dẫn duy nhất.
Mỗi máy khách sẽ được kết nối với máy chủ tin nhắn thông qua kết nối WebSocket. Khi một tin nhắn mới được tạo trên máy khách, nó sẽ được gửi đến máy chủ tin nhắn được liên kết với máy khách đó.
Các máy chủ tin nhắn sẽ xử lý lưu lượng tin nhắn. Khi khách hàng gửi tin nhắn qua kết nối WebSocket, máy chủ sẽ chuyển tin nhắn đó đến Nhà môi giới Kafka. Mỗi máy chủ tin nhắn sẽ chạy một luồng NodeJS để xử lý các tin nhắn đến. Khi một tin nhắn được sử dụng, nó sẽ được gửi đến máy khách thông qua kết nối WebSocket hiện có. Để xử lý các tin nhắn đến ở phía máy khách, chúng tôi sẽ sử dụng react-use-websocket thư viện.
Ứng dụng sẽ sử dụng Upstash Redis để lưu trữ lịch sử tin nhắn. Khi một tin nhắn được gửi tới Kafka, nó cũng sẽ được lưu vào cơ sở dữ liệu Redis. Khi tạo ứng dụng khách mới, các tin nhắn cũ sẽ được truy xuất từ Upstash Redis và hiển thị trong màn hình trò chuyện.
Dưới đây là tổng quan chung về ứng dụng:
Lưu ý: Trong quá trình triển khai, chúng tôi sẽ tạo một máy chủ tin nhắn duy nhất cho mục đích demo, người ta có thể tăng số lượng máy chủ để xử lý tải tin nhắn.

Bản trình diễn
Bạn có thể xem bản demo của ứng dụng tại đây. Phiên bản hiện tại của ứng dụng được triển khai cho Fly.
Bắt đầu
Dưới đây là các bước xây dựng ứng dụng trò chuyện:
- Tạo cơ sở dữ liệu Upstash Redis
- Tạo cụm Kafka Upstash
- Tạo ứng dụng Tiếp theo (giao diện người dùng).
- Tạo máy chủ tin nhắn WebSocket.
- Triển khai ứng dụng lên Fly.io
Tạo cơ sở dữ liệu Upstash Redis
Điều hướng tới Bảng điều khiển Upstash và đăng nhập, sau đó trên Redis tab, hãy nhấp vào Tạo cơ sở dữ liệu nút.

Cứ như vậy, Redis của chúng tôi đã sẵn sàng để sử dụng! Chúng tôi sẽ quay lại bảng điều khiển Redis để lấy thông tin xác thực.
Tạo cụm Kafka Upstash
Bây giờ, hãy chuyển sang Kafka Tab và nhấp vào Tạo cụm nút. Nhập tên cụm và tiếp tục. Sau đó, tạo chủ đề Kafka và xác nhận.

Tạo ứng dụng tiếp theo
Đầu tiên, tạo và điều hướng đến thư mục gốc của ứng dụng từ thiết bị đầu cuối của bạn. Chúng tôi sẽ giữ ứng dụng Tiếp theo và máy chủ trong thư mục này.
mkdir chat-app
cd chat-app Sau đó, hãy tạo ứng dụng tiếp theo của bạn.
$ npx create-next-app@latest
✔ What is your project named? … next-chat-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … No
✔ Would you like to customize the default import alias? … No Xử lý thông tin xác thực
Chúng ta sẽ tạo một tệp có tên .env để lưu trữ thông tin xác thực. Chúng tôi sẽ không cần phải sao chép và dán thông tin đăng nhập nhiều lần mà chỉ cần nhập từ tệp này.
Đầu tiên, tạo .env tập tin.
Sau đó, điều hướng đến bảng điều khiển Redis và sao chép/dán UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN thông tin xác thực cho .env tập tin.

Cuối cùng, chuyển sang bảng điều khiển Kafka và chuyển UPSTASH_KAFKA_REST_URL , UPSTASH_KAFKA_REST_USERNAME , UPSTASH_KAFKA_REST_PASSWORD

Bây giờ, .env của bạn tập tin sẽ trông giống nhau
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...
UPSTASH_KAFKA_REST_URL=...
UPSTASH_KAFKA_REST_USERNAME=...
UPSTASH_KAFKA_REST_PASSWORD=... Bây giờ chúng ta đã định cấu hình thông tin xác thực, chúng ta có thể tiếp tục với ứng dụng.
Trang đăng ký khách hàng
Trang chỉ mục sẽ chứa các hoạt động đăng ký/tạo khách hàng. Khi tên người dùng được gửi, một khách hàng mới sẽ được tạo và liệt kê trong Khách hàng hiện tại bảng.
trang/index.tsximport { useState } from "react";
import Link from "next/link";
import { Redis } from "@upstash/redis";
import styles from "@/styles/Home.module.css";
export default function Home() {
const [usernameInput, setUsernameInput] = useState<string>("");
const [usernameList, setUsernameList] = useState<string[]>(Array<string>);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const inputValue: string = e.target.value;
setUsernameInput(inputValue);
};
const addUsernameClient = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
setUsernameList([...usernameList, usernameInput]);
setUsernameInput("");
};
return (
<div className={styles.container}>
<div className={styles.welcomeSection}>
<h1>Welcome to the demo message app!</h1>
<p>
This application uses Upstash Kafka for message passing, and Upstash
Redis for state management.
<br />
<br />
To get started, create several clients by typing in unique usernames to
the input section below and submitting.
<br />
<br />
The usernames will be added to the list of current clients. Click on a
username to open a new tab with that client's message display.
<br />
<br />
You can have multiple sessions open at once.
</p>
</div>
<form className={styles.formSection} onSubmit={addUsernameClient}>
<input
type="text"
className={styles.formInput}
value={usernameInput}
onChange={handleInputChange}
></input>
<button className={styles.formSubmit} type="submit">
Create the client!
</button>
</form>
<div className={styles.clientListSection}>
<p className={styles.clientListHeader}>Current Clients</p>
<div className={styles.clientList}>
{usernameList.map((username, i) => {
return (
<Link
href={`/user/${username}`}
key={`${i}`}
className={styles.userClient}
target={"_blank"}
>
<p>{username}</p>
</Link>
);
})}
</div>
</div>
</div>
);
} Nếu bạn muốn đặt lại lịch sử trò chuyện mỗi khi tải lại ứng dụng, bạn có thể sử dụng chức năng sau:
trang/index.tsxexport async function getServerSideProps() {
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
await redis.del("messagesList");
return {
props: {},
};
}
Cùng với đó, trang chỉ mục đã sẵn sàng để chạy. Chạy npm run dev lệnh trong next-chat-app thư mục để xem trang chỉ mục hoạt động.
Trang ứng dụng nhắn tin
Để triển khai định tuyến động cho máy khách, chúng tôi sẽ tạo một thư mục có tên /pages/user/[username].tsx Cấu trúc thư mục này sẽ cho phép chúng tôi tạo các tuyến động cho từng khách hàng dựa trên tên người dùng của họ.
Đây là thành phần chính của client. Thành phần này sẽ giữ các trạng thái cho danh sách tin nhắn, tên người dùng, v.v. Chúng tôi đang sử dụng useWebSocket hook để tạo các sự kiện tin nhắn, kết nối và ngắt kết nối khỏi WebSocket. Khi một sự kiện tin nhắn được phát ra, tin nhắn sẽ được thêm vào danh sách tin nhắn và thành phần MessageDisplay sẽ được hiển thị lại.
/pages/user/[tên người dùng].tsximport { useState } from "react";
import { useRouter } from "next/router";
import { Redis } from "@upstash/redis";
import useWebSocket from "react-use-websocket";
import styles from "@/styles/Home.module.css";
type Message = {
id: number;
sender: string;
text: string;
};
export default function MessageApp(props: { messagesData: Message[] }) {
const { messagesData } = props;
const { username } = useRouter().query;
const [inputText, setInputText] = useState<string>("");
const [messageList, setMessageList] = useState<Message[]>(messagesData);
const [messageCounter, setMessageCounter] = useState<number>(0);
const handleMessage = function (message: Message) {
const nextMessages = [...messageList, message];
setMessageList(nextMessages);
};
// handling WebSocket events
const { sendMessage } = useWebSocket("ws://localhost:8080", {
share: true,
filter: () => false,
onOpen: () => {
console.log("WebSocket connection!");
return "connection";
},
onMessage: (message) => {
const data = JSON.parse(message.data);
const { sender, text }: { sender: string; text: string } = data;
const messageData: Message = {
id: messageCounter,
sender: sender,
text: text,
};
setMessageCounter(messageCounter + 1);
handleMessage(messageData);
return message;
},
onClose: () => {
console.log("WebSocket disconnected!");
return "disconnected";
},
});
function handleSendMessage(messageText: string) {
const messageData = {
sender: username,
text: messageText,
};
sendMessage(JSON.stringify(messageData));
}
return (
<div className={styles.Container}>
<MessageDisplay messages={messageList} />
<MessageInput
inputText={inputText}
setInputText={setInputText}
handleSendMessage={handleSendMessage}
/>
</div>
);
} Dưới đây là các thành phần MessageDisplay và MessageInput:
/pages/user/[tên người dùng].tsxconst MessageDisplay = function (props: { messages: Message[] }) {
const { messages } = props;
return (
<div className={styles.messageContainer}>
{messages.map((message) => (
<MessageBubble
key={message.id}
sender={message.sender}
text={message.text}
/>
))}
</div>
);
};
const MessageInput = (props: {
inputText: string;
setInputText: (msg: string) => void;
handleSendMessage: (msg: string) => void;
}) => {
const { inputText, setInputText, handleSendMessage } = props;
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement>
): void => {
const inputValue: string = e.target.value;
setInputText(inputValue);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
handleSendMessage(inputText);
if (inputText.trim() !== "") {
setInputText(" ");
}
};
return (
<form className={styles.inputSection} onSubmit={handleSubmit}>
<input
className={styles.inputText}
type="text"
value={inputText}
onChange={handleInputChange}
></input>
<button className={styles.inputSendButton} type="submit">
Send
</button>
</form>
);
};
const MessageBubble = (props: {
sender: string;
text: string;
key: number;
}) => {
const { sender, text } = props;
const { username } = useRouter().query;
const isSender = sender === username;
const senderClass = isSender ? "sender" : "receiver";
return (
<div className={`${styles["messageBubble"]} ${styles[senderClass]}`}>
<div className={styles.messageSender}>
{isSender ? "You" : sender}
</div>
<div className={styles.messageText}>{text}</div>
</div>
);
};
Để cung cấp lịch sử trò chuyện cho khách hàng, chúng tôi sẽ sử dụng getServerSideProps() chức năng.
export async function getServerSideProps() {
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
const messagesData = (await redis.lrange("messagesList", 0, -1)).reverse();
return {
props: {
messagesData,
},
};
} Ứng dụng Next.js của chúng tôi hiện đang hoạt động. Làm mới trang, tạo ứng dụng khách và điều hướng một trong số chúng. Bạn sẽ thấy trang khách hàng. Tuy nhiên, chúng tôi vẫn cần máy chủ tin nhắn để xử lý luồng tin nhắn.
Tạo máy chủ tin nhắn
Cấu trúc của máy chủ khá đơn giản. Chúng tôi sẽ sử dụng Node.js, thư viện ws và Upstash Kafka để làm cho nó hoạt động. Đầu tiên, tạo một server thư mục bên trong chat-app folder .
mkdir server
cd server
Bên trong server thư mục, chúng tôi sẽ cài đặt các yêu cầu và định cấu hình các tệp.
npm install typescript ws tsc @upstash/kafka @types/ws
tsc --init
Sau đó, chúng ta sẽ tạo các ứng dụng khách WebSocket, Kafka Produce và Kafka Consumer bên trong /server/message_server.ts tập tin:
import * as http from "http";
import { Kafka } from "@upstash/kafka";
import { Redis } from "@upstash/redis";
import { WebSocket } from "ws";
const server = http.createServer();
const wss = new WebSocket.Server({ server });
server.listen(8080, () => {
console.log("Server is running on port 8080");
});
const kafka = new Kafka({
url: process.env.UPSTASH_KAFKA_REST_URL,
username: process.env.UPSTASH_KAFKA_REST_USERNAME,
password: process.env.UPSTASH_KAFKA_REST_PASSWORD,
});
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
const consumer = kafka.consumer();
const producer = kafka.producer();
const clients = new Set<WebSocket>();
Để tương tác với WebSocket, chúng tôi đang tạo connection và message sự kiện.
wss.on("connection", async (connection, req) => {
clients.add(connection);
console.log(`New client connected!`);
connection.on("message", async (message) => {
const jsonMessage = message.toString();
console.log("Received message:", JSON.parse(jsonMessage));
producer.produce("chat", jsonMessage);
});
connection.on("close", () => {
console.log(`Client disconnected:`);
clients.delete(connection);
});
}); Cuối cùng, chúng ta sẽ tạo và chạy chuỗi sử dụng các tin nhắn với khoảng thời gian được xác định trước:
/server/message_server.tsasync function run() {
while (true) {
const messages = await consumer.consume({
consumerGroupId: "group_1",
instanceId: "instance_1",
topics: ["chat"],
autoOffsetReset: "earliest",
});
if (messages.length != 0) {
for (let i = 0; i < messages.length; i++) {
await redis.lpush("messagesList", messages[i].value);
console.log(`Message sending: ${messages[i].value}`);
clients.forEach((connection: WebSocket) => {
connection.send(messages[i].value);
});
}
}
console.log("Run!");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
Mọi thứ đã sẵn sàng. Ứng dụng của chúng tôi hiện đang hoạt động rất tốt. Nếu bạn chạy máy chủ tin nhắn trên máy cục bộ và làm mới trang máy khách, bạn có thể thấy các tin nhắn được truyền giữa các máy khách. Các lệnh bên dưới sẽ biên dịch tệp TS và chạy máy chủ trên localhost:8000
tsc message_server.ts
node message_server.js Triển khai
Chúng tôi sẽ sử dụng Fly.io để triển khai. Vui lòng tạo một tài khoản trước khi chúng ta bắt đầu, nếu bạn chưa có tài khoản.
Triển khai Máy chủ Tin nhắn
Đi đến server thư mục và cài đặt flyctl Công cụ CLI và ủy quyền qua shell
npm install flyctl
flyctl auth login
Để tạo các tệp cấu hình, hãy chạy flyctl init . Điều này sẽ tạo ra một fly.toml . Tới fly.toml và chèn các dòng sau cho cấu hình kết nối WebSocket:
[[services]]
internal_port = 8080
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = "80"
[[services.ports]]
handlers = ["tls", "http"]
port = "443"
[[services.tcp_checks]]
interval = 10000
timeout = 2000
Bây giờ là bước cuối cùng dành cho máy chủ. Chạy flyctl deploy , và chúng tôi đã sẵn sàng để đi! Khi quá trình triển khai hoàn tất, flyctl sẽ cung cấp điểm cuối cho máy chủ của bạn. Hãy sao chép điểm cuối đó. Trong trường hợp của chúng tôi, điểm cuối là message-server.fly.dev .
Triển khai ứng dụng tiếp theo
Trước khi triển khai ứng dụng Next.js, chúng ta cần nhúng điểm cuối triển khai của máy chủ tin nhắn. Vui lòng thay thế URL WebSocket trong pages/user/[username].tsx tập tin từ ws://localhost:8080 đến điểm cuối từ flyctl , kết hợp với wss:// tiền tố. Trong trường hợp của chúng tôi, đó là wss://message-server.fly.dev .
Sau đó, trong next-chat-app thư mục, chạy các lệnh giống như server . Lần này, chúng ta không cần chỉnh sửa fly.toml tệp để chúng ta có thể tiếp tục mà không cần bước đó.
flyctl init
flyctl deploy
Chúng ta đã xong! Nếu bạn chạy flyctl open lệnh, bạn sẽ được điều hướng đến dự án đã triển khai của mình.
Kết luận và đề xuất
Cảm ơn bạn đã theo dõi!
Bạn có thể tìm thấy kho lưu trữ Github của dự án tại đây.
Nếu bạn muốn tiếp tục thực hiện dự án, đây là một số gợi ý:
-
Hiện tại, bất cứ khi nào trang được tải lại, tất cả tin nhắn được lưu trữ trong Upstash Redis đều bị xóa. Hành vi này được kiểm soát bởi mã trong
pages/index.tsxtệp, cụ thể là tronggetServerSidePropschức năng. Tuy nhiên, một vấn đề nghiêm trọng phát sinh khi người dùng quyết định tải lại trang, dẫn đến việc xóa lịch sử trò chuyện của tất cả những người tham gia phòng trò chuyện.
Để giải quyết vấn đề này, giải pháp được đề xuất là triển khai phần mở rộng TTL cho lịch sử trò chuyện mỗi khi tin nhắn được gửi. Cải tiến này sẽ đảm bảo rằng lịch sử trò chuyện vẫn có thể truy cập và được lưu giữ ngay cả sau khi tải lại trang. -
Bạn có thể triển khai tính năng nhiều phòng chat. Để đạt được điều này, bạn có thể tạo nhiều chủ đề Kafka với tên duy nhất cho mỗi phòng trò chuyện. Một cách khác là xử lý nó trên chính máy chủ tin nhắn bằng cách sử dụng cấu trúc dữ liệu phù hợp.
-
Bạn cũng có thể triển khai nhiều máy chủ thư và bộ cân bằng tải để áp dụng các phương pháp thiết kế hệ thống tốt nhất.
Nếu bạn có bất kỳ câu hỏi nào, bạn có thể liên hệ với tôi theo địa chỉ fahreddin@upstash.com