gRPC 碎碎念
source link: https://xiangwangfeng.com/2017/01/09/gRPC-%E7%A2%8E%E7%A2%8E%E5%BF%B5/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
gRPC 碎碎念
09 Jan 2017
新开的一个项目,后台童鞋提议使用 gRPC
,于是默默上了 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
文件 - 后端通过插件生成服务器接口代码,填充实现
- 客户端通过插件生成对应客户端代码,并调用
iOS 中的 gRPC
拍完一波 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
实现这个功能才更为简单么?
无法为所有的 RPC 提供全局拦截器
出于两方面的考虑,我们需要为所有的 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);
}
}
}
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK