IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    gRPC 碎碎念

    Xiang Wangfeng发表于 2017-01-09 00:00:00
    love 0

    前言

    新开的一个项目,后台童鞋提议使用 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 框架的典型开发流程如下

    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 引入 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);
                }
                
            }
        }
    }
    
    


沪ICP备19023445号-2号
友情链接