私信 - 第一部
在本指南中,我们将创建以下应用程序
我们将涵盖以下主题
先决条件
本指南包含四个不同的部分
让我们开始吧!
安装
首先,让我们获取聊天应用程序的初始实现
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,您应该会看到
运行服务器
现在,让我们启动服务器
cd server
npm install
npm start
您的控制台应该打印
server listening at http://localhost:3000
到目前为止,一切顺利!您应该能够打开多个选项卡并在它们之间发送一些消息

工作原理
服务器初始化
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
上,因此我们处于跨域情况下)。
文档
- 跨域资源共享 (CORS)
- Socket.IO CORS 配置
客户端初始化
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
开始,因此会显示选择用户名的表单
表单提交后,我们将到达 onUsernameSelection
方法
onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
}
我们在 auth
对象中附加 username
,然后调用 socket.connect()
。
如果您在开发者工具中打开网络选项卡,您应该会看到一些 HTTP 请求
- Engine.IO 握手(包含会话 ID - 在这里,
zBjrh...AAAK
- 用于后续请求) - Socket.IO 握手请求(包含
auth
选项的值) - Socket.IO 握手响应(包含 Socket#id)
- WebSocket 连接
- 第一个 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.id
或 socket.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);
});
用户列表显示在左侧面板上
私信
选择某个用户时,右侧面板会显示一个聊天窗口
以下是私信的实现方式
客户端(发送方)
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;
}
});
});
您可以通过停止服务器来测试它

回顾
好的,所以... 我们现在拥有的功能很棒,但有一个明显的问题

解释:重新连接时会生成一个新的 Socket ID,因此每次用户断开连接并重新连接时,都会获得一个新的用户 ID。
这就是为什么我们需要一个持久用户 ID,这是本指南 第二部分 的主题。
感谢阅读!