仿QQWeb即时聊天系统课设

一、实验题目

仿QQWeb即时聊天系统

二、实验目的

  • 熟悉WebSocket协议,掌握实时通信技术
  • 掌握前后端分离开发模式,前端使用Vue3,后端使用Spring Boot
  • 设计并实现一个简单的实时聊天系统
  • 提高数据库设计与管理能力
  • 实现基本的用户管理和聊天功能

功能要求

  1. 实现Web的点对点即时的文本消息聊天功能。
  2. 实现Web的表情的发送、接收和显示功能。
  3. 实现Web的图片的发送、接收和显示功能。
  4. 实现本地消息的存储,在离线的时候也能加载和查看历史消息;
  5. 要求使用WebSocket;

三、总体设计

背景知识

WebSocket是一种在单个TCP连接上进行全双工通信的协议,广泛应用于需要实时通信的场景。传统的 HTTP 请求-响应模型不同,WebSocket 允许服务器和客户端之间实时、低延迟的双向通信,减少了因频繁创建连接所带来的开销,适合用于聊天系统、实时通知、在线游戏等应用。

系统功能

  1. 用户注册和登录:用户可以注册账号,并通过登录进入系统
  2. 好友管理:用户可以添加好友,查看好友列表
  3. 实时聊天:用户可以与好友进行实时消息发送和接收
  4. 群聊功能:用户可以创建和加入群聊
  5. 消息存储:聊天记录持久化,用户可以查看历史聊天记录

数据库设计

数据库采用MySQL,主要表结构设计如下:

  1. 用户表(users)
    • id:用户唯一标识(主键)
    • username:用户名
    • password:密码(加密存储)
    • email:用户邮箱
  2. 好友关系表(friends)
    • user_id:用户ID
    • friend_id:好友ID
    • status:好友关系状态(如0表示请求中,1表示已添加)
  3. 消息表(messages)
    • id:消息ID(主键)
    • sender_id:发送者ID
    • receiver_id:接收者ID
    • content:消息内容
    • timestamp:消息发送时间
  4. 群组表(groups)
    • id:群组ID(主键)
    • group_name:群组名称
  5. 群组成员表(group_members)
    • group_id:群组ID
    • user_id:用户ID
  6. 群组消息表(group_messages)
    • id:消息ID(主键)
    • group_id:群组ID
    • sender_id:发送者ID
    • content:消息内容
    • timestamp:消息发送时间

表之间的关联关系

后端功能模块

用户管理模块

    • 注册:用户提供用户名、密码、邮箱进行注册
    • 登录:用户提供用户名和密码进行登录,校验通过后生成会话
    • 用户信息:获取和更新用户信息

好友管理模块

    • 添加好友:发送好友请求,对方同意后成为好友
    • 好友列表:获取当前用户的好友列表

聊天模块

    • 单聊:用户之间点对点的消息发送与接收
    • 群聊:群组内的消息发送与接收

WebSocket管理模块

    • 连接管理:建立和管理WebSocket连接
    • 消息处理:处理消息的发送和接收,包含单聊和群聊

前端设计

前端采用Vue3框架,主要页面包括:

  1. 登录页面
  2. 注册页面根据邮箱发送验证码进行注册
  3. 主页面:包含好友列表、聊天窗口、群组列表
  4. 修改头像界面
  5. 聊天页面:显示当前聊天记录,支持发送消息,发送表情和图片
  6. 修改密码界面
  7. 忘记密码界面:根据邮箱发送验证码进行改密界面截图: 的关联关

Tocken的使用

Token是一种用于身份验证和授权的令牌(Token)机制,在网络通信中广泛使用。它是一个字符串,代表着用户的身份或权限,用于验证用户在系统中的访问权限。

在身份验证方面,Token通常用于替代传统的基于会话的身份验证机制,如使用Cookie+Session的方式。使用Token进行身份验证的好处是,服务器不需要在内存中保存用户的会话信息,因为Token本身包含了所有验证所需的信息。这使得Token在分布式系统或无状态的API接口中非常适用。Token通常由服务器生成,并在用户登录或进行身份验证时发放给客户端。客户端将Token存储起来,并在后续的请求中将Token作为身份认证的凭证发送给服务器。服务器接收到Token后,可以通过验证Token的有效性来确认用户的身份和权限。常见的Token类型包括JWT(JSON Web Token)、OAuth 2.0的访问令牌(Access Token)、Bearer Token等。

在前端,使用浏览器提供的 Web Storage(如LocalStorage或SessionStorage)或者使用HTTP Cookie来存储Token。

使用LocalStorage:

// 存储Token到LocalStorage

localStorage.setItem('token', 'your_token_value');

// 从LocalStorage读取Token

const token = localStorage.getItem('token');

使用SessionStorage:

// 存储Token到SessionStorage

sessionStorage.setItem('token', 'your_token_value');

// 从SessionStorage读取Token

const token = sessionStorage.getItem('token');

使用HTTP Cookie:

// 存储Token到Cookie

document.cookie = 'token=your_token_value; expires=...; path=/';

// 从Cookie读取Token

const cookies = document.cookie.split(';');

let token = null;

cookies.forEach(cookie => {

  const [name, value] = cookie.trim().split('=');

  if (name === 'token') {

    token = value;

  }

});

在前端进行Token验证时,通过在请求头中添加Authorization字段,并将Token值作为其值传递给后端。在后端的代码中,使用@RequestHeader("Authorization") String token来获取请求头中的Token值进行验证和处理。

// axios请求拦截器

httpInstance.interceptors.request.use(

  (config) => {

// 1. 从pinia获取token数据

    const userStore = useUserStore();

    let token = "";

    // console.log("userStore.userInfo.data", userStore.userInfo.value.data.token);

    // 2. 按照后端的要求拼接token数据

    if (userStore.userInfo.value&&userStore.userInfo.value.data) {

      token = userStore.userInfo.value.data.token;

      console.log("token", token);

      // 解析JWT

      const decodedToken = jwtDecode(token);

      console.log(decodedToken);

      userStore.userInfo.id = decodedToken.claims.id;

      console.log("用户id", decodedToken.claims.id);

    }

    console.log("token", token);

    if (token) {

      config.headers.Authorization = `${token}`;

    }

    return config;

  },

  (e) => Promise.reject(e)

);

数据持久化存储的实现

使用的是pinia

Pinia 是一个基于 Vue.js 的状态管理库,用于管理应用程序的数据。它提供了一种简单、直观且可扩展的方式来组织和访问应用程序的状态,下面是详细介绍

基于 Vue 3:Pinia 是专门为 Vue 3 开发的状态管理库,充分利用了 Vue 3 的响应性系统和 Composition API。

类 Vuex 的 API:Pinia 的 API 设计灵感来自于 Vuex,因此对于熟悉 Vuex 的开发人员来说,使用 Pinia 应该会感到非常熟悉。

存储库(Stores):Pinia 将应用程序的状态组织为存储库的形式。每个存储库代表一个特定的数据领域或功能。存储库包含状态(state)、动作(actions)、获取器(getters)等。

响应式状态管理:Pinia 使用 Vue 3 的响应性系统,确保状态的变化能够自动追踪和响应,从而实现了高效的状态管理。

插件系统:Pinia 提供了插件系统,用于扩展和增强其功能。通过插件,您可以添加中间件、持久化存储、调试工具等来满足特定的需求。

类型安全:Pinia 支持 TypeScript,并且提供了类型安全的 API 和开发体验。这使得在开发过程中能够更好地捕获错误和进行静态类型检查。

支持异步操作:Pinia 支持在动作(actions)中执行异步操作,如发送网络请求、处理副作用等。

适用于大型应用程序:Pinia 的设计使得它非常适用于大型应用程序,可以轻松管理复杂的状态逻辑和数据流。

export const useUserStore = defineStore("user",() => {

    const userInfo = reactive({});

    // 2. 定义获取接口数据的action函数

    const getUserInfo = async ({ qqid, password }) => {

        const res=  await  loginAPI({ qqid, password })

          console.log("登录", res);

          userInfo.value = res;

          console.log('23',userInfo.value.code);

          // if(res.code==0)

        }

    const getDetailInfo =  () => {

       getDetailInfoAPI({}).then((result) => {

         console.log('r',result);

          userInfo.detail = result.data;

        })

        .catch((err) => {

          console.log("err", err);

        });

      // console.log('DetailInfo',res);

    };

  {

    persist: true,

  }

);

·  defineStore:这是 Pinia 提供的函数,用来定义一个 store。

·  reactive:Vue 的一个响应式 API,用来创建一个响应式对象。

·  loginAPI 和 getDetailInfoAPI:分别是登录和获取用户详细信息的 API 函数。

userInfo 是一个响应式对象,用来存储用户信息。这样我们在更新 userInfo 时,Vue 组件可以自动响应这些变化。

·  getUserInfo 是一个异步函数,用来调用 loginAPI 并处理返回的结果。

·  使用 try...catch 结构来处理可能的错误。

·  如果用户没有头像,我们为其设置一个默认头像。

·  将 API 返回的用户信息存储到 userInfo 中。·  getDetailInfo 是一个异步函数,用来调用 getDetailInfoAPI 并处理返回的结果。

·  将 API 返回的详细信息存储到 userInfo.detail 中。

默认头像:为没有头像的用户设置一个默认头像,提升用户体验。

跨域问题

 解决跨域问题并实现同一局域网下前后端实现分离调用后端接口,方便加快开发的效率:

首先跨域是什么?浏览器对于javascript的同源策略的限制 。

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。设想这样一种况:A 网站是一家银行,用户登录以后,A 网站在用户的机器上设置了一个 Cookie,包含了一些隐私信息(比如存款总额)。用户离开 A 网站以后,又去访问 B 网站,如果没有同源限制,B 网站可以读取 A 网站的 Cookie,那么隐私信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。

解决跨域问题的方案

1.Jsonp

Jsonp

最早的解决方案,利用script标签可以跨域的原理实现。

https://www.w3cschool.cn/json/json-jsonp.html

限制:

需要服务的支持

只能发起GET请求

原理:

Jsonp其实就是一个跨域解决方案。Js跨域请求数据是不可以的,但是js跨域请求js脚本是可以的。可以把数据封装成一个js语句,做一个方法的调用。跨域请求js脚本可以得到此脚本。得到js脚本之后会立即执行。可以把数据做为参数传递到方法中。就可以获得数据。从而解决跨域问题。

2.nginx

nginx反向代理

思路是:利用nginx把跨域反向代理为不跨域,支持各种请求方式

缺点:需要在nginx进行额外配置,语义不清晰

前端server的域名为:fe.server.com

后端服务的域名为:dev.server.com

现在我在fe.server.com对dev.server.com发起请求一定会出现跨域。

现在我们只需要启动一个nginx服务器,将server_name设置为fe.server.com,然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回dev.server.com。如下面的配置:

server {

        listen       80;

        server_name  fe.server.com;

        location / {

                proxy_pass dev.server.com;

        }

}

这样就可以完美绕过浏览器的同源策略了。

fe.server.com访问nginx的fe.server.com属于同源访问,而nginx对服务端转发的请求不会触发浏览器的同源策略。

3.CORS

CORS

规范化的跨域请求解决方案,安全可靠。

优势:

在服务端进行控制是否允许跨域,可自定义规则

支持各种请求方式

缺点:

会产生额外的请求(预检)

3.1 什么是cors

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

XMLHttpRequest:Ajax的核心对象

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

浏览器端:不用考虑

目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。

服务端:进行相关设置

CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否允许其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。

3.2 原理

预检请求

跨域请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

    OPTIONS /cors HTTP/1.1

    Origin: http://localhost:8888

    Access-Control-Request-Method: GET

    Access-Control-Request-Headers: X-Custom-Header

    User-Agent: Mozilla/5.0...

Origin:会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

Access-Control-Request-Method:接下来会用到的请求方式,比如PUT

Access-Control-Request-Headers:会额外用到的头信息

一个“预检”请求的样板:

预检请求的响应

服务的收到预检请求,如果许可跨域,会发出响应:

    HTTP/1.1 200 OK

    Date: Mon, 01 Dec 2008 01:15:39 GMT

    Server: Apache/2.0.61 (Unix)

    Access-Control-Allow-Origin: http://localhost:8888

    Access-Control-Allow-Credentials: true

    Access-Control-Allow-Methods: GET, POST, PUT

    Access-Control-Allow-Headers: X-Custom-Header

    Access-Control-Max-Age: 1728000

    Content-Type: text/html; charset=utf-8

    Content-Encoding: gzip

    Content-Length: 0

    Keep-Alive: timeout=2, max=100

    Connection: Keep-Alive

    Content-Type: text/plain

如果服务器允许跨域,需要在返回的响应头中携带下面信息:

Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*(代表任意域名)

Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

Access-Control-Allow-Methods:允许访问的方式

Access-Control-Allow-Headers:允许携带的头

Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

有关cookie:

要想操作cookie,需要满足以下条件:

服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。

浏览器发起ajax需要指定withCredentials 为true

四.详细设计

程序流程图

从登录到发送消息的流程图如下:

    A[用户打开应用] --> B[用户登录]

    B -->|登录成功| C[获取用户信息]

    C --> D[获取好友列表]

    D --> E[获取群组列表]

    E --> F[显示主页面]

    F --> G[用户选择好友或群组]

    G --> H[建立WebSocket连接]

    H --> I[用户发送消息]

    I --> J[通过WebSocket发送到服务器]

    J --> K[服务器接收并处理消息]

    K --> L[服务器将消息转发给接收方]

    L --> M[接收方通过WebSocket接收消息]

    M --> N[接收方显示消息]

B -->|登录失败| O[显示错误信息]

前端实现(vue3+vant4+javaScript+websockt+pinia)

  1. Websocket.js配置
  2. 主要先与服务器建立websocket连接,再调用发送消息的sendMessage的方法socket.send(message),websocket.onmessage(message)方法接受者接收消息,注入method方法出来接收的消息

3.传递的消息包括发送者id和接受者id与消息内容,消息类型,消息发送时间

因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller

直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。

新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。

集群版(多个ws节点)需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可。

  1. 聊天界面UI设计

 分为头部聊天对方的昵称,退出

主体部分,显然聊天消息

尾部对话输入框与图片发送

头尾部使用fixed固定

主体部分使用flex和float结合实

一进入界面跳转到最后一条消息,监听窗口中消息地底部到消息顶部的距离以及视口的长度,通过移动右边滑动框到底部使得消息回到底部

onMounted(() => scrollToBottom());

onUpdated(() => scrollToBottom());

const scrollToBottom = () => {

  if (messagesContainer.value) {

    messagesContainer.value.scrollHeight;

    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;

  }

};

后端实现(Spring Boot)

1. WebSocket配置

import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer {

    @Override

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")

                .setAllowedOrigins("*");

    }

}

2. WebSocket处理器

import org.springframework.web.socket.*;import org.springframework.web.socket.handler.TextWebSocketHandler;

public class ChatWebSocketHandler extends TextWebSocketHandler {

    @Override

    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

        // 连接建立后的处理

    }

    @Override

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        // 消息处理

    }

    @Override

    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

        // 连接关闭后的处理

    }

}

  1. 用户管理

import org.springframework.web.bind.annotation.*;

@RestController@RequestMapping("/api/user")public class UserController {

    @PostMapping("/register")

    public ResponseEntity<?> register(@RequestBody User user) {

        // 用户注册逻辑

        return ResponseEntity.ok("注册成功");

    }

    @PostMapping("/login")

    public ResponseEntity<?> login(@RequestBody User user) {

        // 用户登录逻辑

        return ResponseEntity.ok("登录成功");

    }

}

1. WebSocket连接

const socket = new WebSocket("ws://localhost:8080/ws/chat");

socket.onopen = function(event) {

    console.log("WebSocket is open now.");

};

socket.onmessage = function(event) {

    console.log("WebSocket message received:", event);

};

socket.onclose = function(event) {

    console.log("WebSocket is closed now.");

};

  1. 发送消息

function sendMessage(message) {

    if (socket.readyState === WebSocket.OPEN) {

        socket.send(JSON.stringify(message));

    }

}

  1. 接收消息

socket.onmessage = function(event) {

    const message = JSON.parse(event.data);

    // 处理接收到的消息

};

由于之前所写的跨域问题

因此我选择的是第三种cros方法解决跨域问题

 server:{

    port: 5133,

    host: '10.100.170.94',

    open:true,

    proxy: {

      '/api': {

        // target: 'http://47.121.140.11:5000',

        //实验环境

        // target: 'http://10.100.113.33:8889',

        target:'http://10.157.57.202:8889',

        // target: 'http://47.121.140.11:5000',

        changeOrigin: true,

        rewrite: (path) => path.replace(/^\/api/, ''),

      },

    },

  },

七、小结与心得体会

通过使用Spring Boot作为后端框架和Vue3作为前端框架开发实时聊天系统,我深入掌握了多项关键技术和开发经验。Spring Boot为我提供了依赖注入、RESTful API支持和数据库集成等强大功能,我利用它来管理后端业务逻辑、数据持久化和用户身份验证。通过适合Spring Boot的持久化解决方案(如Spring Data JPA),我设计和管理了数据库结构,确保了数据的安全性和一致性。

在前端开发方面通过结合 Vue 3、WebSocket、Vant 4、Pinia 和 Vue Router 技术,我成功开发了一个实时聊天系统。Vue 3作为前端框架,提供了强大的响应式和组件化能力,使得用户界面的构建和交互变得高效而灵活。WebSocket技术则实现了实时通讯,让用户能够即时收发消息。在组件库方面,Vant 4为项目带来了丰富的UI组件和样式,提升了用户界面的美观性和易用性。Pinia作为状态管理库,帮助我有效管理前端应用的状态,包括用户信息和聊天消息等。Vue Router则负责管理前端路由,实现页面间的切换和导航。

在开发过程中,我面对了一些挑战,例如WebSocket连接管理和跨域问题,通过查阅文档和调试代码逐步解决。这些经验不仅加深了对技术细节的理解,也提升了我的问题解决能力和团队协作技巧。

此外,我学习了如何使用Docker将Spring Boot应用部署到服务器上,并掌握了基本的部署和持续集成技能,为项目的上线和运维提供了便利。

综上所述,通过这个项目,我掌握了多种前端技术的应用和整合,还积累了丰富的开发经验,为将来在软件开发领域的探索和发展奠定了坚实的基础。,不仅积累了丰富的技术实践经验,还提升了团队协作和问题解决能力。这些都是未来在软件开发领域中非常宝贵的财富,希望这些经历能为我的未来学习和职业发展带来更多的启发和成长机会,为将来在软件开发领域的探索和发展奠定了坚实的基础。

参考资料

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值