server push 指的是 server 端发生某事件后立刻向 client 端发送消息,而不是像通常的 RPC 访问中那样被动地回复 client。brpc 中推荐以如下两种方式实现推送。
远程事件
和本地事件类似,分为两步:注册和通知。client 发送一个代表事件注册的异步 RPC 至 server,处理事件的代码写在对应的 RPC 回调中。此 RPC 同时也在等待通知,server 收到请求后不直接回复,而是等到对应的本地事件触发时才调用 done->Run() 通知 client 发生了事件。可以看到 server 也是异步的。这个过程中如果连接断开,client 端的 RPC 一般会很快失败,client 可选择重试或结束。server 端应通过 Controller.NotifyOnFailed() 及时获知连接断开的消息,并删除无用的 done。
这个模式在原理上类似 long polling,听上去挺古老,但可能仍是最有效的推送方式。”server push” 乍看是 server 访问 client,与常见的 client 访问 server 方向相反,挺特殊的,但 server 发回 client 的 response 不也和”client 访问 server”方向相反么?为了理解 response 和 push 的区别,我们假定”client 随时可能收到 server 推来的消息”,并推敲其中的细节:
- client 首先得认识 server 发来的消息,否则是鸡同鸭讲。
- client 还得知道怎么应对 server 发来的消息,如果 client 上没有对应的处理代码,仍然没用。
换句话说,client 得对 server 消息”有准备”,这种”准备”还往往依赖 client 当时的上下文。综合来看,由 client 告知 server”我准备好了”(注册),之后 server 再通知 client 是更普适的模式,这个模式中的 “push” 就是 “response”,一个超时很长或无限长 RPC 的 response。
在一些非常明确的场景中,注册可以被简略,如 http2 中的 push promise 并不需要浏览器(client)向 server 注册,因为 client 和 server 都知道它们之间的任务就是让 client 尽快地下载必要的资源。由于每个资源有唯一的 URI,server 可以直接向 client 推送资源及其 URI,client 看到这些资源时会缓存下来避免下次重复访问。类似的,一些协议提供的”双向通信”也是在限定的场景中提高推送效率,而不是实现通用的推送,比如多媒体流,格式固定的 key/value 对等。client 默认能处理 server 所有可能推送的消息,以至于不需要额外的注册。但推送仍可被视作 “response”:client 和 server 早已约定好的请求的 response。
Restful 回调
client 希望在事件发生时调用一个给定的 URL,并附上必要的参数。在这个模式中,server 在收到 client 注册请求时可以直接回复,因为事件不由注册用 RPC 的结束触发。由于回调只是一个 URL,可以存放于数据库或经消息队列流转,这个模式灵活性很高,在业务系统中使用广泛。
URL 和参数中必须有足够的信息使回调知道这次调用对应某次注册,因为 client 未必一次只关心一个事件,即使一个事件也可能由于网络抖动、机器重启等因素注册多次。如果回调是固定路径,client 应在注册请求中置入一个唯一 ID,在回调时带回。或者 client 为每次注册生成唯一路径,自然也可以区分。本质上这两种形式是一样的,只是唯一标志符出现的位置不同。
回调应处理幂等问题,server 为了确保不漏通知,在网络出现问题时往往会多次重试,如果第一次的通知已经成功了,后续的通知就应该不产生效果。上节”远程事件”模式中的幂等性由 RPC 代劳,它会确保 done 只被调用一次。
为了避免重要的通知被漏掉,用户往往还需灵活组合 RPC 和消息队列。RPC 的时效性和开销都明显好于消息队列,但由于内存有限,在重试过一些次数后仍然失败的话,server 就得把这部分内存空出来去做其他事情了。这时把通知放到消息队列中,利用其持久化能力做较长时间的重试直至成功,辅以回调的幂等性,就能使绝大部分通知既及时又不会被漏掉。