Server-Sent Events / EventSource
背景
公司的 OA 项目中,往往需要接入很多业务系统,体现在 App UI 上就类似支付宝首页的九宫格。这些业务系统呢又不一定来自同一家厂商,当 App 需要和他们做一些交互的时候(例如获取每个业务的待办数目,展示在首页图标的角标上),手机端开发者就很头疼了。
就以待办数目为例,接口不统一、参数不统一、响应速度不统一、随时接入新的系统,交互过程如果写在 App 中,那就需要频繁的更新 App,这对用户来说,是非常不友好的。这个时候通常遵循的一个原则就是,将变化放在 OA-Server,OA-App 只需要与 OA-Server 约定好接口规范,只访问自己的后台即可,与各个业务的交互交给后台。那 OA-App 发送请求到 OA-Server,OA-Server 收到多个业务响应的待办数目以后怎么及时的通知 OA-App 呢?
提到服务端数据推送,可能一下子容易想到 WebSocket。WebSocket 是一种全新的协议,随着 HTML5 草案的不断完善,越来越多的现代浏览器开始全面支持 WebSocket 技术了,它将 TCP 的 Socket(套接字)应用在了web page 上,从而使通信双方建立起一个保持在活动状态连接通道。它是一种全双工通信,而我们前面提到的获取待办数目的场景,App 发送一次请求就可以,server 拿到各个业务的数据后再分别实时返回给 App ,更像是一种单向通信,使用 WebSocket 岂不是杀鸡用牛刀?那轮询呢?耗费 server 资源不说,不一定达到实时的效果。(PS:这里吐槽下客户,待办数有的 999 多,也不处理,居然只会嫌弃我们数据获取的不及时🤓)……
好巧不巧,看到了另一个轻量级的方案:Server-Sent Events。
Server-Sent Events
HTML5 中有一个轻量的替代 WebSocket的方案:Server-Sent Events,以下简称 SSE。
SSE 本质
严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。SSE 就是利用这种机制,使用流信息向浏览器推送信息。
SSE 特点
WebSocket 和 SSE 都是传统请求-响应 Web 架构的替代方案,但它们不是完全冲突的技术。
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 需要自己实现。
- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。
显然,我们上面提到的 待办数目 场景,很适合使用 SSE。
EventSource
EventSource 是 SSE 对应的客户端 API,大部分浏览器都是默认支持的。网上的很多资料也都是从浏览器的角度来讲的。那这里我们就从 HTTP 协议的角度讲一下交互过程吧。🙄,毕竟我不懂 web 前端。
API 介绍:EventSource
应用
- 首先 Client 发送一个普普通通的 GET 请求到 SSE-Server。
- SSE-Server 无论需要返回几次数据,数据格式必须是 UTF-8 编码的文本,HTTP 头部信息需要类似如下设置:
1 | Content-Type: text/event-stream |
上面三行之中,第一行的 Content-Type
必须指定 MIME 类型为 event-steam
。
- SSE-Server 每一次发送的信息,由若干个
message
组成,每个message
之间用\n\n
分隔。每个message
内部由若干行组成,每一行都是如下格式:
1 | [field]: value\n |
上面的 field
可以取四个值:
1 | data |
此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
1 | : This is a comment |
下面是一个例子。
1 | : this is a test stream\n\n |
- Client 拿到 message 以后自己解析。
message
前面提到的 field
有四个值,可以分别用作不同的用途。
data 数据内容
如果数据很长,可以分成多行,最后一行用 \n\n
结尾,前面行都用 \n
结尾。
1 | data: message\n\n |
id 数据标识
相当于每条数据的编号。
浏览器用 lastEventId
属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的 Last-Event-ID
头信息,将这个值发送回去,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
1 | id: msg1\n |
event 自定义的事件类型
默认是message
事件。浏览器可以用addEventListener()
监听该事件。
1 | event: foo\n |
上面的代码创造了三条信息。第一条的名字是 foo
,触发浏览器的 foo
事件;第二条未取名,表示默认类型,触发浏览器的 message
事件;第三条是 bar
,触发浏览器的 bar
事件。
下面是另一个例子。
1 | event: userconnect |
retry 重连间隔
服务器可以用 retry
字段,指定浏览器重新发起连接的时间间隔。
1 | retry: 10000\n |
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
iOS 实现
从上面的内容我们可以知道,既然 EventSource 客户端与 server 建立连接是基于标准的 HTTP 协议,那我们想要实现一套自己的 EventSource API ,只需要按规则解析 message 即可。
大致的思路:
- 使用
NSURLSession
来发起请求以及处理服务器的响应。 - 在
NSURLSession
的代理方法中解析得到的文本信息,使用自定义的Event
对象接收。 - 通过回调将
Event
传递给调用者。
代码是在下面这份上做的改动,添加了一些线程安全的处理,以及 session 的释放,不然会造成内存泄漏。
引用的OC代码 Github
改动后的源码:ATommyGirl/YYEventSource
模拟测试
启动服务
1 | node sse-server.js |
会在本机 http://127.0.0.1:8844/stream
启动一个 SSE 服务。
使用浏览器或者 OC 代码访问这个地址就会看到信息输出了。
1 |
|