背景
第一次写文章啊,先说明一些背景,up主最近在套壳openai做个chatgpt的小网站,前后端自己做,然后整合一些自己设计的业务,在对话的需要用到用sse,至于sse是个什么东西,websocket知道吧,它是相当于websocket的精简版,被动接收服务器推送过来的数据;
现象
这里主要是写我遇到的问题过程,我的一些思考过程;
当时部署好前后端到腾信云发现有个问题,就是前端sse每一分钟自动超时重连,下面先放下片段代码;
前端接收部分
// 前端建立
const token = getUserInfo()?.token;
const source = new EventSource(`/sse/subscribe?Authorization=${token}`,{withCredentials:true});
setBtnLoading(true)
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
console.log("建立连接。。。");
setStatus(true);
submitClick(source)
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
if (true) {
setRawData(pre=>{
let copyObj:ConversationDetail = JSON.parse(JSON.stringify(pre));
// console.log(copyObj)
const chatMessage: ChatMessage = copyObj.list[copyObj.list.length - 1];
chatMessage.content = chatMessage.content + msg;
copyObj.list[copyObj.list.length - 1] = chatMessage;
return copyObj;
})
}
});
/**
* 如果发生通信错误(比如连接中断),就会触发error事件
* 另一种写法:source.onerror = function (event) {}
*/
source.addEventListener('error', function (e) {
message.error("连接异常")
if (e.readyState === EventSource.CLOSED) {
// console.log("连接关闭");
} else {
// console.log(e);
}
closeSse(source)
}, false);
后端部分代码
// controller部分
@Value("${sse.timeout:600}")
private Integer sseTimeout;
@GetMapping(path = "sse/subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(String Authorization) {
SysUserDto user = securityService.getUserInfo(Authorization);
MAssert.notNull(user,"未登录");
return SseEmitterConnections.connect(Authorization,sseTimeout);
}
// sse工具类
public static SseEmitter connect(String key){
return connect(key,600);
}
public static SseEmitter connect(String key,int sseTimeout) {
try {
// 设置超时时间,0表示不过期。默认30秒
SseEmitter sseEmitter = new SseEmitter(sseTimeout * 1000L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(key));
sseEmitter.onError(errorCallBack(key));
sseEmitter.onTimeout(timeoutCallBack(key));
sseEmitterMap.put(key, sseEmitter);
// 数量+1
count.getAndIncrement();
log.info("================ 建立连接 Key为:{}", key);
return sseEmitter;
} catch (Exception e) {
log.info("创建新的SSE连接异常,当前连接Key为:{}", key);
}
return null;
}
这样一看好像没什么问题,其中后端的sseTimeout我设置600秒超时;但是问题来了,前端每一分钟会自己跳进error的回调函数,然后自己重连;
一开始我觉得应该是nginx代理有问题,因为sse是走nginx的代理,有可能跟nginx里面的keepalive_timeout,proxy_read_timeout,proxy_send_timeout有关系,后面我就根据一番百度和chatgpt,把nginx参数配置如下
location /sse {
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 500;
proxy_send_timeout 500;
keepalive_timeout 500;
proxy_pass http://localhost:9100;
}
这个配置大概就是500s超时,nginx -s reload之后,前端还是一分钟就超时断开,不可能!
后面我就觉得是不是keepalive_timeout,proxy_read_timeout,proxy_send_timeout不生效呢,我就把500全部调成30,然后sse前端30秒就超时了,证明这几个参数是生效的,是不是有什么更上级的东西限制它一分钟就超时呢?防火墙?还是说ssl_session_timeout?
为了更好的排查问题,先把后端9100端口在腾讯云开放出来,然后sse直接连接9100端口,实时上sse还是一分钟超时了,那就证明不是nginx问题。
只能用排除法了,现在排除的是java后端配置问题(java后端是配置600秒超时,本地机子也测试过没问题),排出nginx问题(因为直连后端也是1分钟超时),现在我想到问题就是防火墙了。
但是后面我又百度和gpt了一下,好像防火墙没有方面特定限制,而且它tcp超时默认两个小时,后面我想了一下,会不会是前端代码本来就是有问题呢?我用了crul http://ip:port/sse/subscribe?Authorization=xxx 测试了一下,结果它没有一分钟超时,而是10分钟超时,这个证明不是防火墙是没问题的,也证明服务器的设置是正确的。
似乎种种问题指向前端代码有问题,于是乎我又做了实验,在本地机子启动后端,然后前端直连到本地后端服务,这次惊喜来了,sse居然正常(10分钟才超时重连),这次确实把我愣住了,各种矛盾无法解释无法找到突破口;
再把问题屡一下(正常情况是10分钟超时):
1、前端连接远程sse服务,一分钟超时;
2、curl连接远程sse服务,10分钟超时;
3、前端连接本地sse服务,10分钟超时;
其中第2点证明远程服务和防火墙的设置都没有问题,第3证明前端代码和后端代码都没问题;
然后不了了之几天后,我意外的关掉了本机的vpn,结果发现一切正常,居然是本机的vpn搞的鬼,这么一来似乎都解释的清楚;点3说明前后端代码没问题,点2说明腾讯云没问题,那就证明本机通往服务器的链路出了问题,至于为什么前端连接本地sse服务也正常,只能说明内部的网络它不走vpn。
这个问题真的是搞的心力交瘁,完全找不到突破口,碰了所有壁,各种现象都是自我矛盾,如果不是意外关掉vpn我估计也无法发现问题。
第一次写文章,记录一下自己的坑和思考过程,另外网站的地址是 https://www.crmei.space