跳至主要内容

私信 - 第一部

在本指南中,我们将创建以下应用程序

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", ...),会将 "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,这是本指南 第二部分 的主题。

感谢阅读!