实现 WebSocket 接入文心一言

news2024/12/22 17:42:43

目录

什么是 WebSocket?

为什么需要 WebSocket?

HTTP 的局限性

WebSocket 的优势

总结:HTTP 和 WebSocket 的区别

WebSocket 的劣势

WebSocket 常见应用场景

WebSocket 握手过程

WebSocket 事件处理和生命周期

WebSocket 心跳机制

配置接入文心一言

WebSocket 实现


什么是 WebSocket?

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

为什么需要 WebSocket?

HTTP 的局限性

HTTP 协议是基于请求-响应模式的,它的设计适合于静态页面的交互,但对于实时通信却有以下局限:

  • 单向通信
    HTTP 是单向通信协议,客户端必须发起请求,服务器才能响应,服务器无法主动向客户端推送消息。

  • 高延迟
    为了获取最新数据,客户端需要不断轮询,即周期性发送请求检查是否有新消息。这会导致明显的延迟,无法满足实时需求。

  • 资源浪费:

    • 在长轮询中,服务器需要保持连接直到有新数据可发送,这会消耗服务器资源。
    • 即使使用普通轮询,大量的请求仍会消耗带宽和服务器处理能力。

WebSocket 的优势

1. 全双工通信

  • WebSocket 支持 双向通信,客户端和服务器可以在单个连接上同时发送和接收消息。
  • 与 HTTP 的单向请求-响应模式相比,WebSocket 提供了更高效的通信机制,特别适合需要频繁数据交换的场景。

2. 长连接

  • WebSocket 建立连接后,保持连接持续打开,直到客户端或服务器主动关闭。无需像 HTTP 那样频繁建立和断开连接。
  • 长连接的特性降低了连接建立和关闭的开销,提高了性能。

3. 低延迟

  • 一旦连接建立,WebSocket 消息传输延迟极低。
  • 服务器可以 主动推送数据 到客户端,无需等待客户端的请求。

4. 较少的网络开销

4.1 减少握手和头部信息

  • WebSocket 的握手过程发生在连接建立时,之后的数据帧头部信息非常小。
  • HTTP 的每个请求都有完整的头部,而 WebSocket 只需一次握手,数据传输更加高效。

4.2 减少带宽消耗

  • 相较于 HTTP 轮询或长轮询,WebSocket 显著减少了带宽消耗,数据流量更小,网络利用率更高。

5. 实现实时功能

WebSocket 为许多实时应用提供了天然支持,避免了传统 HTTP 无法满足的延迟问题。
常见功能包括:

  • 实时消息推送。
  • 实时互动(如在线聊天)。
  • 数据同步。

6. 支持二进制数据

  • WebSocket 不仅可以传输文本数据,还支持传输二进制数据,这使得 WebSocket 能够高效处理图像、音频、视频等多媒体数据。

7. 跨平台支持

  • WebSocket 是一种标准协议,被广泛支持于各种语言、框架和平台(如 JavaScript、Python、Java 等)。
  • 不论是前端浏览器还是后端服务器,都可以轻松实现 WebSocket 功能。

8. 安全性

  • WebSocket 支持加密协议 WSS(WebSocket Secure),通过 TLS/SSL 保障数据传输的安全性。
  • 可以结合身份验证(如 JWT)或 IP 限制等机制,防止连接滥用。

总结:HTTP 和 WebSocket 的区别

特点WebSocketHTTP
通信方式双向通信单向请求/响应模式
连接类型长连接,连接保持打开默认短连接,每次请求需新建连接
实时性高,低延迟中等,轮询或长轮询增加延迟
效率数据传输轻量,性能高每次请求头部信息冗余,开销大
适用场景实时推送、聊天、游戏、物联网等静态内容加载、API 调用

WebSocket 的劣势

1. 复杂性较高

  • 协议实现复杂:与传统的 HTTP 模型相比,WebSocket 需要额外的握手过程,并且要求服务器支持 WebSocket 协议。
  • 开发难度增加:需要实现双向通信的逻辑,并处理连接生命周期、断线重连等问题。

2. 消耗资源

  • 连接资源占用:WebSocket 需要长期占用服务器的连接资源,特别是在高并发场景中,服务器需维护大量的长连接,可能导致资源消耗增加。
    • 例如:服务器需要为每个 WebSocket 连接维护状态,而 HTTP 是无状态的。
  • 客户端性能开销:在移动设备或低性能设备上,长时间保持 WebSocket 连接可能会增加电量和网络资源的消耗。

3. 安全性问题

  • 身份认证不足:WebSocket 本身没有内置的身份认证机制,需要额外实现安全验证(如使用 JWT 或 API Key)。
  • 更易被滥用:
    • DDoS 攻击:攻击者可能通过建立大量 WebSocket 连接,耗尽服务器资源。
    • 劫持风险:如果使用未加密的 WebSocket(ws://),数据可能在传输过程中被劫持或篡改。
  • 跨站风险:可能受到跨站 WebSocket 劫持攻击。

4.协议的复杂性和兼容性

  • 协议版本问题:虽然 WebSocket 是标准化协议,但与特定技术栈或库的版本不兼容可能导致问题(如旧版 WebSocket 客户端和新版服务器之间的兼容性问题)。

WebSocket 常见应用场景

  1. 实时聊天:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。
  2. 实时协作:用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。
  3. 实时数据推送:用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。
  4. 多人在线游戏:实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。
  5. 在线客服:WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。

WebSocket 握手过程

WebSocket 握手过程是客户端和服务器之间建立 WebSocket 连接的关键步骤。它的过程如下:

1.客户端发起握手请求

客户端首先通过 HTTP 协议向服务器发起 WebSocket 握手请求。这个请求的特点是包含一些特殊的头部字段,要求将连接从 HTTP 协议升级到 WebSocket 协议。

请求头包括:

  • Upgrade: websocket(表示客户端希望升级协议为 WebSocket)。
  • Connection: Upgrade(表明客户端希望升级连接)。
  • Sec-WebSocket-Key: 一个随机生成的 Base64 编码的字符串,用于确保 WebSocket 协议升级的安全性。
  • Sec-WebSocket-Version: 这个字段表示客户端支持的 WebSocket 协议版本(通常为 13)。
  • Origin: (可选)指示请求来自的源,服务器可以使用此字段来判断是否允许建立 WebSocket 连接,防止跨站点攻击。

2.服务器响应握手请求

当服务器收到客户端的握手请求后,如果它支持 WebSocket 协议,并同意升级连接,则会返回一个 HTTP 101 状态码(切换协议)。响应头包含以下字段:

  • HTTP/1.1 101 Switching Protocols: 表示服务器同意协议切换。
  • Upgrade: websocket(表示协议切换为 WebSocket)。
  • Connection: Upgrade(表示连接已经升级)。
  • Sec-WebSocket-Accept: 服务器将 Sec-WebSocket-Key 的值与一个固定的 GUID (258EAFA5-E914-47DA-95CA-C5AB0DC85B11) 拼接,并通过 SHA-1 加密后再进行 Base64 编码,生成这个字段的值。这个过程用于确保握手的安全性,防止恶意请求伪造 WebSocket 协议。

3.WebSocket 连接建立

一旦服务器成功回应了客户端的请求并发送了 Sec-WebSocket-Accept 字段,客户端就可以确认协议切换成功,WebSocket 连接正式建立。这时,客户端和服务器之间的通信将切换到 WebSocket 协议,而不再依赖 HTTP。

4.数据交换

连接建立后,客户端和服务器就可以通过 WebSocket 连接进行双向通信,双方可以随时发送消息,而不需要重新建立连接。

5.连接关闭

连接可以通过以下方式关闭:

  • 客户端或服务器发送关闭帧:在传输完消息后,任一方都可以发起关闭连接的请求,另一方确认后关闭连接。
  • 协议约定:WebSocket 协议定义了一个关闭帧,包含一个状态码,表示关闭连接的原因。

WebSocket 握手的核心在于通过 HTTP 协议的升级请求和响应,成功切换到 WebSocket 协议,然后客户端和服务器通过持久化的双向连接进行高效的数据交换。

WebSocket 事件处理和生命周期

1.onopen:连接成功时触发

onopen 事件在 WebSocket 连接成功建立时触发。这通常意味着客户端与服务器之间的 WebSocket 连接已经完成,双方可以开始交换消息。

作用:在连接建立时执行一些初始化操作(如发送第一个消息,记录日志等)。

let ws = new WebSocket('ws://example.com/socket');
ws.onopen = function(event) {
    console.log('Connection established');
    ws.send('Hello Server');
};

2.onmessage:接收到消息时触发

onmessage 事件在 WebSocket 连接收到来自服务器的消息时触发。事件的参数包含接收到的消息,可以是文本或二进制数据。

作用:用于处理服务器发来的数据。event.data 包含服务器传来的消息内容。

let ws = new WebSocket('ws://example.com/socket');
ws.onmessage = function(event) {
    console.log('Received message:', event.data);
};

3.onclose:连接关闭时触发

onclose 事件在 WebSocket 连接关闭时触发。关闭可以是由客户端或服务器主动发起的。事件参数通常包括关闭的代码和原因。

作用:用于执行清理操作,如更新 UI 状态或重新连接等。

let ws = new WebSocket('ws://example.com/socket');
ws.onclose = function(event) {
    if (event.wasClean) {
        console.log('Closed cleanly');
    } else {
        console.log('Closed with error');
    }
    console.log('Close code:', event.code);
};

4.onerror:发生错误时触发

onerror 事件在 WebSocket 连接出现错误时触发。这个事件通常是在网络错误、协议错误等情况下发生,客户端可以捕获这个事件并进行相应的错误处理。

作用:用于捕获并处理 WebSocket 的错误,可能包括连接失败、数据传输失败等。

let ws = new WebSocket('ws://example.com/socket');
ws.onerror = function(event) {
    console.error('WebSocket error:', event);
};

WebSocket 心跳机制

概述

WebSocket 的心跳机制用于确保客户端和服务器之间的连接稳定,并检测连接是否断开。由于 WebSocket 是基于持久连接的协议,如果一方长时间不活动,可能会被防火墙、代理或其他网络设备视为非活跃连接而断开。心跳机制通过定期发送消息来保持连接活跃。

原理

  • 发送心跳消息:客户端或服务器定期发送特殊的“心跳包”消息,表明连接仍然正常。
  • 接收心跳响应:另一方接收到心跳包后,应返回一个确认消息(可以是固定格式,也可以是直接回复心跳包)。
  • 断开检测:如果在一定时间内未收到预期的心跳响应,说明连接可能已断开,此时可以尝试重新连接。

实现

客户端通常通过 setInterval 定期发送心跳包,并监听服务器的响应。

服务器接收到心跳包后,应返回一个固定的心跳响应(如 "pong")。

应用

心跳机制广泛应用于以下场景:

  • 即时通讯(IM)应用:保持用户在线状态。
  • 在线游戏:保持游戏连接,防止掉线。
  • 实时数据更新:如股票、天气等实时推送数据。
  • 长时间后台连接:如 IoT 设备的数据上传。

配置接入文心一言

1.在百度智能云开放平台中注册成为开发者

百度智能云 API开放平台-API服务商-幂简集成

2.进入百度智能云官网进行登录,点击立即体验

3.进入千帆ModelBuilder,点击左侧的应用接入并且点击创建应用

4.在页面上的应用名称输入自己想要的应用名称和应用描述

5.获取对应的API Key 和 Secret Key

6.配置文心一言ERNIE4.0 API并调用,选择一个想要使用的模型

7.添加依赖

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
</dependency>

8.根据示例代码进行测试

package com.qcby.byspringbootdemo;

import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.util.concurrent.TimeUnit;

class Sample {

    // 密钥
    public static final String API_KEY = "";
    public static final String SECRET_KEY = "";

    // OkHttpClient 配置,设置连接超时和读取超时
    static final OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder()
            .connectTimeout(60, TimeUnit.SECONDS)  // 设置连接超时为60秒
            .readTimeout(60, TimeUnit.SECONDS)     // 设置读取超时为60秒
            .build();

    public static void main(String[] args) throws IOException, JSONException {
        // 定义请求的媒体类型
        MediaType mediaType = MediaType.parse("application/json");

        // 构建请求体,消息内容包含了用户请求
        RequestBody body = RequestBody.create(mediaType, "{\"messages\":["
                + "{\"role\":\"user\",\"content\":\"北京的天气是什么\"}" // 用户输入的消息内容
                + "],"
                + "\"temperature\":0.95,"   // 设置温度参数,控制模型的输出多样性
                + "\"top_p\":0.8,"          // 设置 top_p 参数,控制模型输出的多样性
                + "\"penalty_score\":1,"    // 设置惩罚得分参数,影响模型对重复内容的惩罚
                + "\"enable_system_memory\":false," // 禁用系统内存
                + "\"disable_search\":false,"        // 禁用搜索功能
                + "\"enable_citation\":false}");    // 禁用引用功能

        // 构建 HTTP 请求
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token=" + getAccessToken())
                .method("POST", body)  // 设置请求方法为 POST
                .addHeader("Content-Type", "application/json")  // 设置请求头,表示发送的内容是 JSON 格式
                .build();

        // 发送请求并获取响应
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            // 输出响应内容,打印接口返回的数据
            System.out.println(response.body().string());
        } catch (IOException e) {
            // 捕获 IO 异常(如网络错误、超时等),并打印异常信息
            e.printStackTrace();
        }
    }

    /**
     * 从用户的 API 密钥(AK、SK)生成鉴权签名(Access Token)
     *
     * @return 鉴权签名(Access Token)
     * @throws IOException 如果发生 I/O 异常
     * @throws JSONException 如果发生 JSON 解析异常
     */
    static String getAccessToken() throws IOException, JSONException {
        // 设置请求体的媒体类型为 x-www-form-urlencoded
        MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");

        // 创建请求体,包含 API 的 client_id 和 client_secret
        RequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY
                + "&client_secret=" + SECRET_KEY);

        // 构建请求,使用 POST 方法获取 Access Token
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/oauth/2.0/token") // 请求 URL,获取 Access Token
                .method("POST", body)  // 使用 POST 方法发送请求
                .addHeader("Content-Type", "application/x-www-form-urlencoded") // 请求头
                .build();

        // 发送请求并获取响应
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            // 从响应中解析出 Access Token
            return new JSONObject(response.body().string()).getString("access_token");
        }
    }
}

WebSocket 实现

1.WebSocketConfig 

配置一个 Spring Boot 应用中的 WebSocket 支持,通过使用 ServerEndpointExporter 来实现 WebSocket 的启用

package com.qcby.byspringbootdemo.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();
    }
}

2.WebSocketServer 

使用 @ServerEndpoint 注解声明了 WebSocket 端点,指定了路径 /api/websocket/{sid},其中 {sid} 是一个动态路径参数,代表每个连接的唯一标识。

WebSocket 事件处理:

  • @OnOpen: 连接建立时调用的方法。每当一个新的 WebSocket 连接建立时,执行此方法,并将当前连接的 sidsession 保存到 webSocketSet 中,同时增加在线人数。
    • 发送一个 JSON 格式的 "conn_success" 消息给客户端,表示连接成功。
  • @OnClose: 连接关闭时调用的方法。每当一个 WebSocket 连接关闭时,执行此方法,并从 webSocketSet 中移除该连接,同时减少在线人数。
  • @OnMessage: 收到客户端消息时调用的方法。该方法解析接收到的消息并根据目标用户的 sid 将消息发送给目标客户端。如果目标用户是管理员且管理员不在线,系统会通过 WenXinYiYanUtil 获取自动回复,进行自动响应。
  • @OnError: 发生错误时调用的方法,日志记录错误信息。

消息发送:

  • sendMessage: 该方法用于向客户端发送消息,利用 session.getBasicRemote().sendText() 实现。
  • sendInfo: 该方法用于群发消息,向所有连接的客户端发送自定义的消息。可以根据传入的 sid 进行定向推送。
package com.qcby.byspringbootdemo.server;

import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qcby.byspringbootdemo.util.WenXinYiYanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * WebSocket 服务端
 */
@Component
@Slf4j
@Service
@ServerEndpoint("/api/websocket/{sid}")
public class WebSocketServer {
    //当前在线连接数
    private static int onlineCount = 0;

    //存放每个客户端对应的 WebSocketServer 对象
    private static final CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

    //用户信息
    private Session session;

    //当前用户的 sid
    private String sid = "";

    //JSON解析工具
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        this.sid = sid;
        webSocketSet.add(this); //加入集合
        addOnlineCount(); //在线数加1
        try {
            // 发送 JSON 格式的消息
            String successMessage = "{\"message\": \"conn_success\"}";
            sendMessage(successMessage);
            log.info("有新窗口开始监听: " + sid + ", 当前在线人数为: " + getOnlineCount());
        } catch (IOException e) {
            log.error("WebSocket IO Exception", e);
        }
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this); //从集合中删除
        subOnlineCount(); //在线数减1
        log.info("释放的 sid 为:" + sid);
        log.info("有一连接关闭!当前在线人数为 " + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口 " + sid + " 的信息: " + message);

        String targetSid;
        String msgContent;
        try {
            //解析接收到的 JSON 消息
            Map<String, String> messageMap = objectMapper.readValue(message, Map.class);
            targetSid = messageMap.get("targetSid");
            msgContent = messageMap.get("message");
        } catch (IOException e) {
            log.error("消息解析失败", e);
            return;
        }

        //判断目标用户是否为管理员且管理员不在线
        boolean isTargetAdmin = isAdmin(targetSid);
        if (isTargetAdmin && !isAdminOnline()) {
            log.info("管理员不在线,调用文心一言进行自动回复");
            String wenxinResponse = getWenxinResponse(msgContent);
            if (wenxinResponse != null) {
                log.info("文心一言返回的回复: " + wenxinResponse);
                Map<String, String> responseMap = new HashMap<>();
                responseMap.put("sourceSid", sid);
                responseMap.put("message", wenxinResponse);

                String jsonResponse;
                try {
                    //将回复消息转换为 JSON 格式
                    jsonResponse = objectMapper.writeValueAsString(responseMap);
                } catch (IOException e) {
                    log.error("JSON 序列化失败", e);
                    return;
                }

                //发送自动回复消息给发送方
                try {
                    sendMessage(jsonResponse);
                    log.info("发送自动回复消息: " + jsonResponse);
                } catch (IOException e) {
                    log.error("消息发送失败", e);
                }
                return;
            }
        }


        //如果管理员在线或者不是管理员,按照正常逻辑发送消息
        Map<String, String> responseMap = new HashMap<>();
        responseMap.put("sourceSid", sid);
        responseMap.put("message", msgContent);

        String jsonResponse;
        try {
            //将消息转换为 JSON 格式
            jsonResponse = objectMapper.writeValueAsString(responseMap);
        } catch (IOException e) {
            log.error("JSON 序列化失败", e);
            return;
        }

        //将消息发送给目标 sid
        for (WebSocketServer item : webSocketSet) {
            try {
                if (targetSid.equals(item.sid)) {
                    item.sendMessage(jsonResponse);
                    break;
                }
            } catch (IOException e) {
                log.error("消息发送失败", e);
            }
        }
    }



    /**
     * 判断是否是管理员
     */
    private boolean isAdmin(String sid) {
        return "admin".equals(sid);
    }

    /**
     * 发生错误时调用的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误", error);
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
        log.info("推送消息到窗口 " + sid + ",推送内容: " + message);

        for (WebSocketServer item : webSocketSet) {
            try {
                if (sid == null) {
                    item.sendMessage(message); //推送给所有人
                } else if (item.sid.equals(sid)) {
                    item.sendMessage(message); //推送给指定 sid
                }
            } catch (IOException e) {
                log.error("推送消息失败", e);
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }

    public String getSid() {
        return this.sid;
    }

    private boolean isAdminOnline() {
        for (WebSocketServer item : webSocketSet) {
            if (isAdmin(item.sid)) {
                log.info("管理员已在线: " + item.sid);
                return true;
            }
        }
        log.info("管理员不在线");
        return false;
    }




    private String getWenxinResponse(String query) {
        try {
            //调用WenXinYiYanUtil类的静态方法获取回复
            String wenxinReplyJson = WenXinYiYanUtil.getWenxinReply(query);

            //将文心一言回复的JSON字符串解析为JSONObject
            JSONObject wenxinReplyObj = JSONObject.parseObject(wenxinReplyJson);

            //提取出要展示给用户的回复内容
            String result = wenxinReplyObj.getString("result");

            return result;
        } catch (IOException | JSONException e) {
            log.error("调用文心一言失败", e);
            return null;
        }
    }

}

3.ChatController 

  • @GetMapping("/online-users"):该方法处理 GET 请求,返回当前在线的用户列表,排除管理员。该方法遍历 WebSocketServer 中的所有 WebSocket 连接,检查每个连接的 sid(即用户标识符),如果不是管理员(sid 不是 "admin"),就将该用户的 sid 加入返回的列表 sidList
  • @PostMapping("/send"):该方法处理 POST 请求,用于管理员发送消息给指定用户。该方法接收两个请求参数,sidmessagesid 是目标用户的 sid(即唯一标识符),message 是要发送的消息内容。然后,它调用 WebSocketServer.sendInfo() 方法,将消息推送给目标用户
package com.qcby.byspringbootdemo.controller;

import com.qcby.byspringbootdemo.server.WebSocketServer;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    /**
     * 获取在线用户列表,不包含管理员
     */
    @GetMapping("/online-users")
    public List<String> getOnlineUsers() {
        List<String> sidList = new ArrayList<>();
        for (WebSocketServer server : WebSocketServer.getWebSocketSet()) {
            //排除管理员
            if (!server.getSid().equals("admin")) {
                sidList.add(server.getSid());
            }
        }
        return sidList;
    }

    /**
     * 管理员发送消息给指定用户
     */
    @PostMapping("/send")
    public void sendMessageToUser(@RequestParam String sid, @RequestParam String message) throws IOException {
        WebSocketServer.sendInfo(message, sid);
    }


}

4.WenXinYiYanUtil 

通过使用 OkHttp 客户端与百度的文心一言(Wenxin)API 进行交互,获取基于用户查询的回复

package com.qcby.byspringbootdemo.util;

import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class WenXinYiYanUtil {

    //密钥
    public static final String API_KEY = "";
    public static final String SECRET_KEY = "";

    //OkHttpClient 配置,设置连接超时和读取超时
    static final OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder()
            .connectTimeout(60, TimeUnit.SECONDS)  //设置连接超时为60秒
            .readTimeout(60, TimeUnit.SECONDS)     //设置读取超时为60秒
            .build();

    public static String getWenxinReply(String query) throws IOException, JSONException {
        //定义请求的媒体类型
        MediaType mediaType = MediaType.parse("application/json");

        //构建请求体,消息内容包含了从前端传来的用户请求
        RequestBody body = RequestBody.create(mediaType, "{\"messages\":["
                + "{\"role\":\"user\",\"content\":\"" + query + "\"}"
                + "],"
                + "\"temperature\":0.95,"
                + "\"top_p\":0.8,"
                + "\"penalty_score\":1,"
                + "\"enable_system_memory\":false,"
                + "\"disable_search\":false,"
                + "\"enable_citation\":false}");

        //构建 HTTP 请求
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token=" + getAccessToken())
                .method("POST", body)
                .addHeader("Content-Type", "application/json")
                .build();

        System.out.println("调用文心一言,查询内容: " + query);

        //请求代码
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            String responseBody = response.body().string();
            System.out.println("文心一言返回的内容: " + responseBody);
            return responseBody;
        }
    }

    /**
     * 从用户的 API 密钥(AK、SK)生成鉴权签名(Access Token)
     * @return 鉴权签名(Access Token)
     * @throws IOException 如果发生 I/O 异常
     * @throws JSONException 如果发生 JSON 解析异常
     */
    static String getAccessToken() throws IOException, JSONException {
        //设置请求体的媒体类型
        MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");

        //创建请求体,包含 API 的 client_id 和 client_secret
        RequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY
                + "&client_secret=" + SECRET_KEY);

        //构建请求,使用 POST 方法获取 Access Token
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/oauth/2.0/token") // 请求 URL,获取 Access Token
                .method("POST", body)
                .addHeader("Content-Type", "application/x-www-form-urlencoded")
                .build();

        //发送请求并获取响应
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            //从响应中解析出 Access Token
            return new JSONObject(response.body().string()).getString("access_token");
        }
    }

}

 5.用户端

  • WebSocket 连接:
    • connectWebSocket():初始化一个 WebSocket 连接,连接的 URL 是 ws://localhost:8080/api/websocket/{sid},其中 {sid} 是一个随机生成的用户 ID。
    • onopen:WebSocket 连接成功时,会在控制台打印日志,并在聊天窗口显示连接成功的提示信息。
    • onmessage:接收到来自 WebSocket 服务器的消息时,解析消息并根据来源和目标 SID 显示不同的消息。管理员的消息会显示在左侧,用户的消息显示在右侧。
    • 错误处理:处理 WebSocket 错误和连接关闭的情况。
  • 发送消息:
    • sendMessage():当用户点击 "Send" 按钮时,发送输入框中的消息到 WebSocket 服务器,并将该消息显示在聊天窗口的右侧(代表用户)。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>User - Chat Window</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f4f8;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }
        #chatBox {
            position: fixed;
            bottom: 10px;
            right: 10px;
            width: 400px;
            height: 500px;
            background-color: #ffffff;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            background: linear-gradient(to top right, #f9f9f9, #e9eff7);
            display: flex;
            flex-direction: column;
            max-height: 80vh;
        }
        #chatBox h3 {
            font-size: 20px;
            margin-bottom: 15px;
            color: #333;
            text-align: center;
        }
        #messages {
            flex: 1;
            border: 1px solid #ddd;
            padding: 15px;
            overflow-y: auto;
            background-color: #f9f9f9;
            border-radius: 8px;
            margin-bottom: 15px;
            font-size: 14px;
            color: #333;
            line-height: 1.5;
            box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.1);
        }
        .message {
            padding: 10px;
            margin: 5px 0;
            border-radius: 8px;
            max-width: 80%;
            word-wrap: break-word;
        }
        .message-right {
            background-color: #dcf8c6;
            text-align: right;
            margin-left: auto;
        }
        .message-left {
            background-color: #f1f0f0;
            text-align: left;
            margin-right: auto;
        }
        #inputWrapper {
            display: flex;
            width: 100%;
        }
        #messageInput {
            width: calc(100% - 80px);
            padding: 12px;
            border-radius: 25px;
            border: 1px solid #ccc;
            margin-right: 10px;
            font-size: 16px;
            transition: border-color 0.3s ease;
        }
        #messageInput:focus {
            border-color: #007bff;
            outline: none;
        }
        button {
            padding: 12px 20px;
            border-radius: 25px;
            border: 1px solid #007bff;
            background-color: #007bff;
            color: white;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s ease;
            width: 60px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
        }
        button:hover {
            background-color: #0056b3;
        }
        @media (max-width: 768px) {
            #chatBox {
                width: 100%;
                bottom: 20px;
                padding: 15px;
            }
            #messageInput {
                width: calc(100% - 100px);
            }
            button {
                width: 70px;
                padding: 10px;
            }
        }
    </style>
    <script>
        let websocket;
        const sid = Math.random().toString(36).substring(2, 15);
        const isAdmin = false;

        function connectWebSocket() {
            websocket = new WebSocket("ws://localhost:8080/api/websocket/" + sid);

            websocket.onopen = () => {
                console.log("Connection successful, User ID: " + sid);
                document.getElementById("messages").innerHTML += `<div class="message-left">Connection successful, Your ID is: ${sid}</div>`;
            };

            websocket.onmessage = (event) => {
                try {
                    let data = JSON.parse(event.data);

                    if (data.message === "conn_success") {
                        console.log("Connection successful, Your ID is: " + sid);
                        document.getElementById("messages").innerHTML += `<div class="message-left">Connection successful, Your ID is: ${sid}</div>`;
                        scrollToBottom();
                        return;
                    }

                    const { sourceSid, message, targetSid } = data;

                    if (targetSid === sid || sourceSid === 'admin') {
                        let newMessage = document.createElement("div");
                        newMessage.classList.add(sourceSid === 'admin' ? 'message-left' : 'message-right');
                        newMessage.textContent = message;

                        setTimeout(() => {
                            document.getElementById("messages").appendChild(newMessage);
                            scrollToBottom();
                        }, 0);
                    } else {
                        let newMessage = document.createElement("div");
                        newMessage.classList.add('message-left');
                        newMessage.textContent = `${message}`;
                        document.getElementById("messages").appendChild(newMessage);
                        scrollToBottom();
                    }
                } catch (e) {
                    console.error("Failed to parse message", e);
                }
            };

            websocket.onclose = () => {
                console.log("Connection closed");
            };

            websocket.onerror = (error) => {
                console.error("WebSocket error", error);
            };
        }

        function sendMessage() {
            const message = document.getElementById("messageInput").value;
            const targetSid = "admin";

            if (message.trim() !== "") {
                websocket.send(JSON.stringify({ targetSid, message }));
                document.getElementById("messages").innerHTML += `<div class="message-right">${message}</div>`;
                document.getElementById("messageInput").value = '';
                scrollToBottom();
            }
        }

        function scrollToBottom() {
            const messagesDiv = document.getElementById("messages");
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        connectWebSocket();
    </script>
</head>
<body>
<div id="chatBox">
    <h3>User Chat Window</h3>
    <div id="messages"></div>
    <div id="inputWrapper">
        <input id="messageInput" type="text" placeholder="Enter message">
        <button onclick="sendMessage()">Send</button>
    </div>
</div>
</body>
</html>

6.管理员端

  • WebSocket 连接:

    • 使用 new WebSocket() 创建一个连接,目标地址为 ws://localhost:8080/api/websocket/admin,这里的 admin 是管理员的 ID。
    • onopen 事件:当 WebSocket 连接成功时,控制台打印 "连接成功" 信息。
    • onmessage 事件:每当接收到消息时,如果是 JSON 格式的数据,则解析消息并更新聊天记录;如果是纯文本消息,则通过 handleTextMessage 函数处理。
    • onclose 事件:连接关闭时的处理。
    • onerror 事件:处理 WebSocket 错误。
  • 聊天记录管理:

    • chatHistory:一个对象,用来存储每个用户的聊天记录。
    • 当管理员接收到消息时,根据 sourceSid 将消息保存到对应用户的聊天记录中,并根据当前选择的聊天用户来更新聊天窗口。
  • 消息显示:

    • displayMessages():显示当前用户与选择的聊天对象的所有聊天记录,自动滚动到最新消息。
    • scrollToBottom():滚动聊天窗口到最底部,确保用户始终看到最新的消息。
  • 消息发送:

    • sendMessage():用户输入消息后,点击 "Send" 按钮发送消息。如果没有选择聊天对象,系统会提示用户选择一个聊天对象。
  • 在线用户管理:

    • getOnlineUsers():向后端请求在线用户列表,更新 #onlineUsers 列表。
    • selectUser():当用户点击某个在线用户时,该用户被选中,并开始与该用户进行聊天。
    • notifyUnreadMessage():如果接收到非当前用户的消息,则标记该用户为有未读消息。
    • clearUnreadMessage():当与某个用户进行聊天时,清除该用户的未读消息标记。
  • 本地存储:

    • localStorage 用于保存当前选中的聊天用户,以便在页面重新加载后恢复用户的选择。
  • 恢复选中的用户:

    • restoreSelectedUser():在页面加载时,恢复之前选中的聊天用户,显示其聊天记录,并高亮显示。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin - Chat Window</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            display: flex;
            height: 100vh;
            margin: 0;
            background-color: #f4f7fc;
            color: #333;
        }
        /* 左侧在线用户列表 */
        #onlineUsersContainer {
            width: 250px;
            padding: 20px;
            background-color: #fff;
            border-right: 1px solid #ddd;
            box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
            overflow-y: auto;
        }
        #onlineUsers {
            list-style-type: none;
            padding: 0;
            margin-top: 20px;
        }
        #onlineUsers li {
            padding: 10px;
            cursor: pointer;
            border-radius: 5px;
            transition: background-color 0.3s ease;
        }
        #onlineUsers li:hover {
            background-color: #e9f1fe;
        }
        #onlineUsers li.selected {
            background-color: #d0e7fe;
        }
        /* 右侧聊天窗口 */
        #chatBox {
            flex: 1;
            display: flex;
            flex-direction: column;
            padding: 20px;
            background-color: #fff;
        }
        #messages {
            border: 1px solid #ddd;
            height: 500px;
            overflow-y: scroll;
            margin-bottom: 20px;
            padding: 15px;
            background-color: #f9f9f9;
            border-radius: 10px;
            box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .message {
            padding: 10px;
            margin: 8px 0;
            border-radius: 10px;
            max-width: 80%;
            line-height: 1.6;
            word-wrap: break-word;
        }
        .message-right {
            background-color: #dcf8c6;
            text-align: right;
            margin-left: auto;
        }
        .message-left {
            background-color: #f1f0f0;
            text-align: left;
            margin-right: auto;
        }
        #messageInput {
            width: 80%;
            padding: 12px;
            border-radius: 25px;
            border: 1px solid #ccc;
            margin-right: 10px;
            font-size: 16px;
            transition: border-color 0.3s ease;
        }
        #messageInput:focus {
            border-color: #007bff;
            outline: none;
        }
        button {
            padding: 12px 20px;
            border-radius: 25px;
            border: 1px solid #007bff;
            background-color: #007bff;
            color: white;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s ease;
        }
        button:hover {
            background-color: #0056b3;
        }
        h3 {
            font-size: 18px;
            color: #333;
            margin-bottom: 20px;
        }
        #onlineUsers li.unread {
            font-weight: bold;
            color: red;
        }

        @media (max-width: 768px) {
            #onlineUsersContainer {
                width: 100%;
                padding: 15px;
            }
            #chatBox {
                padding: 15px;
            }
            #messageInput {
                width: calc(100% - 100px);
            }
            button {
                width: 80px;
            }
        }
    </style>
</head>
<body>
<div id="onlineUsersContainer">
    <h3>Online Users</h3>
    <ul id="onlineUsers"></ul>
</div>
<div id="chatBox">
    <h3>Chat Window</h3>
    <div id="messages"></div>
    <div style="display: flex;">
        <input id="messageInput" type="text" placeholder="Enter message">
        <button onclick="sendMessage()">Send</button>
    </div>
</div>

<script>
    let websocket;
    const sid = "admin";
    let currentUserSid = null; // 当前选择聊天的用户SID
    let chatHistory = {}; // 用来存储每个用户的聊天记录

    // 页面加载时初始化
    window.onload = () => {
        connectWebSocket();
        getOnlineUsers(); // 刷新在线用户列表
        restoreSelectedUser(); // 恢复选中的用户
    };

    function connectWebSocket() {
        websocket = new WebSocket("ws://localhost:8080/api/websocket/" + sid);

        websocket.onopen = () => {
            console.log("连接成功,管理员ID:" + sid);
        };

        websocket.onmessage = (event) => {
            try {
                let data;
                if (event.data.startsWith("{") && event.data.endsWith("}")) {
                    data = JSON.parse(event.data);  // 如果是有效的JSON格式,解析它
                } else {
                    // 如果不是有效的JSON格式(比如 "conn_success" 这样的文本消息),处理它
                    console.log("收到非JSON消息:", event.data);
                    handleTextMessage(event.data);  // 处理文本消息
                    return;
                }

                const { sourceSid, message } = data;

                if (sourceSid) {
                    // 如果聊天记录中没有该用户,初始化
                    if (!chatHistory[sourceSid]) {
                        chatHistory[sourceSid] = [];
                    }

                    // 存储收到的消息
                    chatHistory[sourceSid].push({ sender: 'left', message });

                    // 如果是当前聊天用户的消息,更新聊天窗口
                    if (sourceSid === currentUserSid) {
                        displayMessages();
                    } else {
                        // 否则标记该用户为有未读消息
                        notifyUnreadMessage(sourceSid);
                    }
                }
            } catch (e) {
                console.error("消息解析失败", e);
            }
        };

        websocket.onclose = () => {
            console.log("连接关闭");
        };

        websocket.onerror = (error) => {
            console.error("WebSocket错误", error);
        };
    }

    function notifyUnreadMessage(userSid) {
        const userListItems = document.querySelectorAll("#onlineUsers li");
        userListItems.forEach(item => {
            if (item.textContent === userSid) {
                item.classList.add("unread"); // 添加未读消息样式
            }
        });
    }

    // 处理文本消息
    function handleTextMessage(text) {
        // 处理接收到的纯文本消息
        const { sourceSid } = JSON.parse(event.data); // 假设event.data中依然包含sourceSid
        if (sourceSid) {
            chatHistory[sourceSid].push({ sender: 'left', message: text });
            if (sourceSid === currentUserSid) {
                displayMessages();
            } else {
                notifyUnreadMessage(sourceSid);
            }
        }
    }

    // 清除未读消息的标记
    function clearUnreadMessage(userSid) {
        const userListItems = document.querySelectorAll("#onlineUsers li");
        userListItems.forEach(item => {
            if (item.textContent === userSid) {
                item.classList.remove("unread");
            }
        });
    }

    function sendMessage() {
        const message = document.getElementById("messageInput").value;

        if (!currentUserSid) {
            alert("请选择一个用户进行聊天!");
            return;
        }

        if (message.trim() !== "") {
            websocket.send(JSON.stringify({ targetSid: currentUserSid, message }));
            chatHistory[currentUserSid] = chatHistory[currentUserSid] || [];
            chatHistory[currentUserSid].push({ sender: 'right', message });
            document.getElementById("messageInput").value = '';
            displayMessages();
        }
    }

    // 显示当前用户的聊天记录
    function displayMessages() {
        const messagesDiv = document.getElementById("messages");
        messagesDiv.innerHTML = "";

        if (currentUserSid && chatHistory[currentUserSid]) {
            chatHistory[currentUserSid].forEach(msg => {
                const messageDiv = document.createElement("div");
                messageDiv.classList.add("message", msg.sender === 'right' ? "message-right" : "message-left");
                messageDiv.textContent = msg.message;  // 显示解析后的消息内容
                messagesDiv.appendChild(messageDiv);
            });
        }
        scrollToBottom();
    }

    function scrollToBottom() {
        const messagesDiv = document.getElementById("messages");
        messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }

    // 获取在线用户列表(不包括管理员)
    function getOnlineUsers() {
        fetch("/api/chat/online-users")
            .then(response => response.json())
            .then(users => {
                const userList = document.getElementById("onlineUsers");
                userList.innerHTML = ""; // 清空当前列表

                users.forEach(user => {
                    if (user !== "admin") {
                        const li = document.createElement("li");
                        li.textContent = user;
                        li.onclick = () => selectUser(user, li);
                        userList.appendChild(li);
                    }
                });
            });
    }

    // 选择一个用户进行聊天
    function selectUser(user, liElement) {
        // 清除所有选中状态
        const userListItems = document.querySelectorAll("#onlineUsers li");
        userListItems.forEach(item => item.classList.remove("selected"));

        // 高亮显示当前选择的用户
        liElement.classList.add("selected");

        if (currentUserSid !== user) {
            currentUserSid = user;
            // 清除未读消息通知
            clearUnreadMessage(user);
            // 显示与该用户的聊天记录
            displayMessages();
            // 保存当前选中的用户
            localStorage.setItem('selectedUserSid', user);
        }
        scrollToBottom();
    }

    // 从localStorage恢复选中的用户
    function restoreSelectedUser() {
        const savedUserSid = localStorage.getItem('selectedUserSid');
        if (savedUserSid) {
            currentUserSid = savedUserSid;
            const userListItems = document.querySelectorAll("#onlineUsers li");
            userListItems.forEach(item => {
                if (item.textContent === savedUserSid) {
                    item.classList.add("selected");
                }
            });
            displayMessages();
        }
    }
</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开源轮子 - Logback 和 Slf4j

spring boot内置&#xff1a;Logback 文章目录 spring boot内置&#xff1a;Logback一&#xff1a;Logback强在哪&#xff1f;二&#xff1a;简单使用三&#xff1a;把 log4j 转成 logback四&#xff1a;日志门面SLF4J1&#xff1a;什么是SLF4J2&#xff1a;SLF4J 解决了什么痛…

MFC/C++学习系列之简单记录13

MFC/C学习系列之简单记录13 前言memsetList Control代码注意 总结 前言 今天记录一下memset和List control 的使用吧&#xff01; memset memset通常在初始化变量或清空内存区域的时候使用&#xff0c;可以对变量设定特定的值。 使用&#xff1a; 头文件&#xff1a; C&#…

Layui table不使用url属性结合laypage组件实现动态分页

从后台一次性获取所有数据赋值给 Layui table 组件的 data 属性&#xff0c;若数据量大时&#xff0c;很可能会超出浏览器字符串最大长度&#xff0c;导致渲染数据失败。Layui table 结合 laypage 组件实现动态分页可解决此问题。 HTML增加分页组件标签 在table后增加一个用于…

网络方案设计

一、网络方案设计目标 企业网络系统的构成 应用软件 计算平台 物理网络及拓扑结构 网络软件及工具软件 网络互连设备 广域网连接 无论是复杂的&#xff0c;还是简单的计算机网络&#xff0c;都包括了以下几个基本元素 &#xff1a; 应用软件----支持用户完成专门操作的软件。…

QT QWidget 背景颜色 鼠标hover 背景颜色研究

自定义控件 UserAvatarWidget 的样式问题及解决方案 场景描述 我开发了一个继承自 QWidget 的自定义控件 UserAvatarWidget&#xff0c;并在 .ui 文件中引入了该控件。引入代码如下&#xff1a; <item><widget class"UserAvatarWidget" name"userAv…

javaScriptBOM

1.1、BOM概述 1.1.1、BOM简介 BOM&#xff08;browser Object&#xff09;即浏览器对象模型&#xff0c;它提供了独立于内容而与浏览器窗口进行交互的对象&#xff0c;其核心对象是window。 BOM由一系列的对象构成&#xff0c;并且每个对象都提供了很多方法与属性 BOM缺乏标准…

【Lua热更新】上篇

Lua 热更新 - 上篇 下篇链接&#xff1a;【Lua热更新】下篇 文章目录 Lua 热更新 - 上篇一、AssetBundle1.理论2. AB包资源加载 二、Lua 语法1. 简单数据类型2.字符串操作3.运算符4.条件分支语句5.循环语句6.函数7. table数组8.迭代器遍历9.复杂数据类型 - 表9.1字典9.2类9.3…

AJAX与Axios

什么是 AJAX ? AJAX 是异步的 JavaScript 和 XML&#xff08;Asynchronous JavaScript And XML&#xff09;。 简单理解AJAX&#xff1a;是一种客户端与服务器进行网络通信技术&#xff0c;AJAX通常使用XMLHttpRequest 对象来发送请求和接收响应 现代开发中我们通常使用 JS…

1.gitlab 服务器搭建流程

前提条件&#xff1a; 一、服务器硬件水平 搭建gitlab服务器最低配置要求2核4G,低于这个配置的服务器运行效果很差。 gitlab官网&#xff1a;https://about.gitlab.com/ 下载地址&#xff1a;gitlab/gitlab-ce - Packages packages.gitlab.com 本机ubuntu 二、安装依赖 su…

springboot462学生心理压力咨询评判(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装学生心理压力咨询评判软件来发挥其高效地信息处理的作用&am…

git分支管理及策略

Git 的默认分支就是 master。你所作的commit会在master分支上自动移动。 在多次提交操作之后&#xff0c;master分支指向最后那个commit object&#xff08;提交对象链&#xff09;。 Git 的 “master” 分支并特殊&#xff0c;跟其它分支没有区别。 之所以几乎每一个仓库都有…

python中wb有什么用

‘wb’&#xff1a;表示以二进制写方式打开&#xff0c;只能写文件&#xff0c; 如果文件不存在&#xff0c;创建该文件&#xff1b;如果文件已存在&#xff0c;则覆盖写。 Python文件使用“wb”方式打开&#xff0c;写入字符串会报错&#xff0c;因为这种打开方式为&#xff…

Connecting to Oracle 11g Database in Python

# encoding: utf-8 # 版权所有 2024 涂聚文有限公司 # 许可信息查看&#xff1a;言語成了邀功盡責的功臣&#xff0c;還需要行爲每日來值班嗎 # 描述&#xff1a;python -m pip install oracledb # python -m pip install cx_Oracle --upgrade # pip install cx_Oracle # Autho…

学工管理系统-职校信息化管理平台

学工管理系统是一种致力于提升职校管理效率和信息化水平的重要工具。它综合运用了现代信息技术和学工管理理念&#xff0c;为学校提供了全面、科学、高效的管理平台。 学工管理系统在学校管理中发挥着重要的作用。它能够实现学生信息的完整管理&#xff0c;包括学籍、课程、成绩…

MySQL三大日志-Binlog

Binlog简介 Redo Log 是属于InnoDB引擎所特有的日志&#xff0c;而MySQL Server也有自己的日志&#xff0c;即 Binary log&#xff08;二进制日志&#xff09;&#xff0c;简称Binlog。Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志&#xff0c;不会记录SELECT…

【Token】校验、会话技术、登录请求、拦截器【期末实训】实战项目学生和班级管理系统\Day15-后端Web实战(登录认证)\讲义

登录认证 在前面的课程中&#xff0c;我们已经实现了部门管理、员工管理的基本功能&#xff0c;但是大家会发现&#xff0c;我们并没有登录&#xff0c;就直接访问到了Tlias智能学习辅助系统的后台。 这是不安全的&#xff0c;所以我们今天的主题就是登录认证。 最终我们要实现…

物联网:全面概述、架构、应用、仿真工具、挑战和未来方向

中文论文标题&#xff1a;物联网&#xff1a;全面概述、架构、应用、仿真工具、挑战和未来方向 英文论文标题&#xff1a;Internet of Things: a comprehensive overview, architectures, applications, simulation tools, challenges and future directions 作者信息&#x…

实验13 C语言连接和操作MySQL数据库

一、安装MySQL 1、使用包管理器安装MySQL sudo apt update sudo apt install mysql-server2、启动MySQL服务&#xff1a; sudo systemctl start mysql3、检查MySQL服务状态&#xff1a; sudo systemctl status mysql二、安装MySQL开发库 sudo apt-get install libmysqlcli…

将4G太阳能无线监控的视频接入电子监控大屏,要考虑哪些方面?

随着科技的飞速发展&#xff0c;4G太阳能无线监控系统以其独特的优势在远程监控领域脱颖而出。这种系统结合了太阳能供电的环保特性和4G无线传输的便捷性&#xff0c;为各种环境尤其是无电或电网不稳定的地区提供了一种高效、可靠的视频监控解决方案。将这些视频流接入大屏显示…

在uniapp Vue3版本中如何解决webH5网页浏览器跨域的问题

问题复现 uniapp项目在浏览器运行&#xff0c;有可能调用某些接口会出现跨域问题&#xff0c;报错如下图所示&#xff1a; 什么是跨域&#xff1f; 存在跨域问题的原因是因为浏览器的同源策略&#xff0c;也就是说前端无法直接发起跨域请求。同源策略是一个基础的安全策略&a…