廢話不多說直接上效果圖
技術(shù)棧
前端:vuejs,vue-socket.io,better-scroll
后端:egg,egg-socket.io
數(shù)據(jù)庫:redis
實(shí)現(xiàn)流程
socket的連接
1.vuex中定義socket模塊芯砸,并且定義socket默認(rèn)事件
const state = {
socketState: false,//連接狀態(tài)
chat_list: getChatList(),//聊天記錄列表
}
const getters = {
//消息未讀總數(shù)
unread_num(state) {
let count = 0;
for (var i = 0; i < state.chat_list.length; i++) {
count += state.chat_list[i].unread_num || 0;
}
return count;
}
}
//socket默認(rèn)事件
const mutations = {
socket_connect(state) {
console.log("連接成功");
state.socketState = true;
},
socket_reconnect(state, data) {
console.log("重新連接" + data);
},
socket_reconnecting(state, data) {
console.log("重新連接中" + data);
Toast('重新連接中')
},
socket_disconnect(state) {
console.log("斷開連接");
state.socketState = false;
},
}
2.客戶端發(fā)起socket連接(初始化socket)
if (!store.state.socket.socketState) {
Vue.use(
new VueSocketIO({
debug: true,
connection:
ip + "?token=" + window.localStorage.getItem("token"),
vuex: {
store,
actionPrefix: "socket_",
mutationPrefix: "socket_"
}
})
);
}
3.服務(wù)端響應(yīng)socket連接
const { app, socket } = ctx;
const token = ctx.request.query.token;
const id = socket.id;
let username = '';
try {
username = (await ctx.app.jwt.verify(token, ctx.app.config.jwt.secret)).username;
let data = { id, username };
//判斷用戶是否在線 如果在線則強(qiáng)制退出
if (await app.redis.exists(username)) {
let receive = await app.redis.get(username);
receive = JSON.parse(receive);
console.log('已經(jīng)有人在線');
ctx.socket.to(receive.id).emit('client_logout');
}
//把在線信息存入redis中
await app.redis.set(username, JSON.stringify(data));
} catch (error) {
//驗(yàn)證失敗直接拒絕socket連接
console.log(error)
socket.emit('connect_deny');
socket.disconnect();
return;
}
其中,const { app, socket } = ctx;
中的socket對(duì)象给梅,是每一個(gè)客戶端連接都會(huì)生成的假丧。對(duì)象里面有socketid,這個(gè)id是每一個(gè)客戶端的唯一標(biāo)識(shí)符(私聊推送需要用到)动羽;<strong>我們可以想象成包帚,客戶端認(rèn)識(shí)用戶的賬號(hào)(username),服務(wù)端認(rèn)識(shí)socketid曹质,因此我們可以把這兩個(gè)標(biāo)識(shí)捆綁在一起婴噩,并且以u(píng)sername為key,value為{ username:'xxxx',socketid:'xxxx' }
保存于redis中羽德。</strong>私聊推送的時(shí)候,前端知道推送的目標(biāo)用戶username耸成,后端redis也會(huì)緩存著每一個(gè)登錄用戶的信息跑筝,如此我們就可以通過username給指定用戶推送消息姻报。
實(shí)現(xiàn)邏輯大致為:服務(wù)端驗(yàn)證客戶端socket客戶端的合法性,通過驗(yàn)證的連接會(huì)去redis緩存中讀取key為username的記錄姨夹,如果記錄存在則觸發(fā)斷開socket事件。(單一登錄功能)
消息推送
客戶端推送
send (data, type) {
this.$socket.emit("chat", data, () => {
//消息推送成功回調(diào)函數(shù)
this.chat_item.chat_list.push(data);
//調(diào)用better事件矾策,讓聊天窗口拉到最底部
this.$nextTick(() => {
this.$refs.wrapper.refresh();
this.$refs.wrapper.scrollToEnd();
});
});
},
由于前端把socket掛載到了vuex中磷账,因此可以通過this.$socket.emit("chat", data, cb)
推送消息。其中chat為事件類型贾虽,與服務(wù)端中定義的socket路由對(duì)應(yīng);data為推送的數(shù)據(jù)逃糟,應(yīng)包括目標(biāo)用戶的id;cb為推送成功的回調(diào)函數(shù)蓬豁。<strong>推送成功之后绰咽,this.nextTick中調(diào)用better事件,讓聊天窗口拉到最底部地粪。(better滾動(dòng)條拉到最底是根據(jù)選擇器實(shí)現(xiàn)的取募,而選擇器是依賴于dom元素的,而vuedom更新是異步的蟆技,因此需要在this.$nextTick后再調(diào)用)</strong>
服務(wù)端定義路由
io.route('chat', app.io.controller.chat.index);//接收客戶端emit('chat')事件
服務(wù)端接收和推送
const { ctx } = this
//讀取用戶推送的消息
const message = this.ctx.args[0]
const cb = this.ctx.args[1]
//判斷目標(biāo)用戶是否在線
if (await app.redis.exists(message.receive_id)) {
let receive = await app.redis.get(message.receive_id)
receive = JSON.parse(receive)
//向目標(biāo)用戶發(fā)送消息
ctx.socket.to(receive.id).emit('client_receive_msg', message)
} else {
console.log('不在線哦')
//以message_+username為key維護(hù)一個(gè)隊(duì)列玩敏,隊(duì)列記錄著關(guān)于用戶的離線信息
//插入數(shù)據(jù)庫
await app.redis.lpush(
'message_' + message.receive_id,
JSON.stringify(message)
)
}
cb && cb('推送成功啦')
服務(wù)端在chat.js中的index方法中拿到推送的消息斗忌,使用ctx.socket.to(receive.id)
推送給目標(biāo)用戶;根據(jù)username去redis中尋找目標(biāo)用戶的socketid(<strong>如果不存在key為username的記錄代表目標(biāo)用戶不在線,把離線信息進(jìn)行緩存起來等目標(biāo)用戶上線統(tǒng)一推送</strong>)
客戶端接收
socket_client_receive_msg(state, data) {
//判斷當(dāng)前用戶所在的頁面是否是當(dāng)前聊天用戶的頁面 如果是則未讀信息不加1
let flag = false;
if (router.currentRoute.name == "SoloChat" && router.currentRoute.query.username == data.send_info.username) { //接受過來的信息時(shí)刻 用戶正在此接收人的聊天窗內(nèi)
flag = true;
}
for (var i = 0; i < state.chat_list.length; i++) {
if (state.chat_list[i].username == data.send_info.username) { //如果已經(jīng)存在聊天對(duì)話框
if (!flag) {
state.chat_list[i].unread_num++;
}
state.chat_list[i].chat_list.push(data);
//置頂
state.chat_list.unshift(state.chat_list.splice(i, 1)[0]);
}
}
},
如上實(shí)現(xiàn)最簡(jiǎn)單版本的即時(shí)通訊旺聚,最后來個(gè)egg的目錄結(jié)構(gòu):