egg安装websocket插件
npm i egg-websocket-plugin --save
开启插件
//config/plugin.js
websocket: {
enable: true,
package: 'egg-websocket-plugin',
},
配置websocket路由
//app/router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
//中间件
app.ws.use(async (ctx, next) => {
// 获取参数 ws://localhost:7001/ws?token=123456
// ctx.query.token
// 验证用户token 安全性考虑
let user = {};
let token = ctx.query.token; //获取token
try {
user = ctx.checkToken(token); //验证token
// 验证用户状态 //查询用户是否存在
let userCheck = await app.model.User.findByPk(user.id);
if (!userCheck) {
//不存在提示
ctx.websocket.send(JSON.stringify({
msg: "fail",
data: '用户不存在'
}));
return ctx.websocket.close(); //关闭连接
}
//验证是否禁用
if (userCheck.status) {
ctx.websocket.send(JSON.stringify({
msg: "fail",
data: '你已被禁用'
}));
return ctx.websocket.close();//关闭连接
}
// 用户上线 user是自定义 挂载到ws上面
app.ws.user = app.ws.user ? app.ws.user : {};
// 下线其他设备
if (app.ws.user[user.id]) {
app.ws.user[user.id].send(JSON.stringify({
msg: "fail",
data: '你的账号在其他设备登录'
}));
app.ws.user[user.id].close(); //关闭其他连接
}
// 记录当前用户id
ctx.websocket.user_id = user.id;
app.ws.user[user.id] = ctx.websocket;
ctx.online(user.id);
await next();
} catch (err) {
console.log(err);
let fail = err.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!';
ctx.websocket.send(JSON.stringify({
msg: "fail",
data: fail
}))
// 关闭连接
ctx.websocket.close();
}
});
// websocket
app.ws.route('/ws', controller.chat.connect);
// 发送消息
router.post('/chat/send', controller.chat.send);
};
全局中间件验证token
middleware/auth.js
module.exports = (option, app) => {
return async (ctx, next) => {
//1. 获取 header 头token
let token = ctx.header.token || ctx.query.token;
if (!token) {
ctx.throw(400, '您没有权限访问该接口!');
}
//2. 根据token解密,换取用户信息
let user = {};
try {
user = ctx.checkToken(token); //验证token
} catch (error) {
let fail = error.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!';
ctx.throw(400, fail);
}
//3. 判断当前用户是否登录
let t = await ctx.service.cache.get('user_' + user.id);
if (!t || t !== token) {
ctx.throw(400, 'Token 令牌不合法!');
}
//4. 获取当前用户,验证当前用户是否被禁用
user = await app.model.User.findByPk(user.id);
if (!user || user.status == 0) {
ctx.throw(400, '用户不存在或已被禁用');
}
// 5. 把 user 信息挂载到全局ctx上
ctx.authUser = user;
await next();
}
}
公共方法
extend/context.js
module.exports = {
// 成功提示
apiSuccess(data = '', msg = 'ok', code = 200) {
this.body = { msg, data };
this.status = code;
},
// 失败提示
apiFail(data = '', msg = 'fail', code = 400) {
this.body = { msg, data };
this.status = code;
},
// 生成token
getToken(value) {
return this.app.jwt.sign(value, this.app.config.jwt.secret);
},
// 验证token
checkToken(token) {
return this.app.jwt.verify(token, this.app.config.jwt.secret);
},
// 发送或者存到消息队列中
async sendAndSaveMessage(to_id, message, msg = 'ok') {
const { app, service } = this;
let current_user_id = this.authUser.id;
// 拿到接受用户所在子进程
let pid = await service.cache.get('online_' + to_id);
if (pid) {
// 消息推送 app.messenger.sendTo是egg进程间的通讯 发送给指定进程 这个需要在根目录创建一个app.js来监听事件 'send'事件名称
app.messenger.sendTo(pid, 'send', {
to_id, message, msg
});
// 存到历史记录当中
if (msg === 'ok') {
service.cache.setList(`chatlog_${to_id}_${message.chat_type}_${current_user_id}`, message);
}
} else {
service.cache.setList('getmessage_' + to_id, {
message,
msg
});
}
// 拿到对方的socket
// let socket = app.ws.user[to_id];
// 验证对方是否在线?不在线记录到待接收消息队列中;在线,消息推送,存储到对方的聊天记录中 chatlog_对方用户id_user_当前用户id
// if (app.ws.user && app.ws.user[to_id]) {
// // 消息推送
// app.ws.user[to_id].send(JSON.stringify({
// msg,
// data: message
// }));
// // 存到历史记录当中
// if (msg === 'ok') {
// service.cache.setList(`chatlog_${to_id}_${message.chat_type}_${current_user_id}`, message);
// }
// } else {
// service.cache.setList('getmessage_' + to_id, {
// message,
// msg
// });
// }
},
// 发送消息
async send(to_id, message, msg = 'ok') {
const { app, service } = this;
let current_user_id = this.authUser.id;
// 拿到接受用户所在子进程
let pid = await service.cache.get('online_' + to_id);
if (pid) {
// 消息推送 app.messenger.sendTo是egg进程间的通讯 发送给指定进程 这个需要在根目录创建一个app.js来监听事件 'send'事件名称
app.messenger.sendTo(pid, 'send', {
to_id, message, msg
});
}
},
// 用户上线 多进程管理 需要安装 npm i egg-scripts --save
async online(user_id) {
const { service, app } = this;
let pid = process.pid; //进程pid
// 下线其他设备
let opid = await service.cache.get('online_' + user_id);
if (opid) {
// 通知对应进程用户下线 app.messenger.sendTo是egg进程间的通讯 发送给指定进程 这个需要在根目录创建一个app.js来监听事件 'offline'事件名称
app.messenger.sendTo(opid, 'offline', user_id);
}
// 存储上线状态
service.cache.set('online_' + user_id, pid);
}
};
控制器
controller/chat.js
'use strict';
const Controller = require('egg').Controller;
class ChatController extends Controller {
// 连接socket
async connect() {
const { ctx, app, service } = this;
if (!ctx.websocket) {
ctx.throw(400, '非法访问');
}
console.log(`clients: ${app.ws.clients.size}`);
// 监听接收消息和关闭socket
ctx.websocket.on('message', msg => {
console.log('接收消息', msg);
}).on('close', (code, reason) => {
// 用户下线
console.log('用户下线', code, reason);
let user_id = ctx.websocket.user_id;
// 移除redis中的用户上线记录
service.cache.remove('online_' + user_id);
if (app.ws.user && app.ws.user[user_id]) {
//删除对象的键值对
delete app.ws.user[user_id];
}
});
}
// 发送消息
async send() {
const { ctx, app, service } = this;
// 拿到当前用户id
let current_user_id = ctx.authUser.id;
// 验证参数
ctx.validate({
to_id: { type: 'int', required: true, desc: '接收人/群id' },
chat_type: { type: 'string', required: true, range: { in: ['user', 'group'] }, desc: '接收类型' },
type: { type: 'string', required: true, range: { in: ['text', 'image', 'video', 'audio', 'emoticon', 'card'] }, desc: '消息类型' },
data: { type: 'string', required: true, desc: '消息内容' },
options: { type: 'string', required: true }
});
// 获取参数
let { to_id, chat_type, type, data, options } = ctx.request.body;
// 单聊
if (chat_type === 'user') {
// 验证好友是否存在,并且对方没有把你拉黑
let Friend = await app.model.Friend.findOne({
where: {
user_id: to_id,
friend_id: current_user_id,
isblack: 0
},
include: [{
model: app.model.User,
as: "userInfo"
}, {
model: app.model.User,
as: "friendInfo"
}]
});
if (!Friend) {
return ctx.apiFail('对方不存在或者已经把你拉黑');
}
// 验证好友是否被禁用
if (!Friend.userInfo.status) {
return ctx.apiFail('对方已被禁用');
}
// 构建消息格式
let from_name = Friend.friendInfo.nickname ? Friend.friendInfo.nickname : Friend.friendInfo.username;
if (Friend.nickname) {
from_name = Friend.nickname;
}
let message = {
id: (new Date()).getTime(), // 唯一id,后端生成唯一id
from_avatar: Friend.friendInfo.avatar,// 发送者头像
from_name, // 发送者昵称
from_id: current_user_id, // 发送者id
to_id, // 接收人/群 id
to_name: Friend.userInfo.nickname ? Friend.userInfo.nickname : Friend.userInfo.username, // 接收人/群 名称
to_avatar: Friend.userInfo.avatar, // 接收人/群 头像
chat_type: 'user', // 接收类型
type,// 消息类型
data, // 消息内容
options: {}, // 其他参数
create_time: (new Date()).getTime(), // 创建时间
isremove: 0, // 是否撤回
}
// 视频,截取封面
if (message.type === 'video') {
message.options.poster = message.data + '?x-oss-process=video/snapshot,t_10,m_fast,w_300,f_png';
}
// 音频,带上音频时长
if (message.type === 'audio') {
options = JSON.parse(options);
message.options.time = options.time || 1;
}
// 名片
if (message.type === 'card') {
// 验证名片用户是否存在
message.options = JSON.parse(options)
}
ctx.sendAndSaveMessage(to_id, message);
// 存储到自己的聊天记录中 chatlog_当前用户id_user_对方用户id
service.cache.setList(`chatlog_${current_user_id}_${message.chat_type}_${to_id}`, message);
// 返回成功
return ctx.apiSuccess(message);
}
// 群聊
// 验证群聊是否存在,且你是否在该群中
let group = await app.model.Group.findOne({
where: {
status: 1,
id: to_id
},
include: [{
model: app.model.GroupUser,
attributes: ['user_id', 'nickname']
}]
});
if (!group) {
return ctx.apiFail('该群聊不存在或者已被封禁');
}
let index = group.group_users.findIndex(item => item.user_id === current_user_id);
if (index === -1) {
return ctx.apiFail('你不是该群的成员');
}
// 组织数据格式
let from_name = group.group_users[index].nickname;
let message = {
id: (new Date()).getTime(), // 唯一id,后端生成唯一id
from_avatar: ctx.authUser.avatar,// 发送者头像
from_name: from_name || ctx.authUser.nickname || ctx.authUser.username, // 发送者昵称
from_id: current_user_id, // 发送者id
to_id, // 接收人/群 id
to_name: group.name, // 接收人/群 名称
to_avatar: group.avatar, // 接收人/群 头像
chat_type: 'group', // 接收类型
type,// 消息类型
data, // 消息内容
options: {}, // 其他参数
create_time: (new Date()).getTime(), // 创建时间
isremove: 0, // 是否撤回
group: group
}
// 视频,截取封面
if (message.type === 'video') {
message.options.poster = message.data + '?x-oss-process=video/snapshot,t_10,m_fast,w_300,f_png';
}
// 音频,带上音频时长
if (message.type === 'audio') {
options = JSON.parse(options);
message.options.time = options.time || 1;
}
// 名片
if (message.type === 'card') {
// 验证名片用户是否存在
message.options = JSON.parse(options)
}
// 推送消息
group.group_users.forEach(item => {
if (item.user_id !== current_user_id) {
ctx.sendAndSaveMessage(item.user_id, message);
}
});
ctx.apiSuccess(message);
}
}
module.exports = ChatController;
前端vue连接websocket
src目录下新建文件utils/websocket.js
let webSocket = null;
let socketOpen = false;
// 发送消息
export const doSend = (message) => {
if (socketOpen) {
webSocket.send(message)
}
}
// 初始化websocket
export const contactSocket = () => {
if ("WebSocket" in window) {
let token=`eyJhbGciOiJIUzI1N`
webSocket = new WebSocket("ws://127.0.0.1:7001/ws?token="+token);
webSocket.onopen = function () {
console.log("连接成功!");
socketOpen = true
};
webSocket.onmessage = function (evt) {
var received_msg = evt.data;
console.log("接受消息:" + received_msg);
};
webSocket.onclose = function () {
console.log("连接关闭!");
};
webSocket.onerror = function () {
console.log("连接异常!");
};
}
}
其他页面引用
import { contactSocket, doSend } from '@/utils/websocket.js'
// 初始化websocket
contactSocket()
// 发送消息内容
setTimeout(() => {
doSend("向服务端发送的消息")
}, 1000)
封装websocket的类
chat.js
class chat {
constructor(arg) {
this.url = arg.url //连接websocket的url
this.isOnline = false //用户是否上线成功
this.socket = null //存储contactSocket
this.reconnectTime = 0
this.isOpenReconnect = true
// 获取当前用户相关信息
let token = window.localStorage.getItem('token')
this.token = token ? token : {}
// 连接和监听
if(this.token){
this.connectSocket()
}
}
// 断线重连
reconnect(){
if(this.isOnline){
return
}
if(this.reconnectTime >= 20){
return this.reconnectConfirm()
}
this.reconnectTime += 1
this.connectSocket()
}
// 断线重连提示
reconnectConfirm(){
this.reconnectTime = 0
this.connectSocket()
}
// 连接socket
connectSocket(){
this.socket = new WebSocket(this.url+"?token="+this.token);
// 监听连接成功
this.socket.onopen=()=>this.onOpen()
// 监听接收信息
this.socket.onmessage=res=>this.onMessage(res)
// 监听断开
this.socket.onclose=()=>this.onClose()
// 监听错误
this.socket.onerror=()=>this.onError()
}
// 监听打开
onOpen(){
// 用户上线
this.isOnline = true
this.isOpenReconnect = true
console.log('socket连接成功')
// 获取用户离线消息
}
async setOnmessageMessage(event) {
console.log(event.data, '获得消息');
// 自定义全局监听事件
window.dispatchEvent(new CustomEvent('onmessageWS', {
detail: {
data: event.data
}
}))
// //发现消息进入 开始处理前端触发逻辑
// if (event.data === 'success' || event.data === 'heartBath') return
}
// 监听关闭
onClose(){
// 用户下线
this.isOnline = false
this.socket = null
if(this.isOpenReconnect){
this.reconnect()
}
console.log('socket连接关闭')
}
// 监听连接错误
onError(){
// 用户下线
this.isOnline = false
this.socket = null
if(this.isOpenReconnect){
this.reconnect()
}
console.log('socket连接错误')
}
// 监听接收消息
onMessage(data){
let res = JSON.parse(data.data)
console.log('监听接收消息',res)
this.setOnmessageMessage(data)
}
// 关闭连接
close(){
if(this.socket){
this.socket.close()
}
this.isOpenReconnect = false
}
}
export default chat
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Chat from '@/assets/common/chat.js'
import $C from '@/assets/common/config.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user:null,
chat:null //需要存起来
},
actions: {
// 存储登录状态
login({ state },user){
state.user = user
window.sessionStorage.setItem('user',JSON.stringify(user))
window.sessionStorage.setItem('token',user.token)
//连接socket
state.chat= new Chat({
url:$C.websocketUrl
})
},
// 初始化用户信息
initUser({ state }){
let user = window.sessionStorage.getItem('user')
if(user){
state.user = user ? JSON.parse(user) : null
//连接socket
state.chat= new Chat({
url:$C.websocketUrl
})
}
},
// 清除登录状态
logout({ state }){
state.user = null
window.sessionStorage.removeItem('user')
window.sessionStorage.removeItem('token')
//关闭socket连接
state.chat.close()
state.chat=null
}
}
})
vue其他组件监听消息
<template>
<div>
{{msg}}
</div>
</template>
<script>
export default {
data() {
return {
msg:''
}
},
mounted () {
// 添加socket通知监听
window.addEventListener('onmessageWS', this.getSocketData)
},
methods: {
// 收到消息处理
getSocketData (res) {
let a=JSON.parse(res.detail.data)
this.msg=a.data
if (res.detail.data === 'success' || res.detail.data === 'heartBath') return
// ...业务处理
},
}
}
</script>
egg多进程管理
npm i egg-scripts --save
根目录下创建app.js
class AppBootHook {
constructor(app) {
this.app = app;
}
configWillLoad() {
// 此时 config 文件已经被读取并合并,但是还并未生效
// 这是应用层修改配置的最后时机
// 注意:此函数只支持同步调用
}
async didLoad() {
// 所有的配置已经加载完毕
// 可以用来加载应用自定义的文件,启动自定义的服务
}
async willReady() {
// 所有的插件都已启动完毕,但是应用整体还未 ready
// 可以做一些数据初始化等操作,这些操作成功才会启动应用
// 例如:从数据库加载数据到内存缓存
}
async didReady() {
// 应用已经启动完毕
const app = this.app;
const ctx = await app.createAnonymousContext();
//多进程通讯接收 监听事件'offline'信息
// app.messenger.on('offline', user_id => {
// if (app.ws.user[user_id]) {
// app.ws.user[user_id].send(JSON.stringify({
// msg: "fail",
// data: '你的账号在其他设备登录'
// }));
// app.ws.user[user_id].close();
// }
// });
//多进程通讯接收 监听事件'send'信息
app.messenger.on('send', e => {
let { to_id, message, msg } = e;
if (app.ws.user && app.ws.user[to_id]) {
app.ws.user[to_id].send(JSON.stringify({
msg,
data: message
}));
}
});
}
async serverDidReady() {
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
}
}
module.exports = AppBootHook;