目前大部分App后端还没有统一的网关。其实不止是后端,移动端也是需要网关的。移动网关帮助我们解决稳定性、业务分级隔离、大促容量评估、异构系统支持等问题。移动网关本质是是,以可管控的方式暴露到外网去,这里的关键是如何管控和暴露。从通讯协议上讲移动网关是对外接收开放的通信协议,HTTP、gRPC等,一般还有协议转换讲HTTP转换成内部的RPC协议。本文笔者将谈谈得物需要什么样的移动网关。
一、电商对网络的要求
1.1 速度 快快快
对于电商平台来说,网络速度不仅仅是用户体验的问题,他直接关系到收入,在亚马逊公开的数据中可以查到:
- 页面加载超过3秒,57%的用户会离开;
- Amazon页面加载延长1秒,一年就会减少16亿美金营收;
1.2 应对复杂的环境
对于移动端来说资源(电量、内存、CPU)永远都是不够用的,最重要的是移动端机型多差异大,而且随身携带,用户可以在任意场景(电梯、高铁、地下车库等),在碎片化时间里使用App;
二、网关的能力
2.1 复用长链接
电商的业务场景,如直播、即时日志回捞、即时消息推送都需要用到长链接,但是目前直播和IM分别使用两套,资源上太浪费,而且在大部分时间,这两个长链接是相对空闲的,如果能利用这个长链接收发请求,将会对用户体验有较大的提升。
把长链接统一收到网关层,全业务层复用,业务不用去关心,请求发送的方式和格式。而客户端统一由APP内置网络服务器来管理所有请求、回调和调度。
在业务层会有“请求(client)--->响应(server)”和“推送(server)--->接收(client)”两种通讯模式。在此基础上,客户端不仅可以利用长链接发送请求,还可以将IM系统的同步机制拓展到其他模块,从而让客户的数据达到增量更新的目的;举个例子,比如用户有很多订单记录,传统的上客户端会发送http请求给服务端拉取用户的所有订单记录,这样很浪费流量,速度也慢。使用同步机制的话,只需要同步差量数据。这样数据量小,速度也快同时成功率也高。而且同步机制在用户不在线的情况下会把差量数据保存下来,等用户再次连接时同步数据,这个逻辑和聊天系统非常类似。核心就是用消息同步的逻辑来替换全量拉取的逻辑。
2.2 服务端连接池
HTTP1.0 在默认情况下,client和server每次进行通信时,都需要建立一次连接,传输完成后中断连接。从1.1起默认使用长连接。在长连接中HTTP协议在响应的头部增加 Connection:keep-alive;虽然是长连接,但是每条连接在同一时间只能处理一个请求/响应,这意味着如果同时收到2两个请求就需要建立2个TCP连接,TCP建立连接的成本相对来讲是很大的。所以在HTTP2.0中引入了Stream/Frame的概念,支持分帧多路复用的能力,在逻辑上区分请求stream和响应stream,即赋予单条连接并发处理多个请求和响应的能力,解决HTTP1.0连接数量和并发量成正比的问题。
http2在协议上实现了stream多路复用,避免了像HTTP1需要排队的方式进行request 等待response,在未拿到response报文之前,该tcp连接不能被其他协程复用。HTTP2虽然解决了应用层的队头阻塞,但是tcp传输层也是存在队头阻塞的。 比如,client根据内核上的拥塞窗口状态,可以并发的发送10个tcp包,每个包最大不能超过mss。但因为各种网络链路原因,服务端可能先收到后面的数据包,那么该数据只能放在内核协议栈上,不能放在socket buf上。这个情况就是tcp的队头阻塞。
解决的方案就是增加连接池。
2.3 统一的加密方式
很多App只有传输层https加密,如果https做双向认证那么也很容易被中间人攻击,很多金融类App都做了应用层加密来防止报文信息泄漏。而应用层的加密最好是放在网关层。
2.4 DNS解析
DNS劫持是移动网络常常遇到的问题,常规操作是采用http协议访问自己的DNS服务器,获取IP映射,在访问域名时替换成IP访问。但是在https请求中这种方式无法使用,这个时候网关就可以发挥作用了,不实用系统的域名解析,而是自己实现域名解析方案。从而获取正确的域名解析,当然我们还可以提前解析域名,提高连接速度。
2.5 降级策略
前文提到移动网络是非常复杂的,各种弱网环境都会经常遇到,但是目前我们的App中还没有对应的降级策略,在网关中我们可以对长链接加上降级策略,在长时间无法重连的情况下,尝试切换协议发送UDP报文。短链接也可以尝试用QUIC收发。
2.6 统一拦截处理逻辑
对于请求中的error code,进行统一拦截,toast弹窗提示,异常处理,监控。有了拦截逻辑我们还可以对请求,进行缓存,短时间内相同的请求可以被合并。减少网络请求。
2.7 兼容异构的后端服务
目前后端有php、go和java开发的后端服务,请求有rpc/pb和http/json。使用统一网关封装rpc和http请求,让业务层从底层协议和数据格式中解脱出来不必关心请求细节专注业务。也可以为以后客户端直接rpc调用打下基础。
三、App侧网关的设计
3.1 协议
Cronet
协议层使用cronet网络库
https://chromium.googlesource.com/chromium/src/+/master/components/cronet/
cronet请求对象生命周期
1、协议支持
Cronet 本身支持 HTTP 协议、HTTP/2 协议和 QUIC 协议。
2、请求优先级
该库支持您为请求设置优先级标签。服务器可以使用优先级标签来确定处理请求的顺序。
3、资源缓存
Cronet 可以使用内存缓存或磁盘缓存来存储在网络请求中检索到的资源。后续请求会自动通过缓存提供。
4、异步请求
默认情况下,使用 Cronet 库发出的网络请求是异步的。您的工作器线程在等待请求返回时不会遭到屏蔽。
5、数据压缩
Cronet 支持使用 Brotli 压缩数据格式来压缩数据。
6、gRPC
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
一旦定义好服务,我们可以使用 protocol buffer 编译器 protoc 来生成创建应用所需的特定客户端和服务端的代码 。
有了 gRPC, 我们可以一次性的在一个 .proto 文件中定义服务并使用任何支持它的语言去实现客户端和服务器,反过来,它们可以在各种环境中,从Google的服务器到你自己的平板电脑—— gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处,包括高效的序列化,简单的 IDL 以及容易进行接口更新。 gRPC 和 proto3 特别适合移动客户端:gRPC 基于 HTTP/2 实现,相比 HTTP/1.1 更加节省网络带宽。序列化和解析 proto 的二进制格式效率高于 JSON,节省了 CPU 和 电池消耗。proto3 使用的运行时在 Google 以及被优化了多年,代码量极小。
- 一个 应答流式 RPC , 客户端发送请求到服务器,拿到返回的应答消息流。通过在 响应 类型前插入
stream
关键字,可以指定一个服务器端的流方法。
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 一个 请求流式 RPC , 客户端发送一个消息序列到服务器。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定
stream
关键字来指定一个客户端的流方法。
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 一个 双向流式 RPC 是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。你可以通过在请求和响应前加
stream
关键字去制定方法的类型。
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
现在让我们看看服务端的情况——流式RPC。 ListFeatures
是一个服务器端的流式 RPC,因此我们需要给客户端返回多个 Feature
。
Status ListFeatures(ServerContext* context, const Rectangle* rectangle,
ServerWriter* writer) override {
auto lo = rectangle->lo();
auto hi = rectangle->hi();
long left = std::min(lo.longitude(), hi.longitude());
long right = std::max(lo.longitude(), hi.longitude());
long top = std::max(lo.latitude(), hi.latitude());
long bottom = std::min(lo.latitude(), hi.latitude());
for (const Feature& f : feature_list_) {
if (f.location().longitude() >= left &&
f.location().longitude() <= right &&
f.location().latitude() >= bottom &&
f.location().latitude() <= top) {
writer->Write(f);
}
}
return Status::OK;
}
如你所见,这次我们拿到了一个请求对象(客户端期望在 Rectangle
中找到的 Feature
)以及一个特殊的 ServerWriter
对象,而不是在我们的方法参数中获取简单的请求和响应对象。在方法中,根据返回的需要填充足够多的 Feature
对象,用 ServerWriter
的 Write()
方法写入。最后,和我们简单的 RPC 例子相同,我们返回Status::OK
去告知gRPC我们已经完成了响应的写入。
如果你看过客户端流方法RecordRoute
,你会发现它很类似,除了这次我们拿到的是一个ServerReader
而不是请求对象和单一的响应。我们使用 ServerReader
的 Read()
方法去重复的往请求对象(在这个场景下是一个 Point
)读取客户端的请求直到没有更多的消息:在每次调用后,服务器需要检查 Read()
的返回值。如果返回值为 true
,流仍然存在,它就可以继续读取;如果返回值为 false
,则表明消息流已经停止。
while (stream->Read(&point)) {
...//process client input
}
最后,让我们看看双向流RPCRouteChat()
。
Status RouteChat(ServerContext* context,
ServerReaderWriter* stream) override {
std::vector received_notes;
RouteNote note;
while (stream->Read(¬e)) {
for (const RouteNote& n : received_notes) {
if (n.location().latitude() == note.location().latitude() &&
n.location().longitude() == note.location().longitude()) {
stream->Write(n);
}
}
received_notes.push_back(note);
}
return Status::OK;
}
这次我们得到的 ServerReaderWriter
对象可以用来读 和 写消息。这里读写的语法和我们客户端流以及服务器流方法是一样的。虽然每一端获取对方信息的顺序和写入的顺序一致,客户端和服务器都可以以任意顺序读写——流的操作是完全独立的。
3.2 网关层
整体框架
网关层封装网关服务,gRPC、代理服务、加密、长链接等等
App网络服务层概要设计
网关管理器统一管理,http/json请求,socket/PB长链接,和RPC请求,同时封装数据转换逻辑,可以切换json和bp数据格式。达到利用长链接发送http请求的需求。数据处理模块不仅负责数据转换,还是负责整请求的生命周期。
文/Matrix