AI对话交互场景使用WebSocket建立H5客户端和服务端的信息实时双向通信

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

一、为什么需要 WebSocket?

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
在这里插入图片描述
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

二、简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
在这里插入图片描述
其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

在这里插入图片描述

服务端的实现

依赖spring-boot-starter-websocket模块实现WebSocket实时对话交互。

CustomTextWebSocketHandler,扩展的TextWebSocketHandler


import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.util.concurrent.CountDownLatch;/*** 文本处理器** @see org.springframework.web.socket.handler.TextWebSocketHandler*/
@Slf4j
public class CustomTextWebSocketHandler extends TextWebSocketHandler {/*** 第三方身份,消息身份*/private String thirdPartyId;/*** 回复消息内容*/private String replyContent;private StringBuilder replyContentBuilder;/*** 完成信号*/private final CountDownLatch doneSignal;public CustomTextWebSocketHandler(CountDownLatch doneSignal) {this.doneSignal = doneSignal;}public String getThirdPartyId() {return thirdPartyId;}public String getReplyContent() {return replyContent;}@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {log.info("connection established, session={}", session);replyContentBuilder = new StringBuilder(16);
//        super.afterConnectionEstablished(session);}@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {super.handleMessage(session, message);}/*** 消息已接收完毕("stop")*/private static final String MESSAGE_DONE = "[DONE]";@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//        super.handleTextMessage(session, message);String payload = message.getPayload();log.info("payload={}", payload);OpenAiReplyResponse replyResponse = Jsons.fromJson(payload, OpenAiReplyResponse.class);if (replyResponse != null && replyResponse.isSuccess()) {String msg = replyResponse.getMsg();if (Strings.isEmpty(msg)) {return;} else if (msg.startsWith("【超出最大单次回复字数】")) {// {"msg":"【超出最大单次回复字数】该提示由GPT官方返回,非我司限制,请缩减回复字数","code":1,// "extParam":"{\"chatId\":\"10056:8889007174\",\"requestId\":\"b6af5830a5a64fa8a4ca9451d7cb5f6f\",\"bizId\":\"\"}",// "id":"chatcmpl-7LThw6J9KmBUOcwK1SSOvdBP2vK9w"}return;} else if (msg.startsWith("发送内容包含敏感词")) {// {"msg":"发送内容包含敏感词,请修改后重试。不合规汇如下:炸弹","code":1,// "extParam":"{\"chatId\":\"10024:8889006970\",\"requestId\":\"828068d945c8415d8f32598ef6ef4ad6\",\"bizId\":\"430\"}",// "id":"4d4106c3-f7d4-4393-8cce-a32766d43f8b"}matchSensitiveWords = msg;// 请求完成doneSignal.countDown();return;} else if (MESSAGE_DONE.equals(msg)) {// 消息已接收完毕replyContent = replyContentBuilder.toString();thirdPartyId = replyResponse.getId();// 请求完成doneSignal.countDown();log.info("replyContent={}", replyContent);return;}replyContentBuilder.append(msg);}}@Overrideprotected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {super.handlePongMessage(session, message);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {replyContentBuilder = null;log.info("handle transport error, session={}", session, exception);doneSignal.countDown();
//        super.handleTransportError(session, exception);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {replyContentBuilder = null;log.info("connection closed, session={}, status={}", session, status);if (status == CloseStatus.NORMAL) {log.error("connection closed fail, session={}, status={}", session, status);}doneSignal.countDown();
//        super.afterConnectionClosed(session, status);}
}

OpenAiHandler


/*** OpenAI处理器*/
public interface OpenAiHandler<Req, Rsp> {/*** 请求前置处理** @param req 入参*/default void beforeRequest(Req req) {//}/*** 响应后置处理** @param req 入参* @param rsp 出参*/default void afterResponse(Req req, Rsp rsp) {//}
}

OpenAiService


/*** OpenAI服务* <pre>* API reference introduction* https://platform.openai.com/docs/api-reference/introduction* </pre>*/
public interface OpenAiService<Req, Rsp> extends OpenAiHandler<Req, Rsp> {/*** 补全指令** @param req 入参* @return 出参*/default Rsp completions(Req req) {beforeRequest(req);Rsp rsp = doCompletions(req);afterResponse(req, rsp);return rsp;}/*** 操作补全指令** @param req 入参* @return 出参*/Rsp doCompletions(Req req);
}

OpenAiServiceImpl


import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketClient;import javax.annotation.Nullable;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;/*** OpenAI服务实现*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OpenAiProperties.class)
@Service("openAiService")
public class OpenAiServiceImpl implements OpenAiService<CompletionReq, CompletionRsp> {private final OpenAiProperties properties;/*** 套接字客户端*/private final WebSocketClient webSocketClient;/*** 模型请求记录服务*/private final ModelRequestRecordService modelRequestRecordService;private static final String THREAD_NAME_PREFIX = "gpt.openai";public OpenAiServiceImpl(OpenAiProperties properties,ModelRequestRecordService modelRequestRecordService) {this.properties = properties;this.modelRequestRecordService = modelRequestRecordService;webSocketClient = WebSocketUtil.applyWebSocketClient(THREAD_NAME_PREFIX);log.info("create OpenAiServiceImpl instance");}@Overridepublic void beforeRequest(CompletionReq req) {// 请求身份if (Strings.isEmpty(req.getRequestId())) {req.setRequestId(UuidUtil.getUuid());}}@Overridepublic void afterResponse(CompletionReq req, CompletionRsp rsp) {if (rsp == null || Strings.isEmpty(rsp.getReplyContent())) {return;}// 三方敏感词检测String matchSensitiveWords = rsp.getMatchSensitiveWords();if (Strings.isNotEmpty(matchSensitiveWords)) {// 敏感词命中rsp.setMatchSensitiveWords(matchSensitiveWords);return;}// 阶段任务耗时统计StopWatch stopWatch = new StopWatch(req.getRequestId());try {// 敏感词检测stopWatch.start("checkSensitiveWord");String replyContent = rsp.getReplyContent();
//            ApiResult<String> apiResult = checkMsg(replyContent, false);
//            stopWatch.stop();
//            if (!apiResult.isSuccess() && Strings.isNotEmpty(apiResult.getData())) {
//                // 敏感词命中
//                rsp.setMatchSensitiveWords(apiResult.getData());
//                return;
//            }// 记录落库stopWatch.start("saveModelRequestRecord");ModelRequestRecord entity = applyModelRequestRecord(req, rsp);modelRequestRecordService.save(entity);} finally {if (stopWatch.isRunning()) {stopWatch.stop();}log.info("afterResponse execute time, {}", stopWatch);}}private static ModelRequestRecord applyModelRequestRecord(CompletionReq req, CompletionRsp rsp) {Long orgId = req.getOrgId();Long userId = req.getUserId();String chatId = applyChatId(orgId, userId);return new ModelRequestRecord().setOrgId(orgId).setUserId(userId).setModelType(req.getModelType()).setRequestId(req.getRequestId()).setBizId(req.getBizId()).setChatId(chatId).setThirdPartyId(rsp.getThirdPartyId()).setInputMessage(req.getMessage()).setReplyContent(rsp.getReplyContent());}private static String applyChatId(Long orgId, Long userId) {return orgId + ":" + userId;}private static String applySessionId(String appId, String chatId) {return appId + '_' + chatId;}private static final String URI_TEMPLATE = "wss://socket.******.com/websocket/{sessionId}";@Nullable@Overridepublic CompletionRsp doCompletions(CompletionReq req) {// 阶段任务耗时统计StopWatch stopWatch = new StopWatch(req.getRequestId());stopWatch.start("doHandshake");// 闭锁,相当于一扇门(同步工具类)CountDownLatch doneSignal = new CountDownLatch(1);CustomTextWebSocketHandler webSocketHandler = new CustomTextWebSocketHandler(doneSignal);String chatId = applyChatId(req.getOrgId(), req.getUserId());String sessionId = applySessionId(properties.getAppId(), chatId);ListenableFuture<WebSocketSession> listenableFuture = webSocketClient.doHandshake(webSocketHandler, URI_TEMPLATE, sessionId);stopWatch.stop();stopWatch.start("getWebSocketSession");long connectionTimeout = properties.getConnectionTimeout().getSeconds();try (WebSocketSession webSocketSession = listenableFuture.get(connectionTimeout, TimeUnit.SECONDS)) {stopWatch.stop();stopWatch.start("sendMessage");OpenAiParam param = applyParam(chatId, req);webSocketSession.sendMessage(new TextMessage(Jsons.toJson(param)));long requestTimeout = properties.getRequestTimeout().getSeconds();// wait for all to finishboolean await = doneSignal.await(requestTimeout, TimeUnit.SECONDS);if (!await) {log.error("await doneSignal fail, req={}", req);}String replyContent = webSocketHandler.getReplyContent();String matchSensitiveWords = webSocketHandler.getMatchSensitiveWords();if (Strings.isEmpty(replyContent) && Strings.isEmpty(matchSensitiveWords)) {// 消息回复异常return null;}String delimiters = properties.getDelimiters();replyContent = StrUtil.replaceFirst(replyContent, delimiters, "");replyContent = StrUtil.replaceLast(replyContent, delimiters, "");String thirdPartyId = webSocketHandler.getThirdPartyId();return new CompletionRsp().setThirdPartyId(thirdPartyId).setReplyContent(replyContent).setMatchSensitiveWords(matchSensitiveWords);} catch (InterruptedException | ExecutionException | TimeoutException e) {log.error("get WebSocketSession fail, req={}", req, e);} catch (IOException e) {log.error("sendMessage fail, req={}", req, e);} finally {if (stopWatch.isRunning()) {stopWatch.stop();}log.info("doCompletions execute time, {}", stopWatch);}return null;}private static final int MIN_TOKENS = 11;/*** 限制单次最大回复单词数(tokens)*/private static int applyMaxTokens(int reqMaxTokens, int maxTokensConfig) {if (reqMaxTokens < MIN_TOKENS || maxTokensConfig < reqMaxTokens) {return maxTokensConfig;}return reqMaxTokens;}private OpenAiParam applyParam(String chatId, CompletionReq req) {OpenAiDataExtParam extParam = new OpenAiDataExtParam().setChatId(chatId).setRequestId(req.getRequestId()).setBizId(req.getBizId());// 提示String prompt = req.getPrompt();// 分隔符String delimiters = properties.getDelimiters();String message = prompt + delimiters + req.getMessage() + delimiters;int maxTokens = applyMaxTokens(req.getMaxTokens(), properties.getMaxTokens());OpenAiData data = new OpenAiData().setMsg(message).setContext(properties.getContext()).setLimitTokens(maxTokens).setExtParam(extParam);String sign = OpenAiUtil.applySign(message, properties.getSecret());return new OpenAiParam().setData(data).setSign(sign);}
}

WebSocketUtil


import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;/*** WebSocket辅助方法*/
public final class WebSocketUtil {/*** 创建一个新的WebSocket客户端*/public static WebSocketClient applyWebSocketClient(String threadNamePrefix) {StandardWebSocketClient webSocketClient = new StandardWebSocketClient();int cpuNum = Runtime.getRuntime().availableProcessors();ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();taskExecutor.setCorePoolSize(cpuNum);taskExecutor.setMaxPoolSize(200);taskExecutor.setDaemon(true);if (StringUtils.hasText(threadNamePrefix)) {taskExecutor.setThreadNamePrefix(threadNamePrefix);} else {taskExecutor.setThreadNamePrefix("gpt.web.socket");}taskExecutor.initialize();webSocketClient.setTaskExecutor(taskExecutor);return webSocketClient;}
}

OpenAiUtil


import org.springframework.util.DigestUtils;import java.nio.charset.StandardCharsets;/*** OpenAi辅助方法*/
public final class OpenAiUtil {/*** 对消息内容进行md5加密** @param message 消息内容* @param secret 加签密钥* @return 十六进制加密后的消息内容*/public static String applySign(String message, String secret) {String data = message + secret;byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);return DigestUtils.md5DigestAsHex(dataBytes);}
}

参考资料

  • WebSocket - 维基百科
  • WebSocket 教程 - 阮一峰
  • 使用WebSocket - 廖雪峰
  • WebSocket Support - Spring Framework
  • Messaging WebSockets - Spring Boot
  • Create WebSocket Endpoints Using @ServerEndpoint - “How-to” Guides - Spring Boot

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WorkPlus AI助理 | 将企业业务场景与ChatGPT结合

近年来&#xff0c;人工智能成为了企业数字化转型的热门话题&#xff0c;作为被训练的语言模型&#xff0c;ChatGPT具备模拟对话、回答问题、写代码、写小说、进行线上内容创作的能力&#xff0c;还能根据聊天的上下文进行互动。作为一款新兴的人工智能应用程序&#xff0c;对于…

用Python比较图片的不同

准备两张不同的图片 原图 修改后&#xff08;在左下角增加了文字&#xff09; 比较不同 使用PIL&#xff08;Pillow library&#xff09;库 安装 pip install pillow&#xff0c;然后直接用其中的ImageChops函数 from PIL import Image from PIL import ImageChops def comp…

对比两张图片的相似度

&#x1f468;‍&#x1f4bb;个人简介&#xff1a; 深度学习图像领域工作者 &#x1f389;总结链接&#xff1a; 链接中主要是个人工作的总结&#xff0c;每个链接都是一些常用demo&#xff0c;代码直接复制运行即可。包括&#xff1a; &am…

[274]用python对比两张图片的不同

from PIL import Image from PIL import ImageChops def compare_images(path_one, path_two, diff_save_location):"""比较图片&#xff0c;如果有不同则生成展示不同的图片参数一: path_one: 第一张图片的路径参数二: path_two: 第二张图片的路径参数三: diff…

怎么判断两张图片是否完全相同,通过读取图片内容进行对比

ep1&#xff1a; ep2&#xff1a; 实现原理&#xff1a; 通过读取图片&#xff0c;把图片转为base64后进行对比即可达到目的。 以下是图片转base64的方法&#xff1a; public string GetBase64StringByImage(Image img){string base64buffer string.Empty;try{if (img ! nul…

Python如何比较两张图片的相似度

前言 本文是该专栏的第21篇,后面会持续分享python的各种干货知识,值得关注。 工作上,可能会需要你对两张图片进行相似度比较。比如现在的图片验证码,需要你对两张图片进行比较,找出图中存在相似特征的地方或动作;再或是在做电商项目的时候,需要你对商品主图进行相似度比…

chatgpt赋能python:Python图片找不同的SEO文章

Python 图片找不同的SEO文章 在网上&#xff0c;图片找不同游戏是一种非常受欢迎的娱乐方式。但是&#xff0c;这些游戏经常需要手动比对两张图片&#xff0c;这是一项费时费力的任务。那么&#xff0c;有没有一种自动化的方法来找到这些不同之处呢&#xff1f; 答案是肯定的…

如何判断两张图片是否类似

如何判断两张图是否相似&#xff1f; 查到了很多算法&#xff0c;流程都是“特征提取”&#xff0c;“特征对比”。以下列出了三个常见算法的浅显的介绍&#xff0c; 平均哈希算法 平均哈希算法是三种Hash算法中最简单的一种&#xff0c;它通过下面几个步骤来获得图片的Hash值…

【图像】搜索相同,或者相似照片

目录 1. 查找完全相同的一对张照片 2. 查找相似照片&#xff0c; 1. 查找完全相同的一对张照片 利用MD5&#xff0c;变换找到两张一模一样的图片。 import cv2 import numpy as np import osimport json import os from hashlib import md5def getmd5(image_path, md5_path):…

go 图片相似 查找两张图片不同的部分 找出两幅图片中的不同处

golang Image similarity comparison 目前网上找了很多的 图片相似 查找两张图片不同的部分&#xff0c;实现图像比较和标记以显示不同 &#xff0c;很多都是python写的&#xff0c;没有找到go语言写的&#xff0c;所以想自己写一个 图片A 为参照物&#xff0c;去寻找图片B 的…

元宇宙是个什么样的概念?

什么是元宇宙&#xff1f; 百度百科上提到&#xff1a; 元宇宙&#xff08;Metaverse&#xff09;&#xff0c;是人类运用数字技术构建的&#xff0c;由现实世界映射或超越现实世界&#xff0c;可与现实世界交互的虚拟世界&#xff0c;具备新型社会体系的数字生活空间。 元宇…

最全元宇宙概念分析!元宇宙为何发展于区块链?

元宇宙&#xff0c;Web3 时代最新热词&#xff0c;和 NFT、DAO 等新晋热门概念一起在 2021 年横空出世。这一概念最早诞生于 1992 年的科幻小说《雪崩》&#xff0c;小说中描绘了一个庞大的虚拟现实世界&#xff0c;人们用数字化身来控制&#xff0c;并相互竞争以提高自己的地位…

chatgpt赋能python:Python期末考:如何顺利通过?

Python期末考&#xff1a;如何顺利通过&#xff1f; Python是一门广受欢迎的编程语言&#xff0c;无论是初学者还是有经验的工程师&#xff0c;都会在其职业生涯中使用Python。在学术领域&#xff0c;Python也被广泛应用于数据分析、人工智能和机器学习等方面。但是&#xff0…

【电商系列】shopee的数据获取

在Amazon&#xff0c;Aliexpress之后&#xff0c;又一个海外电商出现在我的视野里——shopee&#xff0c;在东南亚很火的电商平台。 这战略布局都到南美跟欧洲了 这网站有意思的是啊&#xff0c;每个国家的商品虽然大同小异&#xff0c;但是也能凸显各个国家的风格的&#xff0…

分享4点选品思路,电商大牛都在用

Tiktok选品数据分析是很多跨境电商商家都需要解决的首要问题。如何选品才能提高TikTok变现率&#xff1f;商家选品时需要结合实际数据进行分析&#xff0c;不能一概而论。本文将和大家谈论三个问题。 选品思路 选品方法 选品数据哪里找&#xff1f; 一、选品思路 选品是tiktok小…

电商平台OnBuy选品技巧分享一二

OnBuy是这两年发展较快的蓝海电商平台&#xff0c;是跨境电商人可以选择的一个优质电商平台。今天我们小编就给大家分享一下OnBuy选品技巧以及方法&#xff0c;希望对大家有用。 OnBuy热销类目 1、 健康(防护用品) 2、 美妆护肤 3、 多媒体 4、 玩具 5、 宠物 6、 婴儿用品 …

利用Tushare获取A股所有股票代码

Tusahre注册链接 https://tushare.pro/register?reg365850 import os import tushare as ts import pandas import datetimetoken 自己的 #可以登录文章首的链接注册获取 pro ts.pro_api(token) dateToday datetime.datetime.today().strftime(%Y%m%d)def GetList():try:d…

chatgpt赋能python:Python语言中的语句输入方法

Python语言中的语句输入方法 作为一门广泛应用于科学计算和计算机编程领域的编程语言&#xff0c;Python以其易读易写、简洁明了的语法特点&#xff0c;广受程序员和科学家的喜爱。本文将重点介绍Python中的语句输入方法&#xff0c;包括基本输入方法、文件读取、网络输入和交…

10款爆火且实用的AIGC工具大盘点

大家好。我是不知名设计师 l1m0_&#xff0c;今天分享内容为&#xff1a;10款爆火且实用的AIGC工具。文中我会跟大家针对10款不同功能优势的AI工具向各位朋友进行介绍&#xff0c;对AI创作感兴趣的朋友一定不能错过&#xff0c;一起来看看吧。 人工智能&#xff08;AI&#xff…

欧洲希望WhatsApp和苹果的iMessage能够开放并共同努力

欧洲将很快迫使苹果、Facebook的Meta和谷歌等消息"守门人"&#xff0c;以确保在用户需要时&#xff0c;消息可以与小公司合作。 欧盟一夜之间通过其新的《数字市场法》&#xff08;DMA&#xff09;使自己成为世界上最严格的美国科技公司监管机构&#xff0c;欧洲政界…