實現 WebSocket 接入文心一言

目錄

什么是 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-urlencodedMediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");// 創建請求體,包含 API 的 client_id 和 client_secretRequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY+ "&client_secret=" + SECRET_KEY);// 構建請求,使用 POST 方法獲取 Access TokenRequest 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 Tokenreturn 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 {@Beanpublic 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;//當前用戶的 sidprivate String sid = "";//JSON解析工具private static final ObjectMapper objectMapper = new ObjectMapper();/*** 連接建立成功調用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {this.session = session;this.sid = sid;webSocketSet.add(this); //加入集合addOnlineCount(); //在線數加1try {// 發送 JSON 格式的消息String successMessage = "{\"message\": \"conn_success\"}";sendMessage(successMessage);log.info("有新窗口開始監聽: " + sid + ", 當前在線人數為: " + getOnlineCount());} catch (IOException e) {log.error("WebSocket IO Exception", e);}}/*** 連接關閉調用的方法*/@OnClosepublic void onClose() {webSocketSet.remove(this); //從集合中刪除subOnlineCount(); //在線數減1log.info("釋放的 sid 為:" + sid);log.info("有一連接關閉!當前在線人數為 " + getOnlineCount());}/*** 收到客戶端消息后調用的方法*/@OnMessagepublic 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;}//將消息發送給目標 sidfor (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);}/*** 發生錯誤時調用的方法*/@OnErrorpublic 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字符串解析為JSONObjectJSONObject 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_secretRequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY+ "&client_secret=" + SECRET_KEY);//構建請求,使用 POST 方法獲取 Access TokenRequest 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 Tokenreturn 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; // 當前選擇聊天的用戶SIDlet 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中依然包含sourceSidif (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.pswp.cn/web/63701.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/63701.shtml
英文地址,請注明出處:http://en.pswp.cn/web/63701.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2024.7 XAI 遇見 LLM:可解釋 AI 與大型語言模型之間關系的調查

https://arxiv.org/pdf/2407.15248 問題 Q1&#xff1a;XAI 技術當前如何與 LLMs 集成&#xff1f;Q2&#xff1a;將 LLMs 與 XAI 方法融合的新興趨勢是什么&#xff1f;Q3&#xff1a;當前相關文獻存在哪些差距&#xff0c;哪些領域需要進一步研究&#xff1f; 挑戰 LLMs …

前端滾動錨點(點擊后頁面滾動到指定位置)

三個常用方案&#xff1a; 1.scrollintoView 把調用該方法的元素滾動到屏幕的指定位置&#xff0c;中間&#xff0c;底部&#xff0c;或者頂部 優點&#xff1a;方便&#xff0c;只需要獲取元素然后調用 缺點&#xff1a;不好精確控制&#xff0c;只能讓元素指定滾動到中間&…

前端筆記——大數據量瀏覽器卡頓優化思路

多任務數據量處理卡頓問題 任務分批次 為避免阻塞&#xff0c;可以將 長時間的單一任務 拆分成多個小任務并分批執行。這樣可以在兩次任務之間讓瀏覽器有時間處理渲染、用戶輸入等操作。兩種常見方法&#xff1a; setTimeout 方法&#xff1a; 使用 setTimeout 將任務分段&a…

數智化轉型是什么?

數智化轉型是指企業通過數字化&#xff08;Digitalization&#xff09;和智能化&#xff08;Intelligentization&#xff09;技術的結合&#xff0c;推動業務流程、產品服務、組織管理的全面升級&#xff0c;從而提升效率、增強創新能力&#xff0c;并實現更高價值。相比傳統的…

RIP實驗

要求及分析 路由器上分別配置環回 連接路由器的線路網段為12.1.1.0/24、23.1.1.1.0/24 R1和R3連接的網絡地址分別為192.168.1.0/24/192.168.2.0/24 整個網絡使用RIP達到全網可達 配置 先配置路由器各接口ip和環回和pc ip網關掩碼&#xff08;圖略&#xff09; 進行 RI…

Oracle 中間件 Webcenter Portal服務器環境搭建

環境信息 服務器基本信息 如下表&#xff0c;本次安裝總共使用2臺服務器&#xff0c;具體信息如下&#xff1a; Webcenter1服務器 歸類 SOA服務器 Ip Address 172.xx.xx.xx.xx HostName wcc01.xxxxxx.com Alias wccprd01 Webcenter2服務器 歸類 OSB服務器 Ip Addr…

macOS 配置 vscode 命令行啟動

打開 vscode 使用 cmd shift p 組合快捷鍵&#xff0c;輸入 install 點擊 Install ‘code’ command in PATH Ref https://code.visualstudio.com/docs/setup/mac

3、交換機IP路由功能

每個用例前自己最好先畫個圖&#xff0c;不然容易繞暈&#xff0c;這篇文章寫好久了&#xff0c;自己都覺得有點繞 一、直連路由 如果一個交換機與另一個交換機時直連著的并且他們用來連接的端口屬于同網段&#xff0c;那么這種情況下他們就屬于直連路由。不需要做任何配置便可…

分層架構 IM 系統之多媒體功能設計與實現

現在 IM 系統已經不僅限于文本消息的通訊了&#xff0c;多媒體數據占據越來越多的比重&#xff0c;比如&#xff1a;文件傳輸、語音通話、視頻通話等。 在前面的文章&#xff08;《基于需求分析模型來結構化剖析 IM 系統》&#xff09;中我們分析過&#xff0c;“多媒體消息”…

0.gitlab ubuntu20.04 部署問題解決

安裝依賴&#xff1a; ① sudo apt-get update 出現&#xff1a; 解決方式&#xff1a; 去 /etc/apt/sources.list.d 這個目錄刪除或注釋對應的list文件 第三方軟件的源一般都以list文件的方式放在 /etc/apt/sources.list.d 這個目錄 重新運行sudo apt-get update 安裝…

Next.js v15 - 服務器操作以及調用原理

約定 服務器操作是在服務器上執行的異步函數。它們可以在服務器組件和客戶端組件中調用&#xff0c;用于處理 Next.js 應用程序中的表單提交和數據修改。 服務器操作可以通過 React 的 “use server” 指令定義。你可以將該指令放在 async 函數的頂部以將該函數標記為服務器操…

什么是3DEXPERIENCE SOLIDWORKS,它有哪些角色和功能?

將業界領先的 SOLIDWORKS 3D CAD 解決方案連接到基于單一云端產品開發環境 3DEXPERIENCE 平臺。您的團隊、數據和流程全部連接到一個平臺進行高效的協作工作&#xff0c;從而能快速的做出更好的決策。 目 錄&#xff1a; ★ 1 什么是3DEXPERIENCE SOLIDWORKS ★ 2 3DEXPERIE…

【華為OD-E卷-開心消消樂 100分(python、java、c++、js、c)】

【華為OD-E卷-開心消消樂 100分&#xff08;python、java、c、js、c&#xff09;】 題目 給定一個 N 行 M 列的二維矩陣&#xff0c;矩陣中每個位置的數字取值為 0 或 1。矩陣示例如&#xff1a; 1 1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 現需要將矩陣中所有的 1 進行反轉為 0&#…

[Unity]【圖形渲染】【游戲開發】Shader數學基礎4-更多矢量運算

在計算機圖形學和著色器編程中,矢量運算是核心的數學工具之一。矢量用于描述空間中的位置、方向、速度等各種物理量,并在圖形變換、光照計算、紋理映射等方面起著至關重要的作用。本篇文章將詳細講解矢量和標量之間的乘法與除法、矢量的加法與減法、矢量的模與單位矢量、點積…

【漏洞復現】CVE-2023-37461 Arbitrary File Writing

漏洞信息 NVD - cve-2023-37461 Metersphere is an opensource testing framework. Files uploaded to Metersphere may define a belongType value with a relative path like ../../../../ which may cause metersphere to attempt to overwrite an existing file in the d…

Bcrypt在線密碼加密生成器

具體前往&#xff1a;在線Bcrypt加密工具--使用bcrypt及生成salt的迭代次數強度參數計算生成哈希(摘要)

Django 模板分割及多語言支持案例【需求文檔】-->【實現方案】

Django 模板分割及多語言支持案例 這個案例旨在提供一個清晰的示范&#xff0c;展示如何將復雜的頁面分解為多個可復用的模板組件&#xff0c;使代碼更加模塊化和易于管理。希望這篇案例文章對你有所幫助。 概述 在 Django 項目開發中&#xff0c;使用模板分割和多語言支持能…

wxWidgets使用wxStyledTextCtrl(Scintilla編輯器)的正確姿勢

開發CuteMySQL/CuteSqlite開源客戶端的時候&#xff0c;需要使用Scintilla編輯器&#xff0c;來高亮顯示SQL語句&#xff0c;作為C/C領域最成熟穩定又小巧的開源編輯器&#xff0c;Scintilla提供了強大的功能&#xff0c;wxWidgets對Scintilla進行包裝后的是控件類&#xff1a;…

構建高性能異步任務引擎:FastAPI + Celery + Redis

在現代應用開發中&#xff0c;異步任務處理是一個常見的需求。無論是數據處理、圖像生成&#xff0c;還是復雜的計算任務&#xff0c;異步執行都能顯著提升系統的響應速度和吞吐量。今天&#xff0c;我們將通過一個實際項目&#xff0c;探索如何使用 FastAPI、Celery 和 Redis …

介紹 Html 和 Html 5 的關系與區別

HTML&#xff08;HyperText Markup Language&#xff09;是構建網頁的標準標記語言&#xff0c;而 HTML5 是 HTML 的最新版本&#xff0c;包含了一些新的功能、元素、API 和屬性。HTML5 相對于早期版本的 HTML&#xff08;比如 HTML4&#xff09;有許多重要的改進和變化。以下是…