egg-websocket连接

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光浅止

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值