3

gRPC 碎碎念

 3 years ago
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.
neoserver,ios ssh client

gRPC 碎碎念

09 Jan 2017

新开的一个项目,后台童鞋提议使用 gRPC,于是默默上了 gRPC 这条贼船。目前行使平稳,有些小颠簸,但可以克服。

gRPC 概述

gRPC 是由 Google2015 年开源的一套主要面向移动应用开发的 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 框架的典型开发流程如下

  1. 后端定义服务,生成 .proto 文件
  2. 后端通过插件生成服务器接口代码,填充实现
  3. 客户端通过插件生成对应客户端代码,并调用

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 引入 gRPCObjC 实现。一方面 swiftgRPCrelease 不久,并不一定很稳定,另一方面,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 时,使用 protocgrpc_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 MessagesServices 都是没有任何前缀,如 GreeterHelloReqeust。很明显前期的 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 层代码

我们会发现,在使用 protocplugin 的时候有两个参数 --objc_out,--grpc_out 分别制定生成的 MessagesServices。那么我们就可以只是使用 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);
            }
            
        }
    }
}


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK