目录
什么是 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 的区别
特点 | WebSocket | HTTP |
---|---|---|
通信方式 | 双向通信 | 单向请求/响应模式 |
连接类型 | 长连接,连接保持打开 | 默认短连接,每次请求需新建连接 |
实时性 | 高,低延迟 | 中等,轮询或长轮询增加延迟 |
效率 | 数据传输轻量,性能高 | 每次请求头部信息冗余,开销大 |
适用场景 | 实时推送、聊天、游戏、物联网等 | 静态内容加载、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 常见应用场景
- 实时聊天:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。
- 实时协作:用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。
- 实时数据推送:用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。
- 多人在线游戏:实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。
- 在线客服: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 连接建立时,执行此方法,并将当前连接的sid
和session
保存到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 请求,用于管理员发送消息给指定用户。该方法接收两个请求参数,sid
和message
。sid
是目标用户的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>