跳至主要內容

私人訊息 - 第 1 部分

在本指南中,我們將建立以下應用程式

Chat

我們將涵蓋下列主題

先備條件

  • Socket.IO 的基本知識
  • Vue.js 的基本知識(儘管其他熱門前端架構的知識也應該適用)
  • Redis 的基本知識(適用於最後一部分)

本指南分為四個不同的部分

開始吧!

安裝

首先,讓我們取得聊天應用程式的初始實作

git clone https://github.com/socketio/socket.io.git
cd socket.io/examples/private-messaging
git checkout examples/private-messaging-part-1

以下是您應該在目前目錄中看到的內容

├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── fonts
│ │ └── Lato-Regular.ttf
│ └── index.html
├── README.md
├── server
│ ├── index.js
│ ├── package.json
└── src
├── App.vue
├── components
│ ├── Chat.vue
│ ├── MessagePanel.vue
│ ├── SelectUsername.vue
│ ├── StatusIcon.vue
│ └── User.vue
├── main.js
└── socket.js

前端程式碼位於 src 目錄中,而伺服器程式碼位於 server 目錄中。

執行前端

此專案是一個基本的 Vue.js 應用程式,使用 @vue/cli 建立。

執行方式

npm install
npm run serve

然後,如果您在瀏覽器中開啟 http://localhost:8080,您應該會看到

Username selection

執行伺服器

現在,讓我們啟動伺服器

cd server
npm install
npm start

您的主控台應列印

server listening at http://localhost:3000

到目前為止,一切順利!您應該可以開啟多個標籤頁,並在它們之間傳送一些訊息

Chat

運作方式

伺服器初始化

Socket.IO 伺服器在 server/index.js 檔案中初始化

const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:8080",
},
});

在此,我們建立一個 Socket.IO 伺服器,並將其附加到 Node.js HTTP 伺服器。

文件

需要 cors 設定,以便前端(在 http://localhost:8080 執行)傳送的 HTTP 要求可以到達伺服器(在 http://localhost:3000 執行,因此我們處於跨來源情況)。

文件

用戶端初始化

Socket.IO 用戶端在 src/socket.js 檔案中初始化

import { io } from "socket.io-client";

const URL = "http://localhost:3000";
const socket = io(URL, { autoConnect: false });

export default socket;

autoConnect 設定為 false,因此不會立即建立連線。我們稍後會手動呼叫 socket.connect(),也就是在使用者選擇使用者名稱之後。

文件:Socket.IO 用戶端初始化

我們也註冊了一個 萬用監聽器,這在開發期間非常有用

socket.onAny((event, ...args) => {
console.log(event, args);
});

因此,用戶端收到的任何事件都會列印在主控台中。

選擇使用者名稱

現在,讓我們移到 src/App.vue

應用程式以 usernameAlreadySelected 設定為 false 啟動,因此會顯示選擇使用者名稱的表單

Username selection

提交表單後,我們將到達 onUsernameSelection 方法

onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
}

我們在 auth 物件中附加 username,然後呼叫 socket.connect()

如果您在開發人員工具中開啟網路標籤頁,您應該會看到一些 HTTP 要求

Network monitor upon success

  1. Engine.IO 交握(包含會話 ID — 在這裡是 zBjrh...AAAK — 用於後續要求)
  2. Socket.IO 交握要求(包含 auth 選項的值)
  3. Socket.IO 握手回應(包含 Socket#id
  4. WebSocket 連線
  5. 第一個 HTTP 長輪詢請求,在建立 WebSocket 連線後關閉

如果您看到這則訊息,表示連線已成功建立。

在伺服器端,我們註冊一個中介軟體,用來檢查使用者名稱並允許連線

io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
socket.username = username;
next();
});

username 會新增為 socket 物件的屬性,以便稍後重複使用。您可以附加任何屬性,只要不覆寫現有的屬性,例如 socket.idsocket.handshake

文件

在用戶端(src/App.vue),我們為 connect_error 事件新增一個處理常式

socket.on("connect_error", (err) => {
if (err.message === "invalid username") {
this.usernameAlreadySelected = false;
}
});

connect_error 事件會在連線失敗時發出

  • 由於低層級錯誤(例如伺服器當機)
  • 由於中介軟體錯誤

請注意,在上述函式中,低層級錯誤並未處理(例如,可以通知使用者連線失敗)。

最後一個注意事項:connect_error 的處理常式會在 destroyed 掛勾中移除

destroyed() {
socket.off("connect_error");
}

因此,當元件被銷毀時,由我們的 App 元件註冊的監聽器會被清除。

列出所有使用者

連線後,我們會將所有現有使用者傳送給用戶端

io.on("connection", (socket) => {
const users = [];
for (let [id, socket] of io.of("/").sockets) {
users.push({
userID: id,
username: socket.username,
});
}
socket.emit("users", users);
// ...
});

我們正在迴圈處理 io.of("/").sockets 物件,它是所有目前已連線 Socket 實例的 Map,索引為 ID

這裡有兩點說明

  • 我們使用 socket.id 作為我們應用程式的使用者 ID
  • 我們只擷取目前 Socket.IO 伺服器的使用者(不適用於擴充時)

我們稍後會再回來討論這一點。

在用戶端(src/components/Chat.vue),我們為 users 事件註冊一個處理常式

socket.on("users", (users) => {
users.forEach((user) => {
user.self = user.userID === socket.id;
initReactiveProperties(user);
});
// put the current user first, and then sort by username
this.users = users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
});

我們也會通知現有使用者

伺服器

io.on("connection", (socket) => {
// notify existing users
socket.broadcast.emit("user connected", {
userID: socket.id,
username: socket.username,
});
});

socket.broadcast.emit("user connected", ...) 會發送給所有已連線的用戶端,除了 socket 本身。

另一種廣播形式 io.emit("user connected", ...) 會將「使用者已連線」事件傳送給所有已連線的用戶端,包括新使用者。

文件:廣播事件

用戶端

socket.on("user connected", (user) => {
initReactiveProperties(user);
this.users.push(user);
});

使用者清單顯示於左側面板

Users list

私人訊息

當選擇特定使用者時,右側面板會顯示聊天視窗

Chat

以下是私人訊息的實作方式

用戶端(傳送者)

onMessage(content) {
if (this.selectedUser) {
socket.emit("private message", {
content,
to: this.selectedUser.userID,
});
this.selectedUser.messages.push({
content,
fromSelf: true,
});
}
}

伺服器

socket.on("private message", ({ content, to }) => {
socket.to(to).emit("private message", {
content,
from: socket.id,
});
});

在此,我們使用房間的概念。這些是 Socket 實例可以加入和離開的頻道,並且可以廣播給房間中的所有用戶端。

我們依賴 Socket 實例自動加入由其 ID 識別的房間(為您呼叫 socket.join(socket.id))。

因此,socket.to(to).emit("private message", ...) 會發射至指定的使用者 ID。

用戶端(接收者)

socket.on("private message", ({ content, from }) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
if (user.userID === from) {
user.messages.push({
content,
fromSelf: false,
});
if (user !== this.selectedUser) {
user.hasNewMessages = true;
}
break;
}
}
});

連線狀態

在用戶端,Socket 實例會發出兩個特殊事件

  • connect:連線或重新連線時
  • disconnect:斷線時

這些事件可用於追蹤連線狀態(在 src/components/Chat.vue 中)

socket.on("connect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = true;
}
});
});

socket.on("disconnect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = false;
}
});
});

您可以透過停止伺服器來測試

Connection status

回顧

好的,所以...我們目前為止所擁有的很棒,但有一個明顯的問題

Duplicate users

說明:重新連線時會產生新的 Socket ID,因此每次使用者斷線並重新連線時,都會取得新的使用者 ID。

這就是我們需要持久使用者 ID 的原因,這是本指南第 2 部分的主題。

感謝您的閱讀!