起源
故事的起源是我有一个需求, 我需要做一个类似于 GPT 那样打字机效果的接口, 在查找资料的过程中我看到了这篇文章 , 由此我了解到了 SSE
此时我对 SSE 的理解就是获取到HttpServletResponse
的writer
然后写数据即可, 也没有做过什么复杂功能, 这个理解的简短代码如下:
res.setContentType("text/event-stream;charset=UTF-8");
res.getWriter().write("data: " + data + "\n\n"); // 这个 data: 不能删除
res.getWriter().flush();
后来面试中被问到了多个服务, sse 怎么传递数据, 一开始没有理解这个需求, 或者说是不理解这个需求有什么难点, 当时只是简单的认为多个服务就直接互相传HttpServletResponse
就行. 但是经过一系列经历后发现, 这个需求并不简单, 最起码HttpServletResponse
是不可序列化的, 所以他也无法在多个微服务之间传递.
再次遇到
在实习的过程中, 我遇到了一个需求, 原操作是通过流式输出向前端发送一个 xlsx 表格, 我需要使用feign
对接这个接口. 经过查询资料后我获得了一个解决办法, 即: 先写入到 Feign 的 feign.Response
, 调用方解析后再写入自己的HttpServletResponse
. 由此这个需求算是解决了一半, 不过比较可惜, 这个接口后来被砍掉了.
从此我对于 SSE 在多服务之间的理解变成了: 通过写入中间的一个response
然后进行转换.
解决了但并没有完全解决
不过就在最近我又遇到这个问题了, 这次我遇到的问题以及我想要实现的功能如下:
我希望前端请求服务 A, 服务 A 通过Dubbo
调用服务 B, 服务 B 可以直接通过HttpServletResponse
向前端推送数据.
根据资料搜索, 得知Dubbo
中有一个RpcContext
可以满足我的需求, 但是实际操作上似乎并不起作用, 这里
是我的一些尝试以及回复. 于是在经过大量尝试后我决定改变架构为如下:
这样就将问题转换成了在两个后端服务之间推送数据, 同样在尝试了大量操作后我发现如何保持HttpServletResponse
成了一大难题. 经过了一整天的痛苦鏖战后, 我决定使用两个 websocket 解决这一问题 (没错, ✌ 叛变了), 目前我的完整的业务流程如下:
- 前端先使用 http 请求订阅服务, 这一步完成鉴权, 此时服务之间使用普通的 Rpc 调用.
- 订阅后前端在使用 websocket 请求, 此时各服务之间也都使用 websocket 推送数据, 因为第一步已经完成了鉴权, 第二步推送服务时也都是从已经完成鉴权的连接中选择进行推送数据, 保证了安全性.
其他
在此过程中我也就我的需求请求了群友大佬, 由此我还获取到了以下这些解决方法:
- 使用 mq (放弃了, 因为不划算, 数据小没必要, 数据大会消息堆积)
- 使用 redis 的发布订阅模式 (以前没了解过的新东西, 感觉不错, 但还是不太符合我这个需求)
- 使用两个 sse 接口 (尝试过, 但是多线程的操作导致如何保持
HttpServletResponse
成为一个难题)
此过程中我也发现 SpringBoot 中可以通过SseEmitter
使用 sse, 不需要再手动添加data:
这个符号 (或者说上面我用的 sse 并不标准). 具体可以参考这篇文章
, 文章代码没有复现, 但看起来讲的还是比较全的.
感觉可以改成 sse 避坑贴了
实测 sse 每次传输字符串长度大于 9000 多会被截断,所以保守起见,如果用 sse 一次传输大量数据的情况下建议 8000 字符分个块,前端将每次收到的数据块拼接合成一个完整数据,建议每个 sse 块用如下数据结构包装一下:
public class SSEResp{
private Integer curIdx; // 当前第几个数据块
private Integer allBlokNum; // 一共几个数据块
private Boolean end; // 是否结束
private SSEData data; // 真实数据json字符串的一部分
}