私信 - 第二部分
本指南分为四个不同的部分
这是我们在 第一部分 结束时的位置
交换私信目前基于 socket.id
属性,它工作良好,但这里存在问题,因为此 ID 仅对当前 Socket.IO 会话有效,并且每次客户端和服务器之间的低级连接断开时都会更改。
因此,每次用户重新连接时,都会创建一个新用户
这... 不太好。让我们解决这个问题!
安装
让我们检出第二部分的分支
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
完整的差异可以在 这里 找到。
工作原理
持久会话 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 服务器,我们将在本指南的第四部分中回到这一点。
私信(更新)
私信现在基于在服务器端生成的 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()
方法返回一个 Set,其中包含在给定房间中的所有 Socket 实例的 ID。
注意:我们也可以使用 io.of("/").sockets
对象,就像在第一部分中一样,但 allSockets()
方法也适用于多个 Socket.IO 服务器,这在扩展时将很有用。
回顾
好的,所以... 我们现在拥有的更好,但还有一个问题:消息实际上并没有在服务器上持久化。因此,当用户重新加载页面时,它会丢失所有现有的对话。
这可以通过例如将消息保存在浏览器的 localStorage 中来解决,但还有一个更令人讨厌的后果
- 当发送方断开连接时,它发送的所有数据包都会 缓冲,直到重新连接(在大多数情况下,这很好)
- 但当接收方断开连接时,数据包会丢失,因为给定房间中没有监听的 Socket 实例
我们将在本指南的 第三部分 中尝试解决这个问题。
感谢您的阅读!