前言
总想做点什么,但又没什么可做,就想的从写一下自己博客的后台,然后就走上了一条不归路,看到有个设计稿很不错,就先照着样子做了框架,又见聊天功能心血来潮,说实现一下此功能,这里主要为简单搭建,后期还会在做修改。
什么是 Socket.io
Socket.io 是一个十分流行的 JavaScript 库。它允许我们在浏览器和 Node.js 服务端之间创建一个实时的,双向的通信。它是一个高性能并且可靠的库,经过优化可以以最小的延迟处理大量数据。它遵循 WebSocket 协议并提供更好的功能,比如降级为 HTTP 长链接或者自动重连,这些功能可以协助我们构建一个高效的实时的应用。
创建 node
应用
创建目录安装所需依赖 (express
nodemon
cors
socket.io
)
mkdir socket_node
cd socket_node && npm init --yes
npm i express nodemon cors socket.io
修改 package.json
中的 scripts
...
"scripts": {
"dev": "nodemon index.js"
},
...
创建 index.js
作为 web 服务器的入口文件
touch index.js
使用 Express 创建一个简单的 Node.js 服务。当我们在浏览器访问 http://localhost:5555/
的时候,下面的代码会返回一段文字
const express = require('express')
const http = require('http')
const cors = require('cors')
// 实例化 express
const app = express()
const PORT = 5555
// 创建一个 http 服务器
const server = http.createServer(app)
// 跨域处理
app.use(cors())
app.get('/', (req, res) => {
res.send('work...')
})
// 监听端口号
server.listen(PORT, () => {
console.log(`服务器正在${PORT}端口运行`)
})
引入 socket.io
到项目中并且创建一个实时连接
...
// 初始化连接用户
let connectedUsers = {}
const io = require('socket.io')(server, {
// 设定跨域
cors: {
// 地址配置,允许那些网址
origin: '*',
// 定义可请求的方法
methods: ['GET', 'POST'],
},
})
io.on('connection', (socket) => {
console.log(`socket 客户端已连接${socket.id}`)
// 用户刷新或关闭触发
socket.on('disconnect', () => {
console.log(`socket 客户端已断开`)
// TODO...
)
})
...
从上面的代码中看到,每当用户访问页面的时候,io.on("connection")
建立了连接,然后为每个 socket 创建一个唯一的 ID ,然后将 ID 打印到控制台
当你刷新或者关闭页面,socket 会触发 disconnect 事件,显示一个用户已从 socket 断开链接。
客户端链接
// utils/wss.js
import io from 'socket.io-client'
const SERVER = //localhost:5555
let socket = null
export const connectWithScoketIOServer = () => {
socket = io(SERVER)
socket.on('connect', () => {
console.log('socket 成功链接')
console.log(socket.id)
})
}
将代码在 main.js
中引入
/// main.js
...
import { connectWithScoketIOServer } from './utils/wss'
connectWithScoketIOServer()
...
以上代码,链接成功控制台打印 socket 成功链接
和 socket.id
关于 socket 的 .emit
.to
.on
- emit: 发送出去的事件
- to: 给谁发送
- on: 用于监听事件和 emit 出来的事件
初始化用户
客户端
/// utils/wss.js
...
export const createChat = (data) => {
socket.emit('conn-init', data)
}
...
将以上代码引入到需要聊天的界面
/// /views/main/chat/chat-page.vue
import { useRoute } from 'vue-router'
import {
createChat,
} from './../../../utils/wss'
const route = useRoute()
createChat({
sender: route.query.sender, // 当前用户 ID
receiver: route.query.receiver // 发送给用户 ID
})
node服务端
/// index.js
io.on('connection', (socket) => {
// 用户初始化
socket.on('conn-init', (data) => {
connectedUsers = {
...connectedUsers,
[data.sender]: socket.id,
}
)
})
从上面代码中可以看到,我在 conn-init
时,将当前用户和 socketId 做一个配对,保存起来,这么做的原因是,我给某位用户发送信息时方便操作
发送消息
客户端
/// utils/wss.js
...
import { ref } from 'vue'
export const connectWithScoketIOServer = () => {
...
// 接收服务端发来的消息
socket.on('direct-message', (data: any) => {
directMessageHandler(data)
})
...
}
export const sendMessage = (identity) => {
// identity
// {
// messag, // 需要发送的消息
// userId, // 发送给谁
// id, // 当前用户
// type // 收发者类型
// }
const data = {
...identity,
socketId: socket.id
}
socket.emit('message', data)
}
export const messageContent = ref([
{
message: 'Hey Hi Elena Damyanti…!',
type: 'receiver',
userId: '2',
id: '1'
},
{
message:
`HThanks, all things went well.
Just a little boaring at home.
desktop publishing software like Aldus PageMaker
including versions of Lorem Ipsum.`,
type: 'sender',
userId: '1',
id: '2'
}
])
const directMessageHandler = (data) => {
// 将当前接受过来的值存到 messageContent 当中
messageContent.value.push(data)
}
...
/// /views/main/chat/chat-page.vue
import { useRoute } from 'vue-router'
import {
createChat,
sendMessage
} from './../../../utils/wss'
const route = useRoute()
createChat({
sender: route.query.sender, // 当前用户 ID
receiver: route.query.receiver // 发送给用户 ID
})
// 发送消息事件
const handleClickSendMessage = async () => {
await sendMessage({
message: message.value,
userId: route.query.receiver,
id: route.query.sender,
type: 'sender'
})
message.value = ''
}
node服务端
/// index.js
io.on('connection', (socket) => {
...
// 接收信息
socket.on('message', (data) => {
directMessageHandler(data, socket)
})
...
})
const TYPE_STATUS = {
receiver: 'sender',
sender: 'receiver',
}
const directMessageHandler = async (data, socket) => {
const identity = {
...data,
type: TYPE_STATUS[data.type], // 需要做一个去反,因为发送过来时,
// 此 type 为 sender 所以发送个某个用户时,做一个去反
id: data.userId,
userId: data.id,
}
socket.to(connectedUsers[data.userId]).emit('direct-message', { ...identity })
socket.emit('direct-message', { ...data })
}
以上代码可以做到接收信息了,现在一对一聊天的功能就已经做完了,但是有一个弊端,如果对方此时不在线,就做不到接收消息了,所以需要做一个离线缓存的功能,这里我的想法是做一个消息暂存区,如果当前用户 触发了 conn-int
事件时,我去取出离线缓存到的记录,做一个派发,接下来,我们看代码
/// index.js
io.on('connection', (socket) => {
...
// 初始化用户
socket.on('conn-init', (data) => {
...
// 进行一个判断,是否有离线缓存的记录
messageStoreHandler({}, socket, data.sender)
})
...
})
const TYPE_STATUS = {
receiver: 'sender',
sender: 'receiver',
}
const directMessageHandler = async (data, socket) => {
// 发送消息时,走中间件,判断对方是否为离线状态
await messageStoreHandler(data, socket, data.sender)
const identity = {
...data,
type: TYPE_STATUS[data.type], // 需要做一个去反,因为发送过来时,
// 此 type 为 sender 所以发送个某个用户时,做一个去反
id: data.userId,
userId: data.id,
}
socket.to(connectedUsers[data.userId]).emit('direct-message', { ...identity })
socket.emit('direct-message', { ...data })
}
const userMessageStore = {}
const messageStoreHandler = async (data, socket, userId) => {
// 判断当前 connectUsers 中是否有该用户的信息,如果没有进行缓存
if (!connectedUsers[data.userId] && data.userId !== undefined) {
// 如果 data.userId 不为空,说明是发送消息时走的中间件,所以进行缓存
if (data.userId) {
let messageStore = []
if (userMessageStore[data.userId]) {
messageStore = [...userMessageStore[data.userId]]
}
userMessageStore[data.userId] = [...messageStore, data]
}
return false
}
// 判断 userId 是否为 null 为 null 说明时发送消息时触发的该方法 如果有,则是当前用户登陆了
// 判断 userMessageStore 是否有该用户的缓存记录,如果没有,return false,如果有则反之
if (userId && userMessageStore[userId]?.length) {
// 做一个缓存循环,发送当前信息
for (let i = 0; i < userMessageStore[userId].length; i++) {
const newData = userMessageStore[userId][i]
const identity = {
...newData,
type: TYPE_STTAUS[newData.type],
id: newData.userId,
userId: newData.id,
}
// 这里用 io 而不是 socket 的问题是因为不能自己给自己发送消息
await io
.to(connectedUsers[userId])
.emit('direct-message', { ...identity })
}
// 当信息发完,自动删除当前用户的缓存记录
delete userMessageStore[userId]
return false
}
}
以上代码是我的一些想法,可能后期还会有更优的选择,慢慢来,如果大家有更好的方法,请邮箱留言给我 w920098695@sina.cn,谢谢
总结
Socket.io 是一个非常棒的工具,具有出色的功能,使我们能够构建高效的实时应用程序,例如体育网站、拍卖和外汇交易应用程序,当然还有通过在 Web 浏览器和 Node.js 服务器之间创建持久连接的聊天应用程序
如果你期待在 Node.js 中构建聊天应用程序,Socket.io 可能是一个很好的选择
- 原文代码
忽略目前的界面还未完善如果喜欢可以去 GitHub 自行拉去
https://github.com/nishuiwei/admin-for-blog - 前端代码
前端测试 demo
启动项目之后