多人在线聊天系统

实现需求:

登录鉴权(jwt模块)

  • 实现登陆界面的跳转到聊天页面的同时保存通过jwt模块生成的token保存到本地浏览器中(localStorage),当我们通过更改浏览器的url再次访问聊天界面的时候能够顺利进入
  • 当我们删除浏览器本地存储(localStorage)的token时,此时在次通过修改浏览器url的方式进入是失败的,需要重新登陆生成新的token

数据库操作用户(mysql2模块)

  • 通过数据库中存储的数据来进行用户身份的第一层识别以及token的生成

群聊模式(ws模块)

  • 在聊天页面上能够实现群聊与私聊的区分,群聊即所有在线的用户都能看到,私聊则只有接收方才能看到,其他人是看不到的

私聊模式(ws模块)

  • 在聊天页面上能够实现群聊与私聊的区分,群聊即所有在线的用户都能看到,私聊则只有接收方才能看到,其他人是看不到的

用到的技术栈

  • node.js
  • express框架(快速搭建服务器)
  • axios(使用到里面的请求拦截器和响应拦截器以及ajax请求发送)
    • 请求拦截器:用于登录鉴权,当我们进入聊天界面的时候,首先会通过请求拦截器来将本地的token与登录post请求中请求头(Authorization)中的token进行比对,如果是一致就能进入页面
    • 响应拦截器: 一方面用于首次登录后在想赢回来之前将服务器生成的token存储到本地浏览器中(localStorage)中,另一方面则用于登录鉴权,当我们本地的token失效时,服务器会返回状态码500(error.response.status === 500),此时响应拦截器就会进行判断,如果token过期,就会清除掉本地的token并将页面能转回登录页
  • mysql2模块: 操作数据库
  • ws模块: 实现消息的实时收发

设计思路

登录模块

  • 登录模块的设计: 前端页面使用axios向服务端发送post请求,将用户名和密码放在请求体中,这样相较于使用get请求更为安全,并且在前端页面设置相响应拦截器使得后端生成的token能够在响应页面跳转之前,将token存储到浏览器的本地中(localStorage)
//前端页面***********************************************************************
//响应拦截器(做token的保存),(请求成功后,数据回来之前调用的方法)
axios.interceptors.response.use(function (response) {
// 存储token
const {authorization} = response.headers//将token结构出来
if(authorization){
localStorage.setItem('token',authorization)//将token存储到浏览器本地存储中
}
return response;
}, function (error) {
return Promise.reject(error);
});

// 登陆事件(点击发送post请求)
login.onclick = ()=>{
console.log(username.value,password.value)
axios.post('/login',{//设置响应体内容
username:username.value,
password:password.value,
Authorization:localStorage.getItem('token')//将token发送过去
}).then(res=>{
console.log(res.data);
// 判断返回的状态码status
if(!res.data.status){
alert('登陆失败!账号或者密码错误!!')
}else{
alert('登陆成功!')
location.href = './chat.html'
}
})
}
//****************************************************************************
  • 后端的登录接口接受到前端的ajax请求随后通过mysql21模块来向数据库查询是否存在该数据,查询存在则将数据库中的该条数据里面的用户名(username)加密成token通过res.header添加到响应头中的Authorization字段(默认规矩)返回给前端页面,等待响应拦截器拦截后保存在浏览器的localStroage中。
//服务端接口*******************************************************************
// 登录接口(获取登陆信息)
app.post('/login', async (req , res)=>{
// 查看数据库内的信息是否存在该用户
var admin = await promisePool.query(
'select * from admin where username=? and password=?',
[req.body.username,req.body.password])
// 判断数据库中是否存在该用户(数据库返回的数据的长度)
if(!admin[0].length){
res.send({
status:0,//登陆状态
msg:'该用户不存在,请重新输入账号密码'
})
}else{
console.log(admin[0][0].username);
// 登陆成功生成token
const token = jwt.sign(
{data:admin[0][0].username},//将用户名加密成token
'lam',// 加密密钥为 lam
{expiresIn: '30h'} // token生效时间为30小时
)

// 将生成好的token保存到res.header中返回给前端浏览器
//固定写为Authorization(后面校验也是用这个请求头)
res.header('Authorization' , token)

// 发送登陆状态
res.send({
status:1,//登陆状态为1
data: admin[0],//用户数据
})
}
})
//**************************************************************************
  • 用户登陆成功过后会跳转到聊天室界面,这时在这个界面设置请求拦截器响应拦截器,请求拦截器的作用是用于登录鉴权的,为了加强这一方面的知识以及应用,我在这个聊天页面一挂载的时候,就开始向服务端的/userinfo接口请求数据(当前登录[本地token存储]的用户名),这时会在请求拦截器中做鉴权操作,发请求时将本地的token通过请求头(Authorization)字段携带过去,在对应的接口当中做鉴权,后端接收到这个token进行解密,如果能够解密出来,就证明这个token没有失效,随后返回这个用户的信息即可(其实返回的就是用户名),验证失败的话就返回状态码为500即可,随后前端页面通过接收到的数据进行判断
//服务端接口******************************************************************
// 获取用户信息接口
app.get('/userinfo' , (req,res)=>{
// console.log('111'+req.headers.authorization);
// 验证token
const token = req.headers.authorization
const payload = jwt.verify(token,'lam')//解密token
console.log(payload.data);
if(payload){
console.log('确认token');
// 确认token后返回用户名(我的token中加密的就是用户名)
res.send({
username: payload.data,//用户名
status: 1//登录状态码设置为1
})
}else{
// 验证失败的话返回页面状态码500
res.status(500)
}
})
//***************************************************************************

//前端页面的拦截器*************************************************************
// 使用axios拦截器来实现token验证
// 请求拦截器(请求发出前执行的方法)
axios.interceptors.request.use(function (config) {
// 发送请求之前先验证本地的token是否过期或者合法
const token = localStorage.getItem('token')//取出token
config.headers.Authorization = `${token}`//将token通过请求拦截器发送回服务器进行验证
return config;
}, function (error) {
console.log('出错了');
return Promise.reject(error);
});

// 响应拦截器(请求成功后,数据回来之前调用的方法)
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
console.log('出错了!');
if(error.response.status === 500){//token过期返回500状态码
alert('登录过期! 请重新登录...')
localStorage.removeItem('token')//清除token
location.href = './login.html'//返回登录页面
}
return Promise.reject(error);
});
//***************************************************************************

聊天模块

  • 我们知道,ws模块提供一个wss.on('connection',function)API实现聊天模块的所有功能基本都是基于这个API来实现的。
  • 首先是获取当前来接这台wss服务器的客户端有多少,也就是要知道当前有多少个用户在线, wss.on('connection',function)这个api表示只要当前有新的客户端练到这台wss服务器上就会走里面的回调函数,并且ws模块的强大之处不仅如此,它还提供了.clients这个接口,里面存放着当前连接这台wss服务器的所有客户端的信息(也就是用户信息),因此我们可以通过遍历来将当前所有在线的人的信息发送给验证token成功登录到聊天室页面的用户,一旦有用户断开连接(token失效或者关闭服务器),再次发送当前所有在线用户信息(使用ws.on('close',function)来监听断开连接的用户)
//服务端************************************************************************
wss.on('connection', function connection(ws,req) {//监听连接事件
// 通过new url对象来获取这个url身上所携带的参数
const myURL = new URL(req.url , 'http://127.0.0.1:8080')
console.log(myURL.searchParams.get('token'));//输出token
// 校验token
try {
// 解密成功(token生效)
const payload = jwt.verify(myURL.searchParams.get('token'),'lam')
if(payload){
ws.send(createMessage(
WebSocketType.GroupChat,
'广播',
'欢迎来到聊天室!'
))
//将解密后的信息存放到ws模块当中后续方便前端页面获取用户列表和群聊私聊功能的实现
ws.user = payload
// 并且我们希望在每一个用户连接成功后得到提醒当前的在线人数(包括自己)
// 封装一个群发函数
sendAll()//用户一旦token验证成功立刻返回当前在线用户的数据
}
} catch(err) {//解密失败verify方法报错
// 捕获错误信息防止程序崩溃
console.log(`错误信息${err}`);
}

// 一但有客户端断联服务器(退出浏览器,token失效)就回走这个回调
ws.on('close', ()=>{
console.log(ws.user);
// 直接删除set里面对应的用户即可(clients是set类型数据存放的当前在线的用户信息)
wss.clients.delete(ws.user)
// 随后重新给每一个客户端发送一次在新用户列表即可
sendAll()
})
});

// 获取在线用户人数函数(一旦有新用户上线,就向所有的用户重新发送在线人数列表)
function sendAll(){
// 表示一旦有客户端连接到服务器,就会向所有的客户端发送当前的在线人数(用户列表)
wss.clients.forEach(function each(client) {
// 判断当前所有的客户端是否成功链接
if (client.readyState === WebSocket.OPEN) {
client.send(createMessage(
1,null,//1为获取在线用户列表
JSON.stringify(Array.from(wss.clients).map(item=>item.user))
))
}
});
}
//***************************************************************************
  • 群聊功能的实现: 通过ws模块中的 ws.on('message', function(data))来实现,前端页面一旦使用ws.send()向服务器发送信息的时候,这个API就会走里面的回调函数,我们将里面的data通过JSON.parse解析出来过后再通过.clients的遍历,再由wss服务器向clients里面存的每一个在线用户转发数据
//前端页面js******************************************************************
// 注册发送事件
submit.onclick = ()=>{
if(!message.value){
alert('消息不能为空!')
return
}
// 判断群发和私聊
if(onlineUser.value==='all'){//群发
// 群发,消息内容为我们输入的内容(type为2,群发)
ws.send(createMessage(2,null,`[群聊]${message.value}`))
}else{
// 私聊,消息内容为我们输入的内容(type为3,私聊),将选择框里面的在线用户名传过去
ws.send(createMessage(3,null,`[私聊]${message.value}`,onlineUser.value))
}
message.value = ''//最后清空输入框
}

// 创建消息的发送函数(什么形式(群,私聊),谁发送的,发了什么,给谁发送)
function createMessage(type,user,data,to){
// 因为ws.send方法只能发送字符串形式的值,因此要用stringify来转字符串
return JSON.stringify({
type,//什么形式,群聊还是私聊
user,//谁发的
data,//发的内容是什么
to//给谁发(私聊专属)
})
}
//****************************************************************************

//后端服务器********************************************************************
// 简易的群聊功能(clients里面存放着当前所有链接这台websocket服务器的客户端)
// 对所有链接这台websocket服务器的客户端进行一个遍历
wss.clients.forEach(function each(client) {
// 判断当前所有的客户端是否成功链接
if (client.readyState === WebSocket.OPEN) {
/*
将每一个客户端发过来的信息进行二进制的转换并重新转发回给每一个客户端
也就是说: 每一个客户端发的消息所有的客户端都能看见(群聊功能)
*/
client.send(createMessage(
2,//群发
ws.user.data,//谁发的(用户名在连接服务器的时候存在了ws.user中)
msgObj.data//前端接收过来的信息
), { binary: false });//第二个参数为判断是否为二进制
}
});
//****************************************************************************
  • 私聊与群聊基本上是非常的相似不过私聊在发送信息的同时,将想要发送的对象一并传了过去,随后在服务端中与.clients里面存的在向用户进行对比,对比成立则证明该用户是在线的,即可进行私聊
//前端页面js******************************************************************
// 注册发送事件
submit.onclick = ()=>{
if(!message.value){
alert('消息不能为空!')
return
}
// 判断群发和私聊
if(onlineUser.value==='all'){//群发
// 群发,消息内容为我们输入的内容(type为2,群发)
ws.send(createMessage(2,null,`[群聊]${message.value}`))
}else{
// 私聊,消息内容为我们输入的内容(type为3,私聊),将选择框里面的在线用户名传过去
ws.send(createMessage(3,null,`[私聊]${message.value}`,onlineUser.value))
}
message.value = ''//最后清空输入框
}

// 创建消息的发送函数(什么形式(群,私聊),谁发送的,发了什么,给谁发送)
function createMessage(type,user,data,to){
// 因为ws.send方法只能发送字符串形式的值,因此要用stringify来转字符串
return JSON.stringify({
type,//什么形式,群聊还是私聊
user,//谁发的
data,//发的内容是什么
to//给谁发(私聊专属)
})
}
//****************************************************************************

//后端服务器*******************************************************************
// 简易的私聊功能(clients里面存放着当前所有链接这台websocket服务器的客户端)
// 对所有链接这台websocket服务器的客户端进行一个遍历
wss.clients.forEach(function each(client) {
// 判断前端传过来的私聊对象是否在线且存在
if (client.user.data === msgObj.to &&
client.readyState === WebSocket.OPEN) {
/*
将每一个客户端发过来的信息进行二进制的转换并重新转发回给每一个客户端
也就是说: 每一个客户端发的消息所有的客户端都能看见(群聊功能)
*/
client.send(createMessage(//向目标客户端发送消息
3,//私聊
ws.user.data,//谁发的(用户名在连接服务器的时候存在了ws.user中)
msgObj.data//前端接收过来的信息
), { binary: false });//第二个参数为判断是否为二进制
}
});
//****************************************************************************

完整的代码展示

  • 前端登录页面(login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页</title>
<!-- 引入axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
</head>
<script>
// 响应拦截器(请求成功后,数据回来之前调用的方法)
axios.interceptors.response.use(function (response) {
// 存储token
const {authorization} = response.headers//将token解构出来
if(authorization){
localStorage.setItem('token',authorization)//将token存储到浏览器本地存储中
}
return response;
}, function (error) {
return Promise.reject(error);
});
</script>
<body>
<div>
账号:<input type="text" placeholder="请输入账号" id="username"><br>
密码:<input type="password" placeholder="请输入密码" id="password">
</div><br>
<button id="login">登录</button>

<script>
// 获取输出的参数
var username = document.querySelector('#username')//获取输入账号dom
var password = document.querySelector('#password')//获取输入密码dom
var login = document.querySelector('#login')//获取登录按钮dom

// 登陆事件(点击发送post请求)
login.onclick = ()=>{
axios.post('/login',{//设置响应体内容
username:username.value,//输入的用户名
password:password.value,//输入的密码
Authorization:localStorage.getItem('token')//将token发送过去
}).then(res=>{
// 判断返回的状态码status
if(!res.data.status){//返回为0的话
alert('登陆失败!账号或者密码错误!!')
}else{
alert('登陆成功!')
location.href = './chat.html'//跳转到聊天室
}
})
}


</script>
</body>
</html>
  • 前端聊天室页面(chat.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天室(客户端)</title>
<!-- 引入axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script>
// 使用axios拦截器来实现token验证
// 请求拦截器(请求发出前执行的方法)
axios.interceptors.request.use(function (config) {
// 发送请求之前先验证本地的token是否过期或者合法
const token = localStorage.getItem('token')//取出token
config.headers.Authorization = `${token}`//将token通过请求拦截器发送回服务器进行验证
return config;
}, function (error) {
console.log('出错了');
return Promise.reject(error);
});

// 响应拦截器(请求成功后,数据回来之前调用的方法)
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
console.log('出错了!');
if(error.response.status === 500){//token过期返回500状态码
alert('登录过期! 请重新登录...')
localStorage.removeItem('token')//清除token
location.href = './login.html'//返回登录页面
}
return Promise.reject(error);
});
</script>
</head>
<style>
.chatArea{
width: 800px;
height: 500px;
background-color: aliceblue;
border: 1px solid black;
}
</style>
<body>
<h1>聊天室(客户端)</h1>
<div class="chatArea">
<ul id="chatArea">

</ul>
</div>
<!-- 发送按钮 -->
<button id="submit">发送</button>
<!-- 信息输入框 -->
<input type="text" id="message">
<!-- 在线用户列表 -->
<select id="select"></select>
<!-- 退出登录 -->
<button id="logout" style="float: right;">退出聊天室</button>

<script>
// 创建授权类型
const WebSocketType = {
Error:0,//出错了,没授权
GroupList:1,//获取在线用户列表
GroupChat:2,//群聊
SingleChat:3//私聊
}

var User = ''//创建一个用户名,用于接收后端的用户名
// 页面一挂载就获取用户信息(查看token)
axios.get('/userinfo').then(res=>{
if(!res.data.status){
alert(res.data.msg)
location.href = './login.html'//跳转回登录页
}
alert(`欢迎! "${res.data.username}" 进入聊天室`)
User = `${res.data.username}`
})

// 创建websocket连接器并指定链接端口(固定形式)并将本地的token带过去给服务器验证
var ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem('token')}`)

// ws模块提供的4个回调函数
// 1. onclose(服务器宕机)
ws.onclose = () =>{
console.log('服务器宕机了!');
}
// 2. onerror(客户端与服务器连接失败)
ws.onerror = ()=>{
console.log('连接失败!');
}
// 3. onmessage(每一次服务器返回消息)
ws.onmessage = (msgObj)=>{
msgObj = JSON.parse(msgObj.data)//将发过来的消息字符串转化为消息对象
// 做状态判断
switch(msgObj.type){
case WebSocketType.Error://token失效了(0)
localStorage.removeItem('token')//移除本地token
location.href = './login.html'//跳转回login页面
break;
case WebSocketType.GroupList://获取当前在线用户列表(1)
// 对获取到的在线用户列表进行json解析(因为服务端发过来的是json.stringfy加工的)
console.log(JSON.parse(msgObj.data));
const onlineList = JSON.parse(msgObj.data)//获取当前在线的用户列表
onlineUser.innerHTML = ''//每次获取先清空列表,防止重复压入
onlineUser.innerHTML = `<option value='all'>全部群聊</option>`+onlineList.map(item=>`
<option value='${item.data}'>${item.data}</option>
`).join('')
//将获取到的在新用户名填入到选择框中(因为我的token加密只是将用户名加密,因此这里的data就是用户名)
break;
case WebSocketType.GroupChat://群聊功能(2)
// 每一次服务器返回消息就做li标签的添加
// console.log(`${msgObj.user}说: ${msgObj.data}`);
var Othernew = document.createElement('li')//创建dom节点
Othernew.innerText = `[群聊]${msgObj.user}说: ${msgObj.data}`//将获取回来的消息赋值给li标签
document.querySelector('#chatArea').appendChild(Othernew)//将li标签添加到聊天区域中
break;
case WebSocketType.SingleChat://私聊功能(3)
// console.log(`${msgObj.user}说: ${msgObj.data}`);
// 每一次服务器返回消息就做li标签的添加
var Othernew = document.createElement('li')//创建dom节点
Othernew.innerText = `[私聊]${msgObj.user}说: ${msgObj.data}`//将获取回来的消息赋值给li标签
document.querySelector('#chatArea').appendChild(Othernew)//将li标签添加到聊天区域中
break;

}
}
// 4. onopen(客户端与服务器连接成功)
ws.onopen = ()=>{
console.log('连接成功!');
}

// 获取dom元素
var message = document.querySelector('#message')//消息的输入
var submit = document.querySelector('#submit')//提交按钮
var logout = document.querySelector('#logout')//退出登录
var onlineUser = document.querySelector('#select')//用户列表

// 注册发送事件
submit.onclick = ()=>{
if(!message.value){
alert('消息不能为空!')
return
}
// 判断群发和私聊
if(onlineUser.value==='all'){//群发
// 群发,消息内容为我们输入的内容(type为2,群发)
ws.send(createMessage(2,null,`${message.value}`))
}else{
// 私聊,消息内容为我们输入的内容(type为3,私聊),将选择框里面的在线用户名传过去
ws.send(createMessage(3,null,`${message.value}`,onlineUser.value))
}
message.value = ''//最后清空输入框
}

// 点击发送退出登录请求
logout.onclick = ()=>{
// 清除本地的token
localStorage.removeItem('token')
location.href = './login.html'//退回登录面
}

// 创建消息的发送函数(什么形式(群,私聊),谁发送的,发了什么,给谁发送)
function createMessage(type,user,data,to){
// 因为ws.send方法只能发送字符串形式的值,因此要用stringify来转字符串
return JSON.stringify({
type,//什么形式,群聊还是私聊
user,//谁发的
data,//发的内容是什么
to//给谁发(私聊专属)
})
}
</script>
</body>
</html>
  • 后端服务器(app.js)
// 聊天室(带登录验证:jwt,数据库)
/*
用到的模块: express jwt ws(websocket)
实现功能:用户连接数据库 使用 jwt 来实现token的登录鉴权
ws(websocket)用来实现实时聊天
*/
// 首先引入所需的模块
const express = require('express')//express框架
const mysql = require('mysql2')//mysql数据库操作模块
const jwt = require('jsonwebtoken')//jwt(token加密模块)
const WebSocket = require('ws');// 引入ws模块中的 WebSocket

// 创建服务器
const app = express();

// 配置解析post参数的两个内置中间件
// 通过express.json()这个中间件,解析表单中的JSON格式的数据
app.use(express.json())//解析post的请求题参数(json格式)
// 通过express.urlencoded()这个中间件,来解析表单中的url-encoded格式的数据
app.use(express.urlencoded({extended:false}))//解析post的请求题参数(encoded格式)

//使用express.static()中间件来托管静态资源
app.use(express.static('./public'))

// 1.创建连接池,进行操作
const config = getConfig()//2.创建数据库连接对象
// 3.创建数据库连接池(promise形式调数据)
const promisePool = mysql.createPool(config).promise()

// 登录接口(获取登陆信息)
app.post('/login', async (req , res)=>{
// 查看数据库内的信息是否存在该用户
var admin = await promisePool.query(
'select * from admin where username=? and password=?',
[req.body.username,req.body.password])
// 判断数据库中是否存在该用户(数据库返回的数据的长度)
if(!admin[0].length){
res.send({
status:0,//登陆状态
msg:'该用户不存在,请重新输入账号密码'
})
}else{
// 登陆成功生成token
const token = jwt.sign(
{data:admin[0][0].username},//将用户名加密成token
'lam',// 加密密钥为 lam
{expiresIn: '30h'} // token生效时间为30小时
)

// 将生成好的token保存到res.header中返回给前端浏览器
res.header('Authorization' , token)//固定写为Authorization(后面校验也是用这个请求头)

// 发送登陆状态
res.send({
status:1,//登陆状态为1
data: admin[0],//用户数据
})
}
})

// 获取用户信息接口
app.get('/userinfo' , (req,res)=>{
// console.log('111'+req.headers.authorization);
// 验证token
const token = req.headers.authorization
const payload = jwt.verify(token,'lam')//解密token
console.log(payload.data);
if(payload){//确认token
// 确认token后返回用户名(我的token中加密的就是用户名)
res.send({
username: payload.data,//用户名
status: 1//登录状态码设置为1
})
}else{
// 验证失败的话返回页面状态码500
res.status(500)
}
})

// 启动服务器
app.listen(3000,()=>{
console.log('服务器已启动,3000端口正在监听....');
})

// 创建链接数据库函数
function getConfig(){
return {
host:'127.0.0.1',//域名
port: 3306,//端口号(mysql默认是3306)
user: 'root',//数据库的用户名
password: 'Zpl13189417387',//数据库的密码
database:'jwt',//要连接的数据库名称
connectionLimit:1//创建连接池的数量
}
}

//创建WebSocket的服务器(也就是WebSocketServer)
const WebSocketServer = WebSocket.WebSocketServer

// 创建 WebSocketServer (websocket服务器)
const wss = new WebSocketServer({ port: 8080 });

// 设置websocket服务器监听
//(wss表示websocket服务器 , ws表示当前每一个链接这台服务器的客户端,req连着这台服务器的客户端的请求对象)
wss.on('connection', function connection(ws,req) {//监听连接事件
// 通过new url对象来获取这个url身上所携带的参数
const myURL = new URL(req.url , 'http://127.0.0.1:8080')
console.log(myURL.searchParams.get('token'));//输出token
// 校验token
try {
// 解密成功(token生效)
const payload = jwt.verify(myURL.searchParams.get('token'),'lam')
if(payload){
ws.send(createMessage(
WebSocketType.GroupChat,
'广播',
'欢迎来到聊天室!'
))
//将解密后的信息存放到ws模块当中后续方便前端页面获取用户列表和群聊私聊功能的实现
ws.user = payload
// 并且我们希望在每一个用户连接成功后得到提醒当前的在线人数(包括自己)
// 封装一个群发函数
sendAll()//用户一旦token验证成功立刻返回当前在线用户的数据
}
} catch(err) {//解密失败verify方法报错
// 捕获错误信息防止程序崩溃
console.log(`错误信息${err}`);
}
ws.on('message', function message(data) {//客户端监听信息事件
// console.log('收到了来自客户端的消息:', data.toString());//将接受过来的消息转化为字符串信息
// 对获取到的前端页面传过来的信息进行对json对象的解密
const msgObj = JSON.parse(data)
switch (msgObj.type) {
case 1://获取在线用户列表
// 将接收到的用户列表发送回给前端
ws.send(createMessage(
1,null,//发送1表示获取当前所有在线的用户信息,null当前没人发送,服务器自动发送
JSON.stringify(Array.from(wss.clients).map(item=>item.user))
))
break;
case 2://群聊
// 简易的群聊功能(clients里面存放着当前所有链接这台websocket服务器的客户端)
// 对所有链接这台websocket服务器的客户端进行一个遍历
wss.clients.forEach(function each(client) {
// 判断当前所有的客户端是否成功链接
if (client.readyState === WebSocket.OPEN) {
/*
将每一个客户端发过来的信息进行二进制的转换并重新转发回给每一个客户端
也就是说: 每一个客户端发的消息所有的客户端都能看见(群聊功能)
*/
client.send(createMessage(
2,//群发
ws.user.data,//谁发的(用户名在连接服务器的时候存在了ws.user中)
msgObj.data//前端接收过来的信息
), { binary: false });//第二个参数为判断是否为二进制
}
});
break;
case 3://私聊
// 简易的私聊功能(clients里面存放着当前所有链接这台websocket服务器的客户端)
// 对所有链接这台websocket服务器的客户端进行一个遍历
wss.clients.forEach(function each(client) {
// 判断前端传过来的私聊对象是否在线且存在
if (client.user.data === msgObj.to && //切记是两个条件都成立
client.readyState === WebSocket.OPEN) {
/*
将每一个客户端发过来的信息进行二进制的转换并重新转发回给每一个客户端
也就是说: 每一个客户端发的消息所有的客户端都能看见(群聊功能)
*/
client.send(createMessage(//向目标客户端发送消息
3,//私聊
ws.user.data,//谁发的(用户名在连接服务器的时候存在了ws.user中)
msgObj.data//前端接收过来的信息
), { binary: false });//第二个参数为判断是否为二进制
}
});
break;
}
});

// 一但又客户端断联服务器(退出浏览器,token失效)就回走这个回调
ws.on('close', ()=>{
// 直接删除set里面对应的用户即可
wss.clients.delete(ws.user)
// 随后重新给每一个客户端发送一次在新用户列表即可
sendAll()
})
});

// 创建授权类型
const WebSocketType = {
Error:0,//出错了,没授权
GroupList:1,//获取在线用户列表
GroupChat:2,//群聊
SingleChat:3//私聊
}

// 创建消息的发送信息(有没有授权,谁发送的,发了什么)
function createMessage(type,user,data){
// 因为ws.send方法只能发送字符串形式的值,因此要用stringify来转字符串
return JSON.stringify({
type,//需求类型(获取在线用户信息,群聊还是私聊)
user,//谁发的消息
data//发的什么消息
})
}

// 群发函数(一旦有新用户上线,就向所有的用户重新发送在线人数列表)
function sendAll(){
// 表示一旦有客户端连接到服务器,就会向所有的客户端发送当前的在线人数(用户列表)
wss.clients.forEach(function each(client) {
// 判断当前所有的客户端是否成功链接
if (client.readyState === WebSocket.OPEN) {
client.send(createMessage(
1,null,//1为获取在线用户列表
JSON.stringify(Array.from(wss.clients).map(item=>item.user))
))
}
});
}

结果展示:

登陆页面的跳转

image

清除本地token的登录鉴权验证

image

群聊和私聊功能的实现

image

数据库表图

image