当前位置: 首页 > article >正文

全双工通信协议WebSocket——使用WebSocket实现智能学习助手/聊天室功能

一.什么是WebSocket?

WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器的全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输

HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。

这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 AJAX 请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

http协议:

07e80c5383974729ba528bceef04debb.jpeg

 

websocket协议:

aec402415caf4ee68e8b89bad2e044c8.jpeg

二.HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式 WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

三.基于WebSocket的智能学习助手功能实现

1.需求

通过 websocket 实现一个简易的聊天室功能 

        当点击智能学习助手选项,会进入一个聊天室,小助手自动向用户问好,用户可以向小助手提问问题,小助手后端查询到问题答案后会进行回复

bfa15c4c381e4e42829925803765104d.png

2.前端

前端环境:vue3+element-plus+pinia

因为我的这个项目用户端和管理端共用该功能,所以URL上带上了从pinia中获取的当前登录用户的信息,用于与后端建立唯一的连接标识

下面的代码是只对于学习小助手组件的.vue文件,读者有需自行扩展

<script setup>
import { ref } from "vue";
import { ElMessage } from "element-plus";

import useUserInfoStore from "@/stores/userInfo.js";
const userInfoStore = useUserInfoStore();
const userInfo = ref({ ...userInfoStore.info });

// 发送的信息
const say = ref("");
// 内容
const content = ref("");
// 管理员1,普通用户0
const role = userInfo.value.role == "管理员" ? 1 : 0;
// 发送给后端的URL
const url = ref("ws://localhost:8080/char/" + role);

var websocket = null;
//判断当前浏览器是否支持WebSocket
if ("WebSocket" in window) {
  //连接WebSocket节点
  websocket = new WebSocket(url.value);
} else {
  ElMessage.error("Not support websocket");
}

//连接发生错误的回调方法
websocket.onerror = function () {
  ElMessage.error("连接错误");
};

//连接成功建立的回调方法
websocket.onopen = function () {
  ElMessage.success("连接成功");
  createContent(false, "你好,我是智能学习小助手~");
};

//接收到消息的回调方法
websocket.onmessage = function (event) {
  createContent(false, event.data);
};

//连接关闭的回调方法
websocket.onclose = function () {
  ElMessage.success("连接关闭");
};

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
  websocket.close();
};

//发送消息
function send() {
  websocket.send(say.value);
  createContent(true, say.value);
  say.value = "";
}

//关闭连接
function closeWebSocket() {
  websocket.close();
}

// 构造消息框
const createContent = (isMyMsg, msg) => {
  let html;
  // 当前用户消息
  if (isMyMsg) {
    html =
      '<div class="el-row" style="padding: 5px 0">\n' +
      '  <div class="el-col el-col-22" style="text-align: right; padding-right: 10px">\n' +
      '    <div style="color: white;text-align: center;border-radius: 10px;font-family: sans-serif;padding: 10px;width:auto;display:inline-block !important;display:inline;background-color: deepskyblue;">' +
      msg +
      "</div>\n" +
      "  </div>\n" +
      '  <div class="el-col el-col-2">\n' +
      '  <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
      '    <img src="' +
      userInfo.value.userUrl +
      '" style="object-fit: cover;">\n' +
      "  </span>\n" +
      "  </div>\n" +
      "</div>";
  } else {
    // 助手信息
    html =
      '<div class="el-row" style="padding: 5px 0">\n' +
      '  <div class="el-col el-col-2" style="text-align: right">\n' +
      '  <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
      '    <img src="' +
      "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" +
      '" style="object-fit: cover;">\n' +
      "  </span>\n" +
      "  </div>\n" +
      '  <div class="el-col el-col-22" style="text-align: left; padding-left: 10px">\n' +
      '    <div style="color: white;text-align: center;border-radius: 10px;font-family: sans-serif;padding: 10px;width:auto;display:inline-block !important;display:inline;background-color: deepskyblue;">' +
      msg +
      "</div>\n" +
      "  </div>\n" +
      "</div>";
  }
  content.value += html;
};
</script>
<template>
  <el-card class="page-container">
    <template #header>
      <div class="block text-center">
        <el-row :gutter="20">
          <el-col :span="6" :offset="11"><span>智能学习小助手</span></el-col>
        </el-row>
        <!-- 分割线 -->
        <el-divider />
        <!-- 走马灯 -->
        <el-carousel height="150px">
          <el-carousel-item v-for="item in 4" :key="item">
            <h3 class="small justify-center" text="2xl">
              韩磊大帅哥{{ item }}
            </h3>
          </el-carousel-item>
        </el-carousel>
      </div>
    </template>
    <!-- 滚动条 -->
    <el-scrollbar height="460px">
      <div v-html="content"></div>
    </el-scrollbar>
    <template #footer>
      <el-row>
        <el-input
          style="width: 100%"
          :autosize="{ minRows: 4, maxRows: 8 }"
          type="textarea"
          v-model="say"
          placeholder="有困难就找汪汪队~"
        />
      </el-row>
      <el-row style="margin-top: 10px">
        <el-col :span="2" :offset="20">
            <div class="grid-content ep-bg-purple" />
            <el-button type="primary" @click="send()">发送</el-button>
        </el-col>
        <el-col :span="1">
            <div class="grid-content ep-bg-purple" />
            <el-button type="primary" @click="say = ''">清空文本</el-button>
        </el-col>
      </el-row>
    </template>
  </el-card>
</template>
<style lang="scss" scoped>
.page-container {
  min-height: 100%;
  box-sizing: border-box;

  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
}
.el-carousel__item h3 {
  color: #475669;
  opacity: 0.75;
  line-height: 150px;
  margin: 0;
  text-align: center;
}
.img {
  width: 100%;
  height: 460px;
}
.el-carousel__item:nth-child(2n) {
  background-color: #99a9bf;
}

.el-carousel__item:nth-child(2n + 1) {
  background-color: #d3dce6;
}
.tip {
  color: white;
  text-align: center;
  border-radius: 10px;
  font-family: sans-serif;
  padding: 10px;
  width: auto;
  display: inline-block !important;
  display: inline;
}
.right {
  background-color: deepskyblue;
}
.left {
  background-color: forestgreen;
}
</style>

3.后端

后端环境:springboot+lombok+web+mybatisplus+websocket

3.1.Springboot 添加Pom依赖

<!--websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

3.2.添加Websocket配置


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

3.3.构建数据库

3.3.1.配置并连接数据库并创建问题表(自行完成)

3.3.2.pojo类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("lw_answers")
public class Answer {
    @TableId(type = IdType.AUTO)
    private Long answerId; // 问题ID
    private String issue; // 问题
    private String answer; // 回答
}

3.3.3.注册Mapper接口

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hl.pojo.entity.Answer;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AnswerMapper extends BaseMapper<Answer> {
}

3.3.4.注册WebSocketServer接口

因为我们每个建立会话的对象要唯一!所以对于不同的用户我们根据用户角色和用户id来建立唯一标识

package com.hl.server.websocket;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hl.common.constant.MessageConstant;
import com.hl.common.context.BaseContext;
import com.hl.common.enumeration.UserRole;
import com.hl.pojo.entity.Answer;
import com.hl.server.mapper.AnswerMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket服务
 */
@Slf4j
@Component
@ServerEndpoint("/char/{role}")
public class WebSocketServer {
    /**
     * WebSocket API 是独立于任何特定框架的标准,
     * 它的生命周期和管理是由 WebSocket 容器负责的,而不是由 Spring 容器负责。
     * 因此,WebSocket 容器不会识别或处理 Spring 的依赖注入注解。
     */
    private static AnswerMapper answerMapper;
    @Autowired
    private void setAnswerMapper(AnswerMapper answerMapper) {
        WebSocketServer.answerMapper = answerMapper;
    }


    //存放会话对象
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session,@PathParam("role") Integer role) {
        String key=getUserInfo(role);
        log.info("WebSocketServer onOpen role:{}",key);
        sessionMap.put(key, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message,@PathParam("role") Integer role) {
        String key=getUserInfo(role);
        log.info("WebSocketServer onMessage user:{} message:{}",key,message);
        LambdaQueryWrapper<Answer> queryWrapper=new LambdaQueryWrapper<Answer>()
                .like(Answer::getIssue,message);
        Answer ans = answerMapper.selectOne(queryWrapper);
        String answer = ans==null? MessageConstant.BU_ZHIDAO:ans.getAnswer();
        sessionMap.get(key).getAsyncRemote().sendText(answer);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("role") Integer role) {
        String key=getUserInfo(role);
        log.info("WebSocketServer onClose:{}",key);
        sessionMap.remove(key);
    }

    /**
     * 获取唯一标识key
     */
    public String getUserInfo(Integer role) {
        String key="";
        // 根据LocalThread获取当前登录用户id
        Long id= BaseContext.getCurrentId();
        if(role== UserRole.COMMON.getValue()){
            //普通用户
            key=UserRole.COMMON.getDesc()+id;
        }else{
            //管理员
            key=UserRole.ADMIN.getDesc()+id;
        }
        return key;
    }

}

注: WebSocket API 是独立于任何特定框架的标准, 它的生命周期和管理是由 WebSocket 容器负责的,而不是由 Spring 容器负责。 因此,WebSocket 容器不会识别或处理 Spring 的依赖注入注解,所以我们不能使用字段注入!

 

 

 


http://www.kler.cn/a/383523.html

相关文章:

  • 修改el-select下拉框高度;更新:支持动态修改
  • 【知识】cuda检测GPU是否支持P2P通信及一些注意事项
  • 面向未来的教育技术:智能成绩管理系统的开发
  • Vue3中路由跳转之后删除携带的query参数
  • web-密码安全口令
  • python 内存管理
  • DAY56 ||99.岛屿数量 深搜 |99.岛屿数量 广搜 |100.岛屿的最大面积
  • Android 项目模型配置管理
  • 《无线重构世界》射频模组演进
  • Spring AI 核心概念
  • 数据结构和算法-01背包问题01-认识01背包
  • SpringBoot健身房管理:现代化技术解决方案
  • 如何使用闲置硬件搭建一个安装运行资源较少的Tipask问答网站服务器
  • 如何安全地使用反射API进行数据操作
  • NLP segment-03-基于 TF-IDF 实现关键词提取 java 开源实现
  • 【无标题】123
  • Web Components 是什么
  • 少儿编程教育的多维度对比:软件类、硬件类与软硬件结合课程的选择
  • 【网易云插件】听首歌放松放松
  • Oracle视频基础1.4.5练习
  • sdm845(oneplus6)的开机变砖(启动漰溃)ramdump被开源git仓库linux-ramdump-parser-v2提交3e7f37-正确解析
  • 代码随想录训练营Day19 | 39. 组合总和 - 40.组合总和II - 131.分割回文串
  • OpenCV视觉分析之目标跟踪(8)目标跟踪函数CamShift()使用
  • 【RESP问题】RESP.app GUI for Redis 连接不上redis服务器
  • AI - 使用LangChain请求LLM结构化生成内容
  • Unet++改进3:添加NAMAttention注意力机制