刷新页面未断开Solidjs与Golang后端的SSE连接问题求助
问题分析与解决方案
问题现象
开发的SSE API首次运行正常,但页面刷新后,需多次调用POST接口才能触发前端接收SSE推送;发送次数过多会出现“连接已关闭”错误,之后恢复正常,直到再次刷新页面/关闭标签页。调试发现服务器端ctx.Done()逻辑未触发,推测页面刷新时客户端或服务器未正确关闭SSE连接,导致旧连接残留阻塞消息推送。
前端代码问题排查与修复
当前代码存在以下问题:
createEffect每次触发时直接创建新EventSource,未先关闭旧连接,导致多连接残留onCleanup可能因页面刷新时机问题未及时执行,无法保证连接关闭EventSource的错误处理逻辑不够严谨
修复后的前端代码
const [eventSource, setEventSource] = createSignal(undefined); // 监听页面卸载事件,主动关闭连接 useEffect(() => { const handleUnload = () => { eventSource()?.close(); }; window.addEventListener('beforeunload', handleUnload); return () => { window.removeEventListener('beforeunload', handleUnload); }; }, []); createEffect(() => { // 先关闭旧连接再创建新连接 const oldSource = eventSource(); if (oldSource) { oldSource.close(); console.log("Closed old connection"); } const newSource = new EventSource(url); setEventSource(newSource); console.log("Connected to SSE"); newSource.onmessage = (event) => { // 处理接收的数据 }; newSource.onerror = (event) => { console.error(`SSE Connection Error: ${event}`); newSource.close(); setEventSource(undefined); }; newSource.onopen = () => { console.log("SSE Connection Established"); }; }); onCleanup(() => { console.log("Cleanup: Closing SSE Connection"); eventSource()?.close(); });
前端关键修复点
- 创建新连接前关闭旧连接:避免同一页面存在多个SSE连接,导致消息被分发到已失效的旧连接
- 监听
beforeunload事件:在页面刷新、关闭或跳转时主动触发连接关闭,确保客户端及时通知服务器 - 严谨的错误处理:连接出错时立即关闭并清空引用,防止无效连接残留
后端代码问题排查与修复
当前后端未设置连接存活检测,导致客户端断开后服务器无法及时感知,旧连接持续占用dataChannel,新消息被阻塞在无效连接中。
修复后的后端代码
func sseFunction(appCtx *fiber.Ctx, dataChannel chan map[string]any) error { setHeader(appCtx) // 设置TCP KeepAlive,帮助服务器检测死连接 appCtx.Set(fiber.HeaderConnection, "keep-alive") appCtx.Set(fiber.HeaderKeepAlive, "timeout=5, max=100") ctx := appCtx.Context() // 创建心跳定时器,每30秒发送一次空心跳 ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() ctx.SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { log.Println("SSE Connection Opened") defer log.Println("SSE Connection Closed") for { select { case message := <-dataChannel: jsonString, err := json.Marshal(message) if err != nil { log.Printf("Marshal error: %v", err) continue } if _, err := fmt.Fprintf(w, "data: %s\n\n", jsonString); err != nil { log.Printf("Write message error: %v", err) return } if err := w.Flush(); err != nil { log.Printf("Flush error, closing connection: %v", err) return } case <-ticker.C: // 发送心跳消息,维持连接并检测存活 if _, err := fmt.Fprintf(w, "data: \n\n"); err != nil { log.Printf("Heartbeat write error: %v", err) return } if err := w.Flush(); err != nil { log.Printf("Heartbeat flush error, closing connection: %v", err) return } case <-ctx.Done(): log.Println("Client closed connection via ctx.Done()") return } } })) return nil }
后端关键修复点
- 启用TCP KeepAlive:通过响应头设置连接存活参数,让TCP层主动检测死连接
- 添加SSE心跳机制:定期发送空消息,既能维持连接,又能在客户端断开时通过
Flush()错误及时感知并关闭连接 - 增强错误处理:对消息写入、刷新操作的错误直接返回,确保无效连接被及时清理
核心原理说明
页面刷新时,浏览器会断开旧的HTTP连接,但TCP连接可能存在延迟关闭的情况。通过前端主动调用EventSource.close()和后端心跳检测,能确保旧连接被及时识别并清理,避免dataChannel的消息被分发到无效连接,从而解决多次POST才触发推送的问题。
内容的提问来源于stack exchange,提问作者danialsaufi
相关产品推荐
相关产品推荐

