新开的一个项目,后台童鞋提议使用 gRPC
,于是默默上了 gRPC
这条贼船。目前行使平稳,有些小颠簸,但可以克服。
gRPC 是由 Google
与 2015
年开源的一套主要面向移动应用开发的 RPC
框架。
相对于其他 RPC
框架而言,它有两个显著的特点:
HTTP/2
协议标准设计移动网络高延迟,低带宽,高丢包率的状态,使得我们需要进行大量的网络调优。而 HTTP 1.1
的一些特性使得它并不能很好的适应移动网络,一方面使用文本协议和无法复用 HTTP
头使得 HTTP 1.1
的流量消耗较大,另一方面 HTTP 1.1
的请求是有序堵塞的,使得 head-of-line blocking
问题十分严重,即使采用多连接和 pipelining
效果仍有限。但 HTTP/2
则可以用比较有效解决这些问题:采用二进制协议,完全多路复用,报头压缩,更能主动推送消息到客户端。
IDL
特性默认情况下,gRPC
使用 protobuf
作为 IDL(Interface Definition Language)
来定义服务(当然也可以使用 json
等):给定相应服务的 .proto
文件,gRPC
可以通过插件生成相应的客户端和服务器调用过程代码,使得客户端和服务器不再需要关心具体的请求装配,收发,解析过程,而更专注于相应的业务逻辑。
使用 gRPC
作为 RPC
框架的典型开发流程如下
.proto
文件拍完一波 gRPC
的马屁,让我们进入 iOS
端的流程。
一个最简单的服务定义如下:gRPC Helloworld example
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
这里定义了一个 Greeter
服务,通过 HelloRequest
参数,填充相应的参数 (name
),调用后服务器返回相应的文本信息,基本是一个最简单的 echo service
的实现。那么问题是,在 iOS
端我们怎么来使用这个服务定义呢?
当然我们需要引入 gRPC
框架,这里推荐使用 cocoapods
引入 gRPC
的 ObjC
实现。一方面 swift
版 gRPC
刚 release
不久,并不一定很稳定,另一方面,gRPC
框架有各种依赖,手动导入基本不太可能,只能通过 cocoapods
进入导入。
但不像使用其他第三方库一样我们可以直接 pod grpc
,使用 gRPC
需要另辟蹊径。原因在于 gRPC
为了方便各个平台能够方便使用,除提供一个基于 C
语言的核心实现外,还需集成各种胶水库,工具链。一个完整的 gRPC
框架依赖如下组件
组件 | 作用 |
---|---|
Protobuf | Protobuf,序列化反序列化框架 |
gRPC-Core | C 语言 gRPC 实现 |
gRPC | ObjC gRPC wrapper |
gRPC-ProtoRPC | ObjC gRPC Serivce 定义 |
gRPC-RxLibrary | Reactive 拓展 (大雾,好贴心) |
ProtoCompiler | Protobuf 编译器 |
ProtoCompiler-gRPCPlugin | gRPC protobuf 编译器插件 |
官方推荐的做法是自定义一个本地私有 podspec
,客户端通过 pod install
这个 podsepc
导入所有依赖库并串联所有流程。一个最简单的 podspec
可以参考 gRPC Helloworld example
除去前面一些常见的说明外,这个 podspec
最重要的点在于
# Base directory where the .proto files are.
src = "../../protos"
# Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients.
s.dependency "!ProtoCompiler-gRPCPlugin", "~> 1.0"
# Pods directory corresponding to this app's Podfile, relative to the location of this podspec.
pods_root = 'Pods'
# Path where Cocoapods downloads protoc and the gRPC plugin.
protoc_dir = "#{pods_root}/!ProtoCompiler"
protoc = "#{protoc_dir}/protoc"
plugin = "#{pods_root}/!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin"
# Directory where the generated files will be placed.
dir = "#{pods_root}/#{s.name}"
s.prepare_command = <<-CMD
mkdir -p #{dir}
#{protoc} \
--plugin=protoc-gen-grpc=#{plugin} \
--objc_out=#{dir} \
--grpc_out=#{dir} \
-I #{src} \
-I #{protoc_dir} \
#{src}/helloworld.proto
CMD
在 pod install
时,使用 protoc
和 grpc_objective_c_plugin
编译 src
中的 proto
文件,生成相应的 RPC
代码并最终导入工程。最后通过 #import
响应服务头文件调用方法就完成了一个 gRPC
远程请求的过程。
Greeter *client = [[HLWGreeter alloc] initWithHost:kHostAddress];
HelloRequest *request = [HLWHelloRequest message];
request.name = @"Objective-C";
[client sayHelloWithRequest:request handler:^(HLWHelloReply *response, NSError *error) {
NSLog(@"%@", response.message);
}];
从客户端的角度而言,gRPC
的确很简单,将复杂的网络请求变成了一个简单的 RPC
调用过程,但是在使用 gRPC
的时候还是碰到了一些小问题。
ObjC
只能通过前缀避免命名冲突。但默认生成的 gRPC
Messages
和 Services
都是没有任何前缀,如 Greeter
和 HelloReqeust
。很明显前期的 gRPC
开发对 ObjC
并不了解,甚至于他们自己的 gRPC-ProtoRPC
库中类都是没有任何前缀,如 ProtoRPC
,直到后期才开始添加 GRPC
作为前缀:GRPCProtoCall
,并将前者标记为废弃。
目前的处理方法是在 proto
文件开始处通过 objc_class_prefix
选项为生成的类制定前缀。
option objc_class_prefix = "NTES";
不过需要吐槽的一点是,难道不应该在 podspec
实现这个功能才更为简单么?
出于两方面的考虑,我们需要为所有的 gRPC
请求添加全局拦截器
session
过期这种业务逻辑然而通过 gRPC complier plugin
生成的 RPC
都是以 Service
为单位,提供一个集中式的 API
对应 RPC
的管理方式,即一个 RPC
调用就是对应一个网络请求,所有网络请求都被分开操作,没有任何关联关系。从 RPC
这个概念出发,这种做法是可取的,但是出于具体业务的需求,我更推荐使用基于基类/协议的网络请求封装:所有请求都继承自某个基类/实现某套协议接口,一个网络请求对应一个类,但他们都通过统一的流程进出,自定义需求通过重写基类/协议方法来实现。不过这只是个人设计网络协议时的一种倾向,而回到 gRPC
这边,问题就变成了:既然它已经设计成这种,我们应该怎么插入自己的全局拦截器呢?
gRPC Service
层代码我们会发现,在使用 protoc
和 plugin
的时候有两个参数 --objc_out
,--grpc_out
分别制定生成的 Messages
和 Services
。那么我们就可以只是使用 Messages
中提供的请求和响应类,直接废弃 gRPC
自动生成的 Services
层,通过自主构造 GRPCCall
的方法进行调用。但是这种改动的问题是容易引起调用的不一致,尤其是当后端修改相应服务方法名后。
gRPC ProtoCompiler Plugin
代码,重新生成 RPC
层代码同样是修改生成 RPC
代码的流程,这种方法会安全许多:通过修改 plugin
的代码,按照自己的意愿生成相应的 RPC
层代码,同时由于只是修改而非废弃原 Service
层代码,仍旧能够保证各个 RPC
方法名,请求,响应类和服务器接口的一致性。唯一的问题是需要维护一个私有的 ProtoCompiler Plugin
pod
仓库。有兴趣的同学可以参考 complier 里 objc 相关的实现代码,不过值得吐槽的是,这个 plugin
工程需要使用 Visual Studio
打开编译。(大雾)
AOP
进行拦截这种是我们现在正在使用,也是 ObjC
里喜闻乐见的方法,通过 method swizzle
替换掉所有 RPC
方法,并将所有的回调进行统一包装,就可以实现全局拦截的作用。
- (void)hookAllGRPCCall:(Class)cls
{
unsigned int count = 0;
Method *methods = class_copyMethodList(cls, &count);
for (unsigned int i = 0; i < count; i++)
{
SEL sel = method_getName(methods[i]);
NSString *selName = NSStringFromSelector(sel);
if ([selName hasPrefix:@"RPCTo"]) //所有生成 RPCCall 的方法都有这个前缀
{
FCGRPCHookBlock block = ^(id<AspectInfo> info,id request,GRXSingleHandler handler)
{
NSInteger requestId = [self requestId];
DDLogInfo(@"begin grpc id %zd for %@ %@\nrequest %@",requestId,cls,selName,request);
GRXSingleHandler hookHandler = ^(id value, NSError *errorOrNil){
DDLogInfo(@"end grpc id %zd for %@ %@\nresponse %@ error %@",requestId,cls,selName,value,errorOrNil);
#warning todo 添加 session失效后重新请求的逻辑
if (handler) {
handler(value,errorOrNil);
}
};
NSInvocation *invocation = [info originalInvocation];
[invocation setArgument:&hookHandler atIndex:3];
[invocation invoke];
};
NSError *error = nil;
[cls aspect_hookSelector:sel
withOptions:AspectPositionInstead
usingBlock:block
error:&error];
if (error)
{
DDLogError(@"swizzle %@ selector %@ failed",cls,selName);
}
}
}
}