当WebSocket过于复杂,或者为什么SSE再次流行
\n\n想象一下:您需要将数据从服务器实时发送到客户端。汇率、新消息通知、服务器日志、文件处理进度。首先想到的是WebSocket。但如果您不需要双向通道呢?如果客户端只监听,而服务器只发送呢?
\n\n这时服务器发送事件(SSE)登场了——这是一种协议,允许服务器通过普通的HTTP连接向客户端发送数据。无需多余的握手,无需复杂的协议,无需为代理和负载均衡器头疼。而且,在Rust上,它的表现简直完美。
\n\n在本文中,我们将讲解如何使用流行的框架从零开始在Rust上搭建SSE。您将看到可运行的示例,了解潜在陷阱,并明白何时SSE是您的最佳选择。
\n\n什么是服务器发送事件,为什么选择Rust?
\n\nSSE是W3C标准(EventSource API),允许服务器通过HTTP向客户端发送流式数据。客户端连接到端点,服务器开始以text/event-stream格式发送消息。这一切都基于普通的HTTP/1.1或HTTP/2,无需特殊代理,并得到所有现代浏览器的支持。
为什么选择Rust?因为Rust为我们提供了:
\n- \n
- 无惊无喜的异步性——Tokio和async-std允许在单个线程上维持数千个SSE连接 \n
- 内存安全——在多线程流式传输中,没有内存泄漏,没有竞态条件 \n
- 极低的资源消耗——一个SSE连接仅消耗几KB \n
- 生态系统——Actix Web、Rocket、Axum——所有这些都原生支持SSE,或只需极少的扩展 \n
SSE如何工作:解析协议
\n\n客户端在浏览器中创建一个EventSource,指定URL。服务器以Content-Type: text/event-stream头响应,并开始按以下格式发送字符串:
data: 你好,世界!\\n\\n\n\n每条消息是一个块,其中data:包含有效负载。消息之间用两个换行符分隔。可以传输JSON、数字、字符串——任何内容。此外,还有id、event和retry字段。
以下是一个SSE消息的最小示例:
\n\nid: 1\nevent: update\ndata: {"status": "processing", "progress": 42}\n\n\n\nJavaScript客户端将其作为update事件接收,其中data字段包含JSON。
使用Actix Web编写Rust SSE服务器
\n\n我们从最流行的框架——Actix Web开始。安装依赖项:
\n\n[dependencies]\nactix-web = "4"\nactix-rt = "2"\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n\n现在编写一个简单的端点,每2秒发送一次当前时间:
\n\nuse actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, middleware};\nuse actix_web::rt::time;\nuse futures::stream::StreamExt;\nuse std::time::Duration;\n\n#[get("/events")]\nasync fn stream_events(req: HttpRequest) -> HttpResponse {\n let mut rx = tokio::sync::broadcast::channel::<String>(100).1;\n\n let stream = async_stream::stream! {\n loop {\n let message = format!("data: {}\\n\\n", chrono::Local::now());\n yield Ok::<_, actix_web::Error>(\n web::Bytes::from(message)\n );\n time::sleep(Duration::from_secs(2)).await;\n }\n };\n\n HttpResponse::Ok()\n .insert_header(("Content-Type", "text/event-stream"))\n .insert_header(("Cache-Control", "no-cache"))\n .insert_header(("Connection"