赞
踩
WebSocket 是一种在 Web 应用程序中实现双向通信的协议。它提供了一种持久连接,允许客户端和服务器之间进行实时数据传输,而无需进行频繁的请求和响应。
相对于传统的 HTTP 请求-响应模式,WebSocket 在客户端和服务器之间建立起一条长久的双向通信通道。这意味着服务器可以主动向客户端推送数据,而不仅仅是在客户端发送请求时才能返回响应。
WebSocket 协议具有以下特点:
(1)双向通信:WebSocket 允许客户端和服务器之间进行双向通信,可以实时地发送和接收数据。
(2)持久连接:WebSocket 的连接会一直保持打开状态,而不需要在每次通信时都重新建立连接。这减少了网络开销和延迟。
(3)低延迟:由于 WebSocket 使用了持久连接,数据传输更加实时,没有了 HTTP 请求和响应的额外开销,因此具有较低的延迟。
(4)轻量级:WebSocket 协议相对于其他实现实时通信的技术(如轮询或长轮询)来说,传输的数据包大小较小,节省了带宽和资源消耗。
WebSocket 协议通常用于实时通信、聊天应用、实时数据推送、多人在线游戏等场景,可以提供更好的用户体验和更高效的数据传输。在前端开发中,可以使用 JavaScript 的 WebSocket API 来创建 WebSocket 连接,并进行数据的发送和接收。在后端开发中,需要使用相应的 WebSocket 服务器框架来处理 WebSocket 连接和消息的处理。
Session和Cookie是用于在Web应用程序中跟踪用户状态的机制,它们的工作原理如下:
(1)Cookie:
当用户访问一个网站时,服务器会在响应头中返回一个Set-Cookie
字段,其中包含了一个唯一的标识符(Cookie)以及其他相关信息,如过期时间、域名等。
浏览器接收到响应后,将Cookie保存在本地。之后,每次向同一域名发起请求时,浏览器会自动在请求头中添加一个Cookie字段,将保存的Cookie信息发送给服务器。
服务器通过读取请求头中的Cookie字段来识别用户,并根据其中的信息进行相应的处理,例如保持用户登录状态、记录用户偏好等。
(2)Session:
Session是一种服务器端的会话跟踪机制。当用户第一次访问网站时,服务器会为该用户创建一个唯一的Session ID,并将该ID存储在服务器上,通常存储在内存或数据库中。
服务器将Session ID发送给客户端,通常通过在响应头中设置一个名为Set-Cookie
的字段,其中包含了Session ID的值。
浏览器接收到响应后,会将Session ID保存在Cookie中,然后在每次请求中自动将该Cookie发送给服务器。
服务器通过读取请求中的Session ID来找到对应的会话数据,从而识别和跟踪用户的状态信息。
(3)Session相比于Cookie具有以下特点:
会话数据存储在服务器端,相对较安全,用户无法直接修改。
可以存储更多的数据量,不受Cookie大小限制。
需要在服务器上维护会话数据,对服务器资源有一定的消耗。
Cookie和Session通常一起使用,Cookie用于存储Session ID,而Session ID用于在服务器端标识和管理用户的会话数据。通过这种机制,Web应用程序能够实现用户的状态跟踪、身份验证、个性化设置等功能。
本系统设计实现了一个在线聊天系统,支持用户注册、用户登录、显示当前在线用户人数、查看当前在线用户、选择群聊、选择与当前在线用户私聊、实时接收消息等功能。
本项目采用前后端分离模式
前后端分离模式(Front-end and Back-end Separation)是一种软件架构模式,将前端和后端的开发和部署分离开来。
在这种模式下,前端和后端被视为独立的两个系统,通过接口进行通信。前端主要负责用户界面和用户交互的展示,通过 HTTP 请求与后端进行数据交互,获取数据并更新用户界面。后端主要负责处理业务逻辑和数据处理,提供 RESTful API 或其他形式的接口,接收前端发送的请求并返回相应的数据。
使用前后端分离模式可以将前端和后端解耦,提高了开发效率、用户体验和系统的可扩展性。
技术栈
后端:后端采用SpringBoot,数据库采用MySql,使用Mybatis-Plus对数据库进行操作
前端:前端采用Vue3框架,使用了Element UI、Ajax,采用JSON进行前后端数据交互
WebSocket: 使用 WebSocket 技术进行消息推送,同时保证消息的实时性。
首先搭建后端项目的基本架构,架构如下:
在pom.xml中添加SpringBoot、webSocket、Mysql等相关依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency>
创建 WebSocketConfig 配置类
package com.chat.room.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
编写服务端的WebSocket方法:
OnOpen() :
/** * 连接websocket调用的方法 * @param session * @param username */ @OnOpen public void onOpen(Session session, @PathParam("username") String username){ //1.存放username-session映射关系 sessionMap.put(username,session); //2.创建在线用户群 JSONObject onlineUsers = new JSONObject(); //3.创建在线用户列表 JSONArray onlineUserList = new JSONArray(); //4.创建映射对onlineUsers:onlineUserList onlineUsers.put("onlineUsers",onlineUserList); for (String uname : sessionMap.keySet()) { //创建json对象 JSONObject jsonObject = new JSONObject(); //存放名字 jsonObject.put("username",uname); String url = null; jsonObject.put("url",url); onlineUserList.add(jsonObject); } //5.转换json字符串 String message=JSON.toJSONString(onlineUsers); this.sendAllMessageToAllUsersInit(message); }
OnMessage():
/** * 发送消息方法 * @param message */ @OnMessage public void onMessage(String message){ //处理一下时间 LocalDateTime now = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String nowTime = now.format(dateTimeFormatter); //获取json数据 JSONObject jsonData = JSON.parseObject(message); //添加时间之后,再重新变换成json字符串给message jsonData.put("publishTime",nowTime); message = jsonData.toJSONString(); //判断是群发还是私发 Boolean isClusterSend = jsonData.getBoolean("isClusterSend"); //如果是群发,则执行群发方法 if(isClusterSend){ this.sendAllMessageToAllUsers(message); return; } //获取json数据的target目标用户 String target = jsonData.getString("target"); //获取目标用户对应的session连接 Session targetSession = sessionMap.get(target); //把这个消息传递到对应的session连接 this.sendMessageToOneUser(message,targetSession); }
OnClose():
@OnClose public void onClose(Session session, @PathParam("username") String username) { //移除用户名-session sessionMap.remove(username); //发送消息,用于前端更新在线人数信息 JSONObject onlineUsers = new JSONObject(); JSONArray onlineUserList = new JSONArray(); onlineUsers.put("onlineUsers", onlineUserList); for (String uname : sessionMap.keySet()) { JSONObject jsonObject = new JSONObject(); jsonObject.put("username", uname); String url = null; jsonObject.put("url", url); onlineUserList.add(jsonObject); } String message = JSON.toJSONString(onlineUsers); this.sendAllMessageToAllUsersInit(message); }
有多种方式可以设置session的有效时长,本次开发采用在配置文件中进行设置的方式,将session的有效时长设置为24小时,从而实现24小时内再次进入群聊能够自动识别用户,配置文件中的内容如下:
spring:
datasource:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_chat?useUnicode=true&characterEncoding=UTF-8&useSSL=TRUE&serverTimezone=Asia/Shanghai&useOldAliasMetadataBehavior=true
session:
timeout: 86400
server:
port: 8082
servlet:
session:
cookie:
max-age: 86400
通过上述设置,当用户第一次访问应用时,服务器会生成一个唯一的 session ID,并将其存储到浏览器的 Cookie 中。同时,服务器端也会在 Session 中存储相关的数据。如果用户在24小时内继续访问应用,则服务器会根据浏览器发送的 Cookie 中 session ID 来检索相应的 Session 数据。如果 Session 数据存在且没有过期,则可以认为用户已经登录,并继续处理请求。如果 Session 数据不存在或过期,则要求用户重新登录。
1.登录部分实现
controller层中的login方法负责接受前端传递过来的信息,同时调用service层中的login方法进行处理,service层调用mapper层对数据库进行操作,最终controller将处理完后的结果返回给客户端
controller层代码如下:
@PostMapping("/login")
public Login login(@RequestParam("username") String username, @RequestParam("password") String password,
HttpServletRequest request) {
HttpSession session = request.getSession();
Login login = userService.login(username, password, session);
return login;
}
service层代码如下:
@Override public Login login(String username, String password, HttpSession session) { Login login = new Login(); // 1.查询用户 User user = userMapper.selectOneByUsername(username); // 2.查不到用户,表示没有注册,直接返回 if (user == null) { login.setSuccess(false); login.setInfor("not register"); return login; } // 3.查到后,进行密码比对,如果不相等,表示登录失败 if (!MD5Utils.encrypt(password).equals(user.getPassword())) { login.setSuccess(false); login.setInfor("password error"); return login; } // 4.将用户信息存储在 session 中 session.setAttribute("username", username); // 5.返回 login return login; }
mapper层代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chat.room.mapper.UserMapper">
<select id="selectOneByUsername" resultType="user">
select * from user where username=#{username}
</select>
</mapper>
2.注册部分实现
controller层中的register方法负责接受前端传递过来的信息,同时调用service层中的register方法进行处理,service层调用mapper层对数据库进行添加操作,最终controller将处理完后的结果返回给客户端
controller层代码如下:
@PostMapping("/register")
public Register register(@RequestParam("username")String username,
@RequestParam(value = "password",required = false)String password,
@RequestParam(value = "authCode",required = false)String authCode, @RequestParam("phoneNumber")String phoneNumber){
if(phoneNumber.equals("")){
phoneNumber=null;
}
if(authCode.equals("")){
authCode=null;
}
Register register = userService.register(username, password, authCode,phoneNumber);
return register;
}
service层代码如下:
@Override public Register register(String username, String password, String authCode, String phoneNumber){ User user = new User(); Register register = new Register(); boolean hasRegister = this.checkHasRegister(username); if(hasRegister){ //查到了,说明已经注册过了,返回false注册失败 register.setSuccess(false); register.setInfor("hasRegister"); return register; } user.setUsername(username); user.setPassword(password); user.setRegisterTime(LocalDateTime.now()); //没查到即没有注册过,进行注册,注册前加密密码 user.setPassword(MD5Utils.encrypt(user.getPassword())); int count = userMapper.insertOne(user); if(count==0){ register.setSuccess(false); register.setInfor("system error"); return register; } //注册成功 register.setSuccess(true); return register; }
mapper层代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chat.room.mapper.UserMapper">
<insert id="insertOne">
insert into user(username,password,is_admin,register_time) values (#{username},#{password},#{isAdmin},#{registerTime})
</insert>
</mapper>
Vue.js 是一款用于构建用户界面的渐进式 JavaScript 框架。它专注于视图层,并且易于集成到现有项目中。
Vue 被设计为逐步采用的框架,这意味着你可以将其应用于现有的项目中,或者只使用其中的一部分功能来构建新项目。Vue 使用了基于依赖追踪的响应式系统,能够自动追踪和管理数据变化,并相应地更新视图。你只需要声明数据的关系,Vue 就会负责处理数据变化的更新。
可以将用户界面拆分为多个可重用的组件。每个组件包含了自己的模板、逻辑和样式,使得代码更具可读性、可维护性和复用性。
当数据发生变化时,Vue 会创建一个虚拟 DOM 树来描述最终的 DOM 结构,并通过比较前后两棵虚拟 DOM 树的差异,最小化实际 DOM 操作,从而提高性能。
同时提供了一套内置指令,用于操作 DOM。例如,v-bind 用于绑定属性,v-if/v-else 用于条件渲染,v-for 用于循环渲染等。除了内置指令,你还可以自定义指令来扩展 Vue 的功能。
配置 Vue.js 项目开发服务器的代理配置,解决跨域请求,以便于前端页面能够与后端服务进行通信,可以在vue.config.js中编写如下内容:
module.exports = defineConfig({ transpileDependencies: true, lintOnSave:false, devServer:{ proxy: { // 登录 '/user/login':{ target: 'http://127.0.0.1:8082', ws: true, // 支持websocket, changeOrigin: }, // 注册 '/user/register':{ target: 'http://127.0.0.1:8082', ws: true, // 支持websocket, changeOrigin: true }
前端需要通过代码初始化 WebSocket 对象,并向后端服务器发起 WebSocket 连接请求,以建立客户端与服务器之间的实时双向通信。
在建立连接后,前端可以通过 WebSocket 对象与服务器进行实时数据交换。当服务器有新的数据更新或者推送消息时,前端需要通过 WebSocket 接收器接收服务器推送的消息,并对其进行处理和展示。
在本项目中,前端需要通过 WebSocket 接收器接收其他用户发送的消息,并将其显示在聊天界面中。
WebSocket部分代码如下:
connectWebSocketInit(){ let vc=this const username=JSON.parse(localStorage.getItem("username")) if(typeof (WebSocket)=="undefined"){ console.log("您的浏览器不支持ws") return } console.log("您的浏览器支持ws") const webSocketUrl="ws://127.0.0.1:8082/chat/" + username socket=new WebSocket(webSocketUrl) vc.socket=socket socket.onopen=(()=>{ console.log(username+"已经连接上服务器") }) socket.onmessage=function (message){ let data=JSON.parse(message.data) if(data.onlineUsers!=null){ vc.onlineUsers=data.onlineUsers return } const yourUserName=JSON.parse(localStorage.getItem("username")) if(data.source==yourUserName){ return } vc.messages.push(data) console.log(vc.messages) } //关闭 socket.onclose=function (){ console.log(username+"退出") }
1.登录部分代码:
<template> <div style="width: 100%;height:800px;"> <div style="width: 100%;height: 2000px;"> <el-row style="width: 50%;height: 100%;"> <el-col style="width: 50%;height: 100%;"> <el-image :src="log.png" ></el-image> </el-col> </el-row> </div> <div style="width: 100%;height: 500px;background-color:yellow" > <el-row style="width: 100%;height: 100%;"> <el-col style="width:60%;height: 100%;"> <el-image :src="backg.png" style="width: 200%;height: 90%;position: relative;left: 280px;top: 20px;"></el-image> </el-col> <el-col style="width: 40%;height: 100%;"> <el-card style="width: 70%;height: 300px;position: relative;top: 18%;left: 5%; border-radius: 2%" shadow="always"> <el-row style="width: 100%;height: 60px;"> <el-form :model="logForm" :rules="rules" ref="logForm" label-width="90px" style="margin-top: 40px;"> <el-form-item label="用户名" prop="username"> <el-input v-model="logForm.username" auto-complete="off" style="width: 220px;" @focus="inputUsername"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="logForm.password" auto-complete="off" type="password" style="width: 220px;" @focus="inputPwd"></el-input> </el-form-item> </el-form> <el-col style="width: 100%;height: 100%;margin-top: 20px;font-family: 宋体;font-size: 22px;margin-left: 22px;"> <el-button type="primary" style="position: relative;left: 3%;bottom:25px;font-size: 16px;width: 150px;" @click="loginData('logForm')"> 登录 </el-button> <el-button type="primary" style="position: relative;left: 10%;bottom:25px;font-size: 16px;width: 150px;" @click="toRegPage">注册</el-button> </el-col> </el-row>> </el-card> </el-col> </el-row> </div> </div> </template>
2.注册部分代码:
<template> <el-container style="width: 100vw;height: 100vh;" class="backbg"> <el-row style="width: 100%;height: 100%;"> <el-row style="width: 100%;height: 14%;margin-top: 12%"> <el-image :src="re.png" style="height: 100px;width: 100%;"></el-image> </el-row> <el-row style="width: 100%;height: 70%;"> <el-card style="width: 100%;height: 55%;font-family:宋体;font-size: 30px;" shadow="always"> <el-form :model="regForm" :rules="rules" ref="regForm" label-width="90px" class="demo-regForm"> <el-form-item label="用户名" prop="username" > <el-input v-model="regForm.username" auto-complete="off"></el-input> </el-form-item> <el-form-item label="密码" prop="password" > <el-input v-model="regForm.password" auto-complete="off" type="password"></el-input> </el-form-item> <el-form-item label="确认密码" prop="actPassword" > <el-input v-model="regForm.actPassword" auto-complete="off" type="password"></el-input> </el-form-item> </el-form> <el-button type="primary" style="position: absolute;bottom: 50%;left: 20%;font-size: 20px;width: 100px;" @click="submitData('regForm')"> </el-button> <el-button type="primary" style="position: absolute;bottom: 50%;left: 50%;font-size: 20px;width: 120px;" @click="backLogin"> </el-button> </el-card> </el-row> </el-row> </el-col> </el-container> </template>
链接:代码仓库
1.登录界面
2.注册界面
3.群聊天室界面
通过此次课程,我学习理解了如何使用WebSocket进行双向通信和应用,以及如何处理前后端交互时可能出现的跨域问题并配置代理。此外,我还了解了Session的会话控制等内容,同时,我也学会了如何在实际项目中运用所学知识解决问题。
感谢中科大孟宁老师在教授我们网络程序设计课程中所付出的努力和耐心。在孟老师的悉心指导下,我对这门课程的理解有了显著提高,对计算机网络也有了更深入的认识。孟宁老师生动有趣地讲解课程内容,提供实际案例和编程实践,鼓励学生积极参与讨论和实验。这种教学方式使得课程更加生动有趣,让我们能够更好地理解和应用所学知识,通过这门课程的学习和实验,我不仅掌握了许多实用的技能,而且对网络编程有了更全面的认识。现在,我能够更加熟练地进行网络程序设计和开发,并且对其中的原理和技术有了更深入的理解。总的来说,这门课程给我带来了很多收获,我非常感谢孟宁老师的知识传授和耐心教导!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。