🌐 实现一个局域网聊天室(支持公网)
前言
这篇文章将介绍如何实现一个简单的局域网聊天室(当然也支持部署到公网)。后端使用 Express + MongoDB,前端采用 Vue 3。
由于是聊天系统,我们需要建立一个长连接。目前较常见的两种方式是:
- WebSocket:双向通信,适用于对实时性要求较高的应用。
- SSE(Server-Sent Events):单向通信,适用于服务器向客户端推送数据的场景。
本文将专注于对比和实现 SSE 的方式。
💡 注:SSE 的优势在于实现简单,基于 HTTP 协议,天然支持断线重连和浏览器兼容(除 IE)。
需求分析
我们的核心需求非常简单:
- 发送消息
- 获取历史消息
除此之外,我们还需要一个用于建立 SSE 长连接的接口,用来让客户端接收其他用户发送的消息:
- 发送消息接口
- 获取历史消息接口
- SSE 接口(用于建立长连接)
代码实现部分
后端
我们使用 Express 搭建服务器,并使用 MongoDB 存储聊天记录。
接口逻辑
const express = require('express')
const cors = require('cors')
const mongoose = require('mongoose')
const Message = require('./models/Message')
const app = express()
app.use(cors())
app.use(express.json())
mongoose.connect('mongodb://127.0.0.1:27017/chat-sse-demo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
mongoose.connection.once('open', () => {
console.log('✅ MongoDB 已连接')
})
let clients = [] // 所有连接的 SSE 客户端
// SSE 接口
app.get('/api/chat/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.flushHeaders()
const clientId = Date.now()
const newClient = {
id: clientId,
res,
}
clients.push(newClient)
// client 断开连接时,移除该客户端 (客户端关闭网页,刷新页面,网络异常断开
// 避免内存泄漏
req.on('close', () => {
clients = clients.filter(client => client.id !== clientId)
})
})
// 心跳机制
// 每 15 秒向所有客户端发送一个空消息,保持连接活跃
setInterval(() => {
clients.forEach(client => {
client.res.write(':\n\n') // SSE 心跳格式
})
}, 15000)
// 接收用户输入的接口
app.post('/api/chat/message', async (req, res) => {
const { message, userId, timestamp } = req.body
if (!message || !userId) {
return res.status(400).json({ error: '缺少 用户ID 或 消息' })
}
// 存储用户消息
await Message.create({ message, userId, timestamp })
// 向所有客户端广播整条消息
const payload = JSON.stringify({ message, userId, timestamp })
clients.forEach(client => {
client.res.write(`data: ${payload}\n\n`)
})
return res.json({ status: 'ok', message: '消息已接收' })
})
app.get('/api/chat/history', async (req, res) => {
const messages = await Message.find().sort({ timestamp: 1 })
res.json(messages)
})
const PORT = 3000
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`)
})
小结
/api/chat/stream:建立 SSE 长连接/api/chat/message:发送消息并广播/api/chat/history:获取历史记录
前端实现(Vue 3)
前端实现了一个简洁的聊天界面,并展示在线用户。关键逻辑如下:
onMounted(() => {
getChatHistory()
startStream()
})
const startStream = () => {
eventSource = new EventSource('/api/chat/stream')
eventSource.onmessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (data.userId !== myId) {
chatMessages.value.push(data)
addUserIfNotExists(data.userId)
scrollToBottom()
}
}
eventSource.onerror = () => {
console.error('SSE 连接出错,尝试重连...')
eventSource?.close()
setTimeout(() => {
startStream()
}, 1000)
}
}
const getChatHistory = async () => {
const response = await fetch('/api/chat/history')
const data = await response.json()
chatMessages.value = data.map((msg) => {
addUserIfNotExists(msg.userId)
return msg
})
scrollToBottom()
}
const sendMessage = async () => {
if (!input.value.trim()) return
try {
// 本地显示
const timestamp = new Date().toISOString()
chatMessages.value.push({ message: input.value, userId: myId, timestamp })
addUserIfNotExists(myId)
scrollToBottom()
await fetch('/api/chat/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: input.value, userId: myId, timestamp }),
})
} catch (error) {
console.error('发送消息失败:', error)
}
input.value = ''
}
小结
-
页面加载后会:
- 拉取历史消息
- 建立 SSE 长连接
-
接收新消息时:
- 添加到消息列表
- 自动滚动到底部
-
发送消息时:
- 先本地展示
- 再发送给后端广播
⚠️ 当前 SSE 是单向通信,因此本地用户的消息需要前端手动显示。
🔗 项目地址
👉 代码都在
develop分支中