SSE
介绍
Server-Sent Events(SSE)是一种基于 HTTP
的服务器推送技术,它允许服务器向客户端实时发送数据
通过SSE,服务器可以主动推送数据给客户端,而无需客户端发起请求
SSE 建立在 HTTP
协议之上,使用了长连接(持久连接)来实现服务器和客户端之间的实时通信
与传统的请求-响应模型不同,SSE
允许服务器在事件发生时主动向客户端发送数据。这使得服务器可以实时地向客户端推送更新的信息、通知、状态变化等
例如 GPT 等 AI 的问答窗口,往往就是用 SSE
协议,这样可以减少用户等待响应的时间;其次模拟打字机等效果也可以增强用户体验
对比
HTTP
标准的 HTTP 协议中,每个 HTTP 响应都会在发送完毕后关闭连接,HTTP/1.1
协议默认采用短连接(short-lived connections)方式
HTTP 响应的头部中会包含一个 Connection
字段,用于指示连接的状态,当该字段的值为 close
时,表示服务器会在发送完响应后主动关闭连接
也可以设置为
Keep-Alive
,服务器可以自行决定是否在每个响应后关闭连接,在响应体中同样设置
Connection
为 Keep-Alive
可以让客户端保持连接
基于这个机制就可以实现长轮询、SSE 等效果
WebSocket
WebSocket 是一种浏览器和服务器之间进行全双工通信的协议,同样基于
TCP
它提供了一种实时、高效的双向通信机制,允许服务器主动向客户端推送数据,而不需要客户端发起请求,WebSocket
协议相对于传统的 HTTP 协议更适合实时通信和实时更新的应用场景
WebSocket 不是 HTTP 协议,但是需要先使用 HTTP 协议进行协议升级
对比表格
HTTP |
单向短连接 |
请求-响应 |
HTTP SSE |
单向长连接 |
服务器推送 |
WebSocket |
双向长连接 |
双向通信 |
协议细节
要订阅服务器事件,客户端发出 GET 请求带有指定的 header:
- Accept:
text/event-stream
表示可接收事件流类型
- Cache-Control:
no-cache
禁用任何的事件缓存
- Connection:
keep-alive
表示正在使用持久连接
服务器应该使用相应的响应来确认订阅:
- Content-Type:
text/event-stream;charset=UTF-8
表示标准要求的事件的媒体类型和编码
- Transfer-Encoding:
chunked
表示服务器流式传输动态生成的内容,因此内容大小事先未知
- Connection:
keep-alive
表示正在使用持久连接,
- Keep-Alive:例如
timeout=60
,服务端告知客户端保持连接的时长
数据内容
事件之间由两个换行符分隔 \n\n
每个事件由一个或多个 名称:值
字段组成,由单个换行符
\n
分隔
comment
属性则只会有
:值
,客户端可以根据特征判断
1 2 3 4 5 6 7 8 9
| id:this is id event:this is event name :this is comment data:this is data 1
id:this is id event:this is event name :this is comment data:this is data 2
|
简单实现
Controller
使用 Spring 实现一个 SSE 接口,Spring MVC 已经提供了对 SSE 的支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @RestController @Slf4j public class SSEController {
@Autowired private SSEService sseService;
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamEvents() { SseEmitter emitter = new SseEmitter();
new Thread(() -> { String sentence = sseService.giveMeASentence(); Stream.of(sentence.split("")).forEach(c -> { try { emitter.send(SseEmitter.event().data(String.valueOf(c))); TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(30, 100)); } catch (Exception e) { emitter.completeWithError(e); log.error("SSE error", e); } }); emitter.complete(); }).start();
return emitter; } }
|
基本步骤:
- 使用
@RestController
注解创建一个 Controller
- REST 方法返回一个
SseEmitter
,处理 GET
请求并产生文本/事件流 text/event-stream
- 创建一个新的
SseEmitter
,保存它并从方法中返回
- 异步线程操作
SseEmitter
,实现服务端的多次响应和对连接的关闭
这里为了模拟打字机效果,操作 SseEmitter
时做了一个 sleep
操作,获取文案后拆分调用 SseEmitter
,响应完成后关闭连接
Service
SSEService
主要是提供一段文案,这个不重要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Service public class SSEService {
private static final List<String> SENTENCES = new ArrayList<>();
static { SENTENCES.add("成功不是最终的,失败不是致命的,勇气继续前进才是最重要的。 ———— 丘吉尔"); SENTENCES.add("生活就像骑自行车,你必须保持前进才能保持平衡。 ———— 爱因斯坦"); SENTENCES.add("成功的关键是把握机会的能力 ———— 爱迪生"); SENTENCES.add("不管你走了多远,决定转身总是你自己 ———— 马丁·路德·金"); }
public String giveMeASentence() { return SENTENCES.get(RandomUtil.randomInt(SENTENCES.size())); } }
|
客户端
比如前端等,可以制作出打字机的效果
这里简单使用 Java 的 print
操作来模拟出这种效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public class SSEClient {
public static void main(String[] args) throws IOException { String sseUrl = "http://localhost:8080/sse";
URL url = new URL(sseUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "text/event-stream");
connection.connect();
int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line; while ((line = reader.readLine()) != null) { if (line.length() < 5) { continue; } String eventData = line.substring(5).trim(); System.out.print(eventData); }
reader.close(); } else { System.out.println("Failed to connect to SSE endpoint. Response code: " + responseCode); }
connection.disconnect(); } }
|
效果
参考
SSE(Server-Send
Events)实战 - 知乎 (zhihu.com)