私人訊息 - 第 2 部分
本指南分為四個不同的部分
以下是我們在第 1 部分結束時所處的階段

目前交換私人訊息是根據socket.id
屬性,這很有效,但這裡有問題,因為此 ID 僅對目前的 Socket.IO 會話有效,而且每次用戶端和伺服器之間的低階連線中斷時,此 ID 都會變更。
因此,每次使用者重新連線時,都會建立一個新使用者

這...不太好。來修正它吧!
安裝
讓我們檢視第 2 部分的分支
git checkout examples/private-messaging-part-2
以下是您應該在目前目錄中看到的內容
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── fonts
│ │ └── Lato-Regular.ttf
│ └── index.html
├── README.md
├── server
│ ├── index.js (updated)
│ ├── package.json
│ └── sessionStore.js (created)
└── src
├── App.vue (updated)
├── components
│ ├── Chat.vue (updated)
│ ├── MessagePanel.vue
│ ├── SelectUsername.vue
│ ├── StatusIcon.vue
│ └── User.vue
├── main.js
└── socket.js
完整的 diff 可以在此找到。
運作方式
持續性會話 ID
在伺服器端 (server/index.js
),我們建立兩個隨機值
- 會話 ID,私密的,將用於在重新連線時驗證使用者
- 使用者 ID,公開的,將用作交換訊息的識別碼
io.use((socket, next) => {
const sessionID = socket.handshake.auth.sessionID;
if (sessionID) {
// find existing session
const session = sessionStore.findSession(sessionID);
if (session) {
socket.sessionID = sessionID;
socket.userID = session.userID;
socket.username = session.username;
return next();
}
}
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
// create new session
socket.sessionID = randomId();
socket.userID = randomId();
socket.username = username;
next();
});
然後將會話詳細資料傳送給使用者
io.on("connection", (socket) => {
// ...
socket.emit("session", {
sessionID: socket.sessionID,
userID: socket.userID,
});
// ...
});
在客戶端(src/App.vue
),我們將會話 ID 儲存在 localStorage 中
socket.on("session", ({ sessionID, userID }) => {
// attach the session ID to the next reconnection attempts
socket.auth = { sessionID };
// store it in the localStorage
localStorage.setItem("sessionID", sessionID);
// save the ID of the user
socket.userID = userID;
});
實際上,有幾個可能的實作
- 完全沒有儲存:重新連線會保留會話,但重新整理頁面會遺失會話
- sessionStorage:重新連線和重新整理頁面會保留會話
- localStorage:重新連線和重新整理頁面會保留會話,而且這個會話會在瀏覽器分頁之間共用
在這裡,我們選擇 localStorage
選項,因此您的所有分頁都會連結到同一個會話 ID,這表示
- 您可以和自己聊天(太棒了!)
- 您現在需要使用另一個瀏覽器(或瀏覽器的私人模式)來建立另一個對等方
最後,我們在應用程式啟動時擷取會話 ID
created() {
const sessionID = localStorage.getItem("sessionID");
if (sessionID) {
this.usernameAlreadySelected = true;
socket.auth = { sessionID };
socket.connect();
}
// ...
}
您現在應該可以在不遺失會話的情況下重新整理分頁

在伺服器端,會話儲存在記憶體內儲存體中(server/sessionStore.js
)
class InMemorySessionStore extends SessionStore {
constructor() {
super();
this.sessions = new Map();
}
findSession(id) {
return this.sessions.get(id);
}
saveSession(id, session) {
this.sessions.set(id, session);
}
findAllSessions() {
return [...this.sessions.values()];
}
}
同樣地,這只會在單一的 Socket.IO 伺服器上運作,我們會在本指南的第 4 部分中回顧這一點。
私人訊息傳送(已更新)
私人訊息傳送現在根據在伺服器端產生的 userID
,因此我們需要執行兩件事
- 讓 Socket 實例加入相關的房間
io.on("connection", (socket) => {
// ...
socket.join(socket.userID);
// ...
});
- 更新轉發處理常式
io.on("connection", (socket) => {
// ...
socket.on("private message", ({ content, to }) => {
socket.to(to).to(socket.userID).emit("private message", {
content,
from: socket.userID,
to,
});
});
// ...
});
以下是發生的事情

使用 socket.to(to).to(socket.userID).emit(...)
,我們在收件者和寄件者(排除給定的 Socket 實例)房間中廣播。
因此現在我們有

中斷處理常式
在伺服器端,Socket 實例會發出兩個特殊事件:disconnecting 和 disconnect
我們需要更新我們的「中斷」處理常式,因為會話現在可以在分頁之間共用
io.on("connection", (socket) => {
// ...
socket.on("disconnect", async () => {
const matchingSockets = await io.in(socket.userID).allSockets();
const isDisconnected = matchingSockets.size === 0;
if (isDisconnected) {
// notify other users
socket.broadcast.emit("user disconnected", socket.userID);
// update the connection status of the session
sessionStore.saveSession(socket.sessionID, {
userID: socket.userID,
username: socket.username,
connected: false,
});
}
});
});
allSockets()
方法會傳回一個包含給定房間中所有 Socket 實例 ID 的 Set。
注意:我們也可以使用 io.of("/").sockets
物件,就像在第 I 部分中一樣,但 allSockets()
方法也可以與多個 Socket.IO 伺服器一起使用,這在擴充時會很有用。
檢閱
好的,所以…我們現在所擁有的比較好,但還有一個問題:訊息並未實際儲存在伺服器上。因此,當使用者重新整理頁面時,它會失去所有現有的對話。
例如,這可以用儲存在瀏覽器 localStorage 中的方式來修正,但還有另一個更令人困擾的後果
- 當傳送者斷線時,它傳送的所有封包都會緩衝,直到重新連線(在多數情況下,這很好)

- 但當接收者斷線時,封包會遺失,因為在給定的房間中沒有監聽的 Socket 實例

我們將嘗試在本指南的第 3 部分中修復此問題。
感謝您的閱讀!