前端栈学习(十六)-Socket
为什么需要 Socket?
由于 HTTP 的限制,我们的 CatBook 的 Chat 在每次获取新的聊天时都需要刷新下网页才能加载。
因此,我们需要Socket
来教服务器发送会话
什么是 Socket?
Socket 是网络通信的一个抽象概念,它提供了服务器和客户端之间双向通信的能力。通过 Socket,客户端和服务器可以持续地交换数据,而不需要像传统 HTTP 请求那样频繁建立和关闭连接。
在 JavaScript 中,最常用的 Socket 技术是 WebSocket 和基于库的实现(如 Socket.IO)
Socket IO
基本用法
1 | socketManager.getIo().emit("event_name", data); |
"event_name"
是订阅的主题
1 | socket.on("event_name", someFunction); |
someFunction
: 当你收到一个这样的 socket 事件时,应该做什么:
(data) => { do something with data }
私聊
1 | socketManager.getSocketFromUserID(“whiskers_id”).emit(“meow”, “FOOD”) |
如果使用getIo()
的话会导致所有人可见
详细流程解析
1. 用户连接到服务器
- 当一个用户(如 Whiskers)与服务器建立连接时,服务器会通过
socketManager.addUser
将用户和 socket 进行绑定。
在客户端,用户初始化 socket 连接并向服务器发送初始化请求。
App.js
1 | const handleLogin = (res) => { |
客户端代码 (Chatbook.js
):
1 | useEffect(() => { |
服务器端代码 (api.js
):
1 | router.post("/initsocket", (req, res) => { |
- 流程总结:
- 客户端向服务器发送请求初始化 socket 连接。
- 服务器调用
socketManager.addUser
,将用户和对应的 socket 绑定。
2. 将用户与对应的 Socket 进行绑定
- 服务器通过
socketManager.addUser
将用户的 ID 和 socket 进行映射,便于后续查找。
示例代码:
1 | router.post("/initsocket", (req, res) => { |
socketManager.addUser
是一个封装的函数,它负责记录用户和对应 socket 的关系。
3. 服务器端存储映射关系
socketManager
通过存储用户和 socket ID 的映射来实现快速查找。- 在客户端,用户的 socket 连接会持续更新 活跃用户列表。
服务器端代码 (api.js
):
1 | router.get("/activeUsers", (req, res) => { |
客户端代码 (Chatbook.js
):
1 | useEffect(() => { |
4. 校验用户信息
- 当用户发送消息时,服务器通过
auth.ensureLoggedIn
校验用户的登录状态。
服务器端代码 (api.js
):
1 | router.post("/message", auth.ensureLoggedIn, (req, res) => { |
- 流程总结:
- 客户端发送消息,服务器通过
auth.ensureLoggedIn
确保用户处于登录状态。 - 服务器将消息广播给目标用户或
ALL_CHAT
。
- 客户端发送消息,服务器通过
5. 实际数据通信过程
- 用户发送消息,服务器接收并保存到数据库,同时通过 socket 向目标用户或聊天室广播消息。
客户端代码 (Chatbook.js
):
1 | const addMessages = (data) => { |
- 当消息发送给当前活跃用户,客户端会调用
addMessages
更新聊天界面的消息状态。
服务器端代码 (api.js
):
1 | if (req.body.recipient._id === "ALL_CHAT") { |
- 流程总结:
- 用户通过 socket 发送消息。
- 服务器保存消息到数据库。
- 服务器向目标用户或所有用户广播消息。
- 客户端监听到
message
事件后更新消息列表。
6. 总结流程
- 用户连接到服务器:
- 客户端初始化 socket 连接,并向服务器发送
initsocket
请求。 - 服务器通过
socketManager.addUser
绑定用户与 socket。
- 客户端初始化 socket 连接,并向服务器发送
- 获取活跃用户:
- 服务器向客户端发送当前活跃用户列表。
- 客户端监听
activeUsers
事件并更新列表。
- 消息发送与接收:
- 客户端发送消息,服务器校验用户身份并保存消息。
- 服务器通过 socket 广播消息给目标用户或聊天室。
- 客户端接收消息并更新消息状态。
- 实时更新界面:
- 通过监听
message
和activeUsers
事件,客户端实现实时聊天和活跃用户列表的更新。
- 通过监听
流程图(总结)
- 用户初始化 socket → 服务器绑定用户与 socket
- 客户端请求活跃用户 → 服务器返回活跃用户列表
- 用户发送消息 → 服务器广播消息 → 客户端更新聊天界面
lab 解析
1. 客户端(Chatbook
组件)初始化:
在 React 前端中,Chatbook
组件负责管理聊天功能,用户状态以及与服务器进行通信。
代码片段:
1 | import { socket } from "../../client-socket.js"; |
- 导入
socket
对象,用于与服务器进行 Socket.IO 通信。 - 导入
get
函数,用于向服务器发送 HTTP 请求,比如加载消息历史记录和获取活跃用户。
2. 加载历史消息:
Chatbook
在组件挂载或切换聊天用户时,会加载历史聊天记录。
代码片段:
1 | const loadMessageHistory = (recipient) => { |
- 通过
/api/chat
GET 请求,从服务器端获取指定recipient
的历史聊天记录。 - 将返回的消息列表存储到组件状态
activeChat
中。
3. 监听服务器消息推送:
通过 Socket.IO 监听来自服务器的新消息并更新聊天状态。
代码片段:
1 | useEffect(() => { |
- 使用
socket.on("message", callback)
监听服务器发送的消息。 addMessages
函数将新的消息合并到当前聊天的消息列表中。
消息更新逻辑:
1 | const addMessages = (data) => { |
- 如果消息的
recipient
匹配当前的activeChat
,则将消息追加到messages
中。
4. 获取活跃用户列表:
客户端通过 REST API 和 Socket.IO 获取和监听在线用户列表。
代码片段(初次加载活跃用户):
1 | useEffect(() => { |
- 通过
/api/activeUsers
获取当前所有在线用户列表。
代码片段(Socket 更新活跃用户):
1 | useEffect(() => { |
- 监听服务器推送的
activeUsers
事件,实时更新在线用户状态。
5. 服务端 API 与 Socket.IO 交互:
在服务端,通过 socketManager
管理用户与 Socket.IO 实例,并处理消息发送和用户连接。
初始化 Socket 连接:
当用户连接时,通过 /initsocket
API 将用户与 Socket 关联。
1 | router.post("/initsocket", (req, res) => { |
socketManager.addUser
关联用户 ID 与 socket 实例。
处理消息发送:
在 /message
路由中,接收用户发送的消息并通过 Socket.IO 广播。
1 | router.post("/message", auth.ensureLoggedIn, (req, res) => { |
- 消息保存到数据库。
- 如果消息的
recipient
是ALL_CHAT
,通过getIo().emit
广播给所有用户。 - 否则,只发送给发送者和接收者。
获取在线用户:
在 /activeUsers
API 路由中,获取所有当前连接的用户列表。
1 | router.get("/activeUsers", (req, res) => { |
- 通过
socketManager.getAllConnectedUsers()
获取所有在线用户。
6. 客户端与服务端事件对接:
客户端事件 | 服务端监听/触发 | 描述 |
---|---|---|
socket.on("message") |
socketManager.getIo().emit("message") |
广播消息给所有连接的客户端。 |
socket.on("activeUsers") |
手动触发 activeUsers |
更新当前在线用户列表。 |
HTTP 请求 /api/chat |
MongoDB 查询 Message 表 |
获取历史消息记录。 |
HTTP 请求 /api/activeUsers |
socketManager.getAllConnectedUsers() |
获取所有当前在线用户。 |
HTTP 请求 /initsocket |
socketManager.addUser |
将用户与 socket 实例关联。 |
总结流程:
- 客户端启动,加载在线用户列表和历史聊天记录。
- 初始化 Socket 连接,客户端监听
message
和activeUsers
事件。 - 用户通过输入消息触发
/message
API,服务端保存消息并通过 Socket.IO 推送给接收方。 - 服务端在用户上线或下线时,触发
activeUsers
事件,客户端实时更新在线用户状态。 - 客户端界面通过 React 状态管理实时更新聊天内容和用户列表。
通过以上流程,前端 React 组件和服务端 API 通过 Socket.IO 实现了双向实时通信,同时保证了数据的持久化存储和在线状态管理。
前后端交互逻辑
从用户角度看,当用户第一次登录网站、进入 ALL_CHAT(公共聊天)以及与他人进行私聊的整个流程,涉及一系列前后端交互,下面我会详细解释流程:
1. 第一次登录网站
前端:
用户打开网站
在
App
组件的useEffect
中,前端调用/api/whoami
接口,检查当前用户是否已登录。1
2
3
4
5
6
7useEffect(() => {
get("/api/whoami").then((user) => {
if (user._id) {
setUserId(user._id);
}
});
}, []);如果用户未登录,返回空对象,用户无法使用功能;如果登录成功,用户信息会保存在
userId
状态中。
登录操作
用户通过第三方登录(例如 Google)登录后,前端会调
/api/login
接口,将登录信息发送给后端。1
2
3
4post("/api/login", { token: userToken }).then((user) => {
setUserId(user._id);
post("/api/initsocket", { socketid: socket.id });
});登录后,前端初始化用户的 WebSocket 连接,调用
/api/initsocket
。
后端:
/api/login
接收用户的登录请求,验证身份后保存用户会话信息。/api/initsocket
将用户和其 WebSocket 连接绑定,方便后续消息推送。
2. 进入 ALL_CHAT 公共聊天
前端:
用户进入
/chat/
路由,渲染Chatbook
组件。通过
useEffect
调用/api/activeUsers
接口,获取所有当前活跃用户列表,包括ALL_CHAT
。1
2
3
4
5get("/api/activeUsers").then((data) => {
if (props.userId) {
setActiveUsers([ALL_CHAT].concat(data.activeUsers));
}
});
ChatList
渲染用户列表:- 用户列表通过
map
显示每个用户,SingleUser
组件负责显示单个用户的名称。 active
属性 用于标记当前选中的聊天对象,CSS 变化表示用户处于活跃状态。- 点击某个用户时,触发
props.setActiveUser(user)
。
- 用户列表通过
useEffect
中触发加载历史消息:当用户初始加载
ALL_CHAT
或点击其他用户时,调用loadMessageHistory
,请求历史消息:1
2
3
4
5
6
7
8const loadMessageHistory = (recipient) => {
get("/api/chat", { recipient_id: recipient._id }).then((messages) => {
setActiveChat({
recipient: recipient,
messages: messages,
});
});
};
后端:
/api/activeUsers
返回所有当前活跃的用户列表。/api/chat
:- 如果
recipient_id
是"ALL_CHAT"
,查询数据库中发送到ALL_CHAT
的消息。 - 返回这些消息给前端。
- 如果
3. 与他人私聊
前端:
用户点击某个用户
在
ChatList
中,点击用户触发setActiveUser(user)
:1
2
3
4
5
6
7
8const setActiveUser = (user) => {
if (user._id !== activeChat.recipient._id) {
setActiveChat({
recipient: user,
messages: [],
});
}
};useEffect
监听activeChat.recipient._id
的变化,调用loadMessageHistory
,加载与该用户的历史聊天记录。
接收消息的实时更新
前端通过 WebSocket 监听
"message"
事件:1
2
3
4
5
6useEffect(() => {
socket.on("message", addMessages);
return () => {
socket.off("message", addMessages);
};
}, [activeChat.recipient._id, props.userId]);当新消息推送到前端时,
addMessages
会将消息追加到当前活跃聊天的messages
中。
发送消息(假设通过输入框发送)
用户发送消息时,前端通过
/api/message
向后端发送消息。1
2
3
4post("/api/message", {
recipient: activeChat.recipient,
content: messageContent,
});
后端:
存储和广播消息
/api/message
保存消息到数据库,同时通过 WebSocket 发送消息给接收方:1
2
3
4
5
6
7if (req.body.recipient._id == "ALL_CHAT") {
socketManager.getIo().emit("message", message);
} else {
socketManager
.getSocketFromUserID(req.body.recipient._id)
.emit("message", message);
}
实时同步活跃用户
- 通过 WebSocket 事件
"activeUsers"
推送当前在线用户列表。
- 通过 WebSocket 事件
4. 用户切换聊天对象
当用户在
ChatList
中点击其他用户时:setActiveUser
更新当前活跃聊天对象,并清空消息。useEffect
会自动调用loadMessageHistory
,加载与新用户的历史消息。
切换时的消息同步
如果后台推送了新消息,
addMessages
会检查新消息的接收者是否与当前聊天对象匹配:1
2
3
4setActiveChat((prevActiveChat) => ({
recipient: prevActiveChat.recipient,
messages: prevActiveChat.messages.concat(data),
}));
总结流程
登录时:前端获取当前用户信息并初始化 WebSocket。
进入聊天页:加载活跃用户列表和
ALL_CHAT
历史消息。点击用户:加载私聊历史消息并切换
activeChat
。消息实时同步:
- 前端监听
"message"
,将新消息追加到当前聊天中。 - 后端通过 WebSocket 推送消息给接收方或广播给所有用户。
- 前端监听
切换聊天:
setActiveUser
切换聊天对象,重新加载对应历史消息。
这样,整个前后端交互的逻辑就完整了!核心是通过 API 加载历史消息、WebSocket 实时同步新消息,以及 activeChat
状态控制当前活跃聊天对象。