多人在线聊天系统(io模块实现)

实现需求:

登录鉴权(jwt模块)

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

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

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

群聊模式(io模块)

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

私聊模式(io模块)

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

用到的技术栈

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

设计思路

登录模块

  • 登录模块的设计: 前端页面使用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);
});
//***************************************************************************

聊天模块

  • 我们都知道io模块ws模块是有很大的不同的,其中一个就是它非常的自由,它可以自定义消息事件,而ws模块则是固定消息事件,如我们上一个demo用到的 ws.on('message', function(data)) , ws.on(‘close’,function(data)) , 等….. 但是io模块不同,我们可以通过socket.emit('消息事件','消息')来发送消息,通过socket.on('消息事件',(msg)=>{})来接收和处理对应的消息,这就是io模块自由之处,这就意味着,我们不再需要用switch分支来判断是群发,私聊,还是获取在线的用户列表 , 直接在前端使用socket.emit('群聊,私聊还是获取用户列表','数据'),服务端则通过socket.on('群聊,私聊还是获取用户的数据列表',回调函数{群聊全部转发;私聊单独转发;列表返回数据}即可, 非常的方便

  • 首先是获取当前来接这台io服务器的客户端有多少,也就是要知道当前有多少个用户在线, io.on('connection',function)这个api表示只要当前有新的客户端练到这台wss服务器上就会走里面的回调函数,并且io模块提供了io.sockets.sockets这个接口,里面以map的数据结构存放着当前连接这台io服务器的所有客户端的信息(也就是用户信息),因此我们可以将其转变成数组(map转化成数组是二维数组)后再通过遍历里面的每一项来将当前所有在线的人的信息发送给验证token成功登录到聊天室页面的用户,一旦有用户断开连接(token失效或者关闭服务器),再次发送当前所有在线用户信息(使用socket.on('disconnect',function)来监听断开连接的用户)

//服务端**********************************************************************
io.on('connection', (socket,req) => { //io模块固定的消息事件之一,用户加入
// 相较于ws模块,socket.io模块中接收前端传过来的参数并不是存放在req里面的了
// 而是存在socket里面
console.log('连接成功!!',socket.handshake.query.token);

//做权限验证(将token解密)
try {
const payload = jwt.verify(socket.handshake.query.token,'lam')
// 鉴权成功后第一件事就是发送欢迎词以及获取当前在线的用户
if(payload){//鉴权成功(token能够成功解密出来)
//给socket绑定解密的用户名(用于后续显示消息的发送者是谁)
socket.user = payload.data
//发送欢迎词(这与vue的事件总线非常的相似(前端用on来监听msg事件即可))
socket.emit(WebSocketType.GroupChat,createMessage('',`欢迎 "${socket.user}" 来到聊天室!!`))
//更新在线用户列表
sendAll(io)
}
} catch (err) {//解密失败verify方法报错
// 捕获错误信息防止程序崩溃
console.log(`错误信息${err}`);
}

//监听获取用户列表
socket.on(WebSocketType.GroupList,()=>{
//这就是ws模块中的.clients,用于存放所有连接到这台服务器的客户端
// console.log(io.sockets.sockets);
//里面接收的是一个map结构数据,ws模块中是一个set结构数据
// 将这个map数据转化为数组方便我们后面取值
console.log(Array.from(io.sockets.sockets).map(item=>item[1].user));
})

//监听用户断联
socket.on('disconnect',()=>{//io模块的固定消息时间之一:用户退出
//一旦有用户断联服务器重新发送当前在线的用户列表
sendAll(io)
})
});//设置监听事件

// 群发函数(一旦有新用户上线,就向所有的用户重新发送在线人数列表)
function sendAll(io){
// 表示一旦有客户端连接到服务器,就会向所有的客户端发送当前的在线人数(用户列表)
io.sockets.emit(WebSocketType.GroupList,createMessage(//io模块提供一个.sockets方法可以向所有的客户端进行操作
'',//不将消息的发送者传过去
Array.from(io.sockets.sockets).map(item=>item[1].user)//将所有的在线用户发送过去
))
}
//*****************************************************************************
  • 群聊功能的实现: 通过io模块中的 socket.on('群聊事件', function(data))来实现,前端页面一旦使用socket.emit('群聊事件','数据(发送的消息,谁发送的)')向服务器发送信息的时候,这个API就会走里面的回调函数,我们将里面的data解析出来过后再通过io.sockets.emit('群聊','发送的消息'),给当前连接到这个socket服务器上的说有用户端发送消息
  • 注意:io.socketsio模块内置的用于操作所有当前连接到这台socket服务器上的所用客户端的API
//前端页面**********************************************************************
// 注册发送事件
submit.onclick = ()=>{
if(!message.value){
alert('消息不能为空!')
return
}
// 判断群发和私聊
if(onlineUser.value==='all'){//群发
// 群发,消息内容为我们输入的内容(群发)
socket.emit(WebSocketType.GroupChat,createMessage(`${message.value}`))//io模块
}else{
// 私聊,消息内容为我们输入的内容(私聊),将选择框里面的在线用户名传过去
socket.emit(WebSocketType.SingleChat,createMessage(`${message.value}`,select.value))//io模块
}
message.value = ''//最后清空输入框
}

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

//后端服务器*********************************************************************
//监听群聊功能(接收客户端发送过来的消息在做全体的转发)
socket.on(WebSocketType.GroupChat,(msg)=>{
// console.log(msg);
io.sockets.emit(WebSocketType.GroupChat,//设置为群聊模式
createMessage(//io模块提供一个.sockets方法可以向所有的客户端进行操作
socket.user,//发送者
msg.data//发送的消息
))
})

// 创建消息的发送信息(有没有授权,谁发送的,发了什么)
function createMessage(user,data){
// emit方法可以直接传对象,ws模块不能
return {
user,//谁发的消息
data//发的什么消息
}
}
//******************************************************************************

  • 私聊与群聊基本上是非常的相似不过私聊在发送信息的同时,将想要发送的对象一并传了过去,随后在服务端中与io.sockets.sockets里面存的在向用户进行对比,对比成立则证明该用户是在线的,即可进行私聊
//前端页面***********************************************************************
// 2. 私聊功能
socket.on(WebSocketType.SingleChat,(msgObj)=>{
// 每一次服务器返回消息就做li标签的添加
// console.log(msgObj);
var Othernew = document.createElement('li')//创建dom节点
Othernew.innerText = `[私聊]${msgObj.user}说: ${msgObj.data}`//将获取回来的消息赋值给li标签
document.querySelector('#chatArea').appendChild(Othernew)//将li标签添加到聊天区域中
})

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

//后端服务器*********************************************************************
//监听私聊功能
socket.on(WebSocketType.SingleChat,(msg)=>{
// console.log(msg);
//将获取到的信息数据做一个遍历
Array.from(io.sockets.sockets).forEach(item=>{//对当前在线的用户做一个遍历
if(item[1].user === msg.to){//如果存在前端传过来的私聊对象
//就将消息指定发送给该用户
// console.log(item[1]);//item[1]就是指定接收信息的客户端
item[1].emit(
WebSocketType.SingleChat,//设置为私聊模式
createMessage(
socket.user,//消息的发送者,本地的token解析出来的信息
msg.data//发送的消息
)
)
}
})
})

// 创建消息的发送信息(有没有授权,谁发送的,发了什么)
function createMessage(user,data){
// emit方法可以直接传对象,ws模块不能
return {
user,//谁发的消息
data//发的什么消息
}
}
//******************************************************************************

完整的代码展示

  • 前端登陆页面(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('登陆成功!')
//将用户名存到本地存储中方便后面展示当前的用户是谁
localStorage.username = username.value
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>
<!-- 引入socket.io.min.js -->
<!-- <script type="text/javascript" src="../node_modules/socket.io/client-dist/socket.io.js"></script> -->
<!-- 或者使用官方文档推荐的引入方式 -->
<script src="./js/socket.io.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>
<h3>当前用户:
<b id="user"></b>
</h3>
<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}`
})

// 获取dom元素
var message = document.querySelector('#message')//消息的输入
var submit = document.querySelector('#submit')//提交按钮
var logout = document.querySelector('#logout')//退出登录
var user = document.querySelector('#user')//当前用户是谁
var onlineUser = document.querySelector('#select')//用户列表
user.innerHTML = localStorage.getItem('username')//展示当前用户

// 使用socket.io模块(接受前端)
const socket = io(`ws://localhost:3000?token=${localStorage.getItem('token')}`);

// 1. 群聊功能以及新用户上线发送欢迎词
socket.on(WebSocketType.GroupChat,(msgObj)=>{
// 每一次服务器返回消息就做li标签的添加
console.log('连接成功');
var Othernew = document.createElement('li')//创建dom节点
Othernew.innerText = `[群聊]${msgObj.user}说: ${msgObj.data}`//将获取回来的消息赋值给li标签
document.querySelector('#chatArea').appendChild(Othernew)//将li标签添加到聊天区域中
})
// 2. 私聊功能
socket.on(WebSocketType.SingleChat,(msgObj)=>{
// 每一次服务器返回消息就做li标签的添加
// console.log(msgObj);
var Othernew = document.createElement('li')//创建dom节点
Othernew.innerText = `[私聊]${msgObj.user}说: ${msgObj.data}`//将获取回来的消息赋值给li标签
document.querySelector('#chatArea').appendChild(Othernew)//将li标签添加到聊天区域中
})
// 3. 新用户上线就获取用户列表
socket.on(WebSocketType.GroupList,(msgObj)=>{
// console.log(msgObj);
const onlineList = msgObj.data//是一个数组,里面存放着当之前所有的在线用户
onlineUser.innerHTML = ''//每次获取先清空列表,防止重复压入
onlineUser.innerHTML = `<option value='all'>全部群聊</option>`+onlineList.map(item=>`
<option value='${item}'>${item}</option>
`).join('')
//将获取到的在新用户名填入到选择框中(因为我的token加密只是将用户名加密,因此这里的data就是用户名)
})

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

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

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

const mysql = require('mysql2')//mysql数据库操作模块
const jwt = require('jsonwebtoken')//jwt(token加密模块)
const server = require('http').createServer(app);//创建socket服务器并与http服务器绑定在一起
const io = require('socket.io')(server);//创建socket服务器

io.on('connection', (socket,req) => { //io模块固定的消息事件之一,用户加入
// 相较于ws模块,socket.io模块中接收前端传过来的参数并不是存放在req里面的了
// 而是存在socket里面
console.log('连接成功!!',socket.handshake.query.token);

//做权限验证(将token解密)
try {
const payload = jwt.verify(socket.handshake.query.token,'lam')
// 鉴权成功后第一件事就是发送欢迎词以及获取当前在线的用户
if(payload){//鉴权成功(token能够成功解密出来)
//给socket绑定解密的用户名(用于后续显示消息的发送者是谁)
socket.user = payload.data
//发送欢迎词(这与vue的事件总线非常的相似(前端用on来监听msg事件即可))
socket.emit(
WebSocketType.GroupChat,//群聊
createMessage('',`欢迎 "${socket.user}" 来到聊天室!!`)//发送的信息
)
//更新在线用户列表
sendAll(io)
}
} catch (err) {//解密失败verify方法报错
// 捕获错误信息防止程序崩溃
console.log(`错误信息${err}`);
}

//监听获取用户列表
socket.on(WebSocketType.GroupList,()=>{
//这就是ws模块中的.clients,用于存放所有连接到这台服务器的客户端
// console.log(io.sockets.sockets);//里面接收的是一个map结构数据,ws模块中是一个set结构数据
// 将这个map数据转化为数组方便我们后面取值
console.log(Array.from(io.sockets.sockets).map(item=>item[1].user));
})

//监听群聊功能(接收客户端发送过来的消息在做全体的转发)
socket.on(WebSocketType.GroupChat,(msg)=>{
// console.log(msg);
io.sockets.emit(WebSocketType.GroupChat,//设置为群聊模式
createMessage(//io模块提供一个.sockets方法可以向所有的客户端进行操作
socket.user,//发送者
msg.data//发送的消息
))
})

//监听私聊功能
socket.on(WebSocketType.SingleChat,(msg)=>{
// console.log(msg);
//将获取到的信息数据做一个遍历
Array.from(io.sockets.sockets).forEach(item=>{//对当前在线的用户做一个遍历
if(item[1].user === msg.to){//如果存在前端传过来的私聊对象
//就将消息指定发送给该用户
// console.log(item[1]);//item[1]就是指定接收信息的客户端
item[1].emit(
WebSocketType.SingleChat,//设置为私聊模式
createMessage(
socket.user,//消息的发送者,本地的token解析出来的信息
msg.data//发送的消息
)
)
}
})
})

//监听用户断联
socket.on('disconnect',()=>{//io模块的固定消息时间之一:用户退出
//一旦有用户断联服务器重新发送当前在线的用户列表
sendAll(io)
})
});//设置监听事件

// 配置解析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)
}
})

// 启动服务器(这是http服务器与io模块开启的websocket服务器绑定在一起后生成的新的服务器)
server.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//创建连接池的数量
}
}

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

// 创建消息的发送信息(有没有授权,谁发送的,发了什么)
function createMessage(user,data){
// emit方法可以直接传对象,ws模块不能
return {
user,//谁发的消息
data//发的什么消息
}
}

// 群发函数(一旦有新用户上线,就向所有的用户重新发送在线人数列表)
function sendAll(io){
// 表示一旦有客户端连接到服务器,就会向所有的客户端发送当前的在线人数(用户列表)
io.sockets.emit(
WebSocketType.GroupList,
createMessage(//io模块提供一个.sockets方法可以向所有的客户端进行操作
'',//不将消息的发送者传过去
Array.from(io.sockets.sockets).map(item=>item[1].user)//将所有的在线用户发送过去
))
}

结果展示

登陆页面的跳转

image

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

image

群聊和私聊功能的实现

image

退出功能并清除本地token

image

数据库表图

image