1. 具体代码在需要的下载
https://gitee.com/zyqwasd/socket
效果:
2. package.json文件
1. 下载基本的模块 修改了start 脚本 nodemon 需要先单独下载 npm install nodemon 开启服务器直接nodemon就好
1 { 2 "name": "socketio", 3 "version": "1.0.0", 4 "description": "", 5 "main": "app.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1", 8 "start": "nodemon app.js" 9 }, 10 "author": "", 11 "license": "ISC", 12 "dependencies": { 13 "jsonwebtoken": "^8.5.1", 14 "koa": "^2.13.4", 15 "koa-bodyparser": "^4.3.0", 16 "koa-router": "^12.0.0", 17 "koa-static": "^5.0.0", 18 "mysql2": "^2.3.3", 19 "socket.io": "^4.5.1" 20 } 21 }
3. 搭建简单的koa服务器
const koa = require("koa"); //koa模块 const static = require("koa-static"); //静态路由模块 const path = require("path"); //path处理地址模块 const bodyParser = require("koa-bodyparser");// 解析post传参模块 const indexRouter = require("./routers/indexRouter");// 自己定义的路由模块 const { createServer } = require("http");// 因为socket.io 用到http 中creareServer 方法const app = new koa();// 实例化 koa const httpServer = createServer(app.callback()); //引入socketServer模块 const socketServer = require("./service/socketService"); // 把需要的httpServer传进去 这个是后面的方法 因为里面要用到httpServer 我就传了进去 socketServer(httpServer); // 获取post数据 app.use(bodyParser()); // 静态资源 app.use(static(path.join(__dirname, "public"))); // 路由 app.use(indexRouter.routes());
// 监听 httpServer.listen(3000, () => { console.log("server start"); });
4.indexRouter路由 主要是管理和分配其他路由模块
const indexRouter = require("koa-router");// const loginRouter = require("./loginRouter"); const chatRouter = require("./chatRouter"); const router = new indexRouter(); //实例化路由模块 router.use("/login", loginRouter.routes(), loginRouter.allowedMethods());// 如果访问关于 /login 就进入这个 router.use("/", chatRouter.routes(), chatRouter.allowedMethods());// 如果访问 / 就走这个 allowedMethods()可以很友好的告诉客户端服务端响应的是什么请求 module.exports = router; // 导出模块
5. loginRouter 路由 主要是验证用户名和密码来设置token的
const loginRouter = require("koa-router"); const router = new loginRouter(); const controller = require("../controller/loginController"); // 访问/login 就指向/login.html页面 router.get("/", (cxt, next) => { cxt.redirect("/login.html"); });
// 如果post 请求来了就传到 controller 模块中的find 方法 这个封装的方法 router.post("/", controller.find); module.exports = router;
6. chat 路由 主要是指向页面的
const chatRouter = require("koa-router"); const router = new chatRouter(); // 访问/chat 就返回 /chat.html 页面 router.get("/", (cxt, next) => { cxt.redirect("/chat.html"); }); module.exports = router;
7.login中的controller模块 用来验证用户名密码给前端传token
const service = require("../service/loginService");//导入模块主要用来去数据库查找 const jwt = require("../uilt/JWT");// 这个主要用来加密生成token 方法在下面有说明 const controller = { async find(cxt, next) {// 定义响应给服务器的函数 const { password, username } = cxt.request.body;// 把数据解构出来 // 去数据库查找 let [data] = await service.find(username, password);// 去service模块中去查找数据 等待数据返回 这里用到了 async await 解决异步问题 if (data.length) {//返回的是数组 如果找到到了length就不为0 let token = jwt.generate(data[0], "1d"); // 利用封装的方法 传入数据生成token cxt.set("Authorization", token);//设置响应头字段为Authorization 给前端返回token cxt.body = { OK: 1 };// 返回前端一个 ok:1 } else {// 不成功就返回一个ok:0 cxt.body = { OK: 0 }; } }, }; module.exports = controller;
8. 封装的jwt 模块
const jwt = require("jsonwebtoken");//导入安装好的jsonwebtoken模块 const secret = "chatjsonwebtoken"; // 设置秘钥 myjwt = {
//写一个生成token 的方法传入值和过期时间就能生成token generate(value, expires) { return jwt.sign(value, secret, { expiresIn: expires }); },
//生成解密的办法 因为这个token 解密失败会阻断服务器 所以用trycath 来处理 成功返回数据 不成功返回false verify(token) { try { return jwt.verify(token, secret); } catch (error) { return false; } }, }; module.exports = myjwt;
9.定义service方法来链接数据库这里用到的是mysql 数据库和mysql2模块
const mysql = require("mysql2"); // 创建一个对象 导出这个对象就好 方法写在这个大对象中 const service = {
//接收传来的数据 find(username, password) {
//因为这个找不到也会报错阻止浏览器运行 所以用到了 trycath try {//创建连接 因为这个是异步的所以用到了promise() const promisePool = mysql.createPool(getDBConfig()).promise();
//sql 语句查询 return promisePool.query( `select username from user where username=? && password=?`, [username, password]// 这个是传参 对应的是里面的?号 ); } catch (error) {//找不到就返回 【】 return []; } }, }; module.exports = service;
// 定义连接数据 function getDBConfig() { return { host: "127.0.0.1",//地址 user: "root",//用户名字 port: "3306",// 端口 一般都是3306 如果换了就换这个端口就行 password: "",// 密码因为我是本地的就没有设置密码 database: "chat", // 数据库名字 connectionLimit: 1,//连接池 }; }
核心代码 处理数据和响应数据 socket.io 的处理私聊和群聊 和渲染列表
1. 主要用官网的emit 发送消息 和 on 事件监听消息 类似与一种订阅和发布的那种感觉 去看看官网文档 非常的详细 https://socket.io/docs/v4
const { Server } = require("socket.io"); // 引入jwt 模块 const jwt = require("../uilt/JWT"); // 定义函数来处理数据 function socketServer(httpServer) {
//创建io 这个服务器它和koa用的端口是一个 const io = new Server(httpServer); io.on("connection", async (socket) => {//当有设备连入的时候 会发生的事情 // 拿到token值 let token = socket.handshake.query.token; let payload = jwt.verify(token); if (payload) { // 如果 token 没过期就走这 socket.user = payload.username;// 给每一个设备都打上一个标签挂上他自己的username 方便后面查找 socket.emit(msgType.chatGroup, chatData(`欢迎${socket.user}`, "广播"));// 给每个进入的设备都发一个广播欢迎用户 } else { // token 过期了就直接send error 给前端 socket.emit(msgType.chatError, chatData("token过期")); } // 当获取列表传来的时候 socket.on(msgType.chatList, (msg) => { // 就给前端返回列表 sendAll(io); }); // 当客户端发来群发 socket.on(msgType.chatGroup, (msg) => { socket.broadcast.emit(msgType.chatGroup, chatData(msg.data, socket.user));//这个是利用官方给的方法忽略自己给所有人发 数据类型是群发 }); // 当客户端发来私聊 监听单聊发来的类型 socket.on(msgType.singleChat, (msg) => {
//这里用到前面在socke上打上的标记 数据类型是真的不好找而且嵌套太多开始参考了许多文献发现都没有太好的方法 官方给的方法是用嵌套字id 大家也可以试试上面有文档 Array.from(io.sockets.sockets).forEach((item) => {
// 其中item[1]是每一个的客户端 item[0]好像是id 大家可以自行打印一下 if (item[1].user === msg.user) {
//当item[1]找到给他打上的标记 等于了 客户端给我们传来的向哪个用户发的名字 就让这个客户端返回一个私聊的数据 item[1].emit(msgType.singleChat, chatData(msg.data, socket.user)); } }); }); // 当客户端关闭的时候会发生的 事情 这个也是官网给的 方法 socket.on("disconnect", () => { sendAll(io);//重新渲染列表 }); }); } // 把模块暴露出去 module.exports = socketServer; // 定义回传的数据回传一样的数据可以方便前端渲染页面 function chatData(data, user) { return { data, user, }; } //定义回传类型 这个定义回传类型 语义化更加清晰 和前端的类型保持一致好判断不至于混乱 const msgType = { chatError: 0, chatList: 1, chatGroup: 2, singleChat: 3, }; // 发送列表 function sendAll(io) { // 获取到列表;
let arr = Array.from(io.sockets.sockets)// 先拿到 io.sockets.sockets这个是没一个的客户端 把它变为 数组 .map((item) => item[1].user) // 利用数组的map方法来映射处理拿到每一个我们给它打的标记就是每个的用户名把数据在返回回去成为一个新的数组 .filter((item) => item);//因为中间有好多undefined 必须得处理不然客户端会一直报错 把它筛选过滤利用数组的filter 只有真的留下了返回新的数组 io.emit(msgType.chatList, Array.from(new Set(arr))); // 向客户端发送列表信息 但是这个列表信息如果多次刷新客户端会造成用户名多次出现在中列表所以用set结构来进行数组去重再变回数组 }
前端代码login 登录页面代码只有js 代码 页面结构比较乱大家可以去 gitee 查看下载上面有地址
1 document.querySelector("button").addEventListener("click", function () {// 获取页面元素 添加点击事件 2 fetch("/login", {// fetch方法向login发post 请求携带获取到的input 的值 3 method: "post", 4 body: JSON.stringify({ 5 username: username.value, 6 password: password.value, 7 }), 8 headers: {// 以josn 格式发送 9 "Content-Type": "application/json", 10 }, 11 }) 12 .then((res) => {//这个是没经过处理的函数想要获取到请求头中的token必须在第一个里面获取这个方法的请求头通过get方法获取到 大家可以依次打印
13 localStorage.setItem("token", res.headers.get("authorization")); // 设置本地存储 存入token 14 return res.json(); 15 }) 16 .then((res) => { 17 if (res.OK) { // 判断后端传来的ok 18 localStorage.setItem("username", username.value);//设置username 方便后期渲染页面处理数据 19 location.href = "/";//跳转到/路径这里指向的是chat.html 20 } else { 21 text_err.innerHTML += "用户名密码错误"; //错误了就在页面上显示错误 22 } 23 }); 24 });
前端chat 代码 处理数据和发送数据 先导入socket.io模块的客户端代码 我用的是4.5.2 版本的min.js
- https://github.com/socketio/socket.io/tree/main/client-dist
- 客户端和服务器端用的方法啥的都挺 一样的非常的不错
// scoket服务器连接
const socket = io( `ws://localhost:3000?token=${localStorage.getItem("token")}`//先连接服务器发送token验证token ); //定义接受后端类型 和给服务端传的数据类型和服务器的一样 const msgType = { chatError: 0, chatList: 1, chatGroup: 2, singleChat: 3, }; // --------------------------------------发送消息 // 进来直接去请求列表 socket.emit(msgType.chatList); document .querySelector("button") .addEventListener("click", function () { // 给按钮添加点击事件 // 如果为空return if (!ipt.value.trim()) return alert("不能为空"); if (select.value === "all") { // 发送群聊 数据是输入框的数据 socket.emit(msgType.chatGroup, chatData(ipt.value)); // 因为群聊没有给自己发所以要自己渲染页面 renderLi(chatData(ipt.value, localStorage.getItem("username")),true); } else { // 私聊 发送想要聊天的用户给客户端和数据 socket.emit(msgType.singleChat, chatData(ipt.value, select.value)); // 因为群聊没有给自己发所以要自己渲染页面 renderLi(chatData(ipt.value, localStorage.getItem("username")),true); } //清空聊天框 ipt.value = ""; }); // ---------------------------------------接受消息 // 接受群聊消息 socket.on(msgType.chatGroup, (msg) => { renderLi(msg);//传到方法中渲染小li }); // 接收列表信息 socket.on(msgType.chatList, (msg) => { select.innerHTML = ` <option value="all">all</option>` + // 第一个一定是all 所以用到这种拼接的方法 msg.map((item) => ` // 用到了map 加工处理每个数据 <option value="${item}">${item}</option> `).join("");// 把数据转为字符串 }); // 接受错误消息 socket.on(msgType.chatError, (msg) => { localStorage.removeItem("token"); //当有错误传来的时候先删除无用的token 在跳转页面 location.href = "/login"; }); // 接受私聊消息 socket.on(msgType.singleChat, (msg) => { renderLi(msg);// 渲染数据 }); // ------------------------------------------方法 // 定义渲染ul中li function renderLi(data, flag) { // 创建一个小 li const li = document.createElement("li"); // 给li 加class类 判断 flag 来决定左右 li.className = flag ? "right" : "left"; // 把数据放进去 li.innerHTML = `<span>${data.user}</span>${data.data}`; ul.appendChild(li);//给ul 添加 li 在最后面 // 定义的方法让聊天框到最低下 scrollTo(); } // 定义接受后端的数据 function chatData(data, user) { return { data, user, }; } // 聊天框滚动事件 function scrollTo() { document.querySelector(".dialog").scrollTo(0, ul.offsetHeight);//让聊天框到最下面去 }