先戴个头盔,以下所有论述不保证正确性,请自行甄别服用。
不想看前面的屁话,要直接上代码的,请跳到「iOS App端如何实现和RPC服务器通信」章节
什么是RPC、gRPC、grpc-swift
要搞清楚什么是grpc-swift,
就要先搞清楚什么是gRPC,
要搞清楚什么是gRPC,
就要先搞清楚什么是RPC。
What is RPC
RPC,是Remote procedure call的简称,翻译过来——「远程过程调用」。
请讲人话?OK,举个🌰,假如我要转1个比特币给你(事实上我并没有1个比特币,不嫌弃波卡幣/Polkadot的话,可以转给你——最近跌好惨🫠),然后我就通过RPC这种「传输方式」转给你。
本质上这是一个传输数据的过程。所以RPC,就简单理解成「一种传输数据的方式」。
对比地看,我们还有另一种更常用的方式:HTTP+REST。(不知道啥玩意儿?不要紧。就理解成是互联网上另一种传输数据的方式就好了。)
简单来说,HTTP+REST方式,聚焦在数据data
上:发送一个请求request
,然后返回数据response
。
而RPC,聚焦在「方法」上——直接调用一个「方法/函数/command」——只是对比于在同一个软件内部调用方法,RPC中调用有点不太一样,它是从电脑A,直接调用电脑B中的某个「方法」,是一个远程调用(Remote Call)。
然后这个「方法」和我们常见的「方法」一样,会有参数、返回值。要传输的数据,就放在参数、返回值里面,最终实现数据的传输。如下图:
截图出处: Comparing web API types: SOAP, REST, GraphQL and RPC
What is gRPC
OK,RPC是一种传输数据的方式,那gRPC又是什么?
聪明的你意识到了,这里多了一个「g」。不过,大多数人第一反应,应该不会认为「g」表示的是「Google」——毕竟百度是众多宇宙中第一好用的搜索引擎。
事实上「g」表示的,正是Google(起码大多数人是这样认为的。关于「g」的其他含义,下面再作补充),gRPC是Google主导的对RPC的具体实现。卖点:高性能、开源、通用(支持很多语言: Supported languages)。
其中,Protocol Buffers,有需要认识一下的。可以把它类比成XML、JSON,但是Protocol Buffers的数据包更小、速度更快、实现更简单。
你可能会猜到,RPC还有XML-RPC,JSON-RPC这些其他的实现。而gRPC,更准确的对标,我觉得应该叫「Protocol Buffers-RPC」~
再回到「g」,事实上,把它理解成「Google」没有错,不过,经常没事找抽的工程师,对「g」是有另一番调侃的,详情: GRPC Core: g_stands_for
为了让大家有更直观的理解,下面把互联网数据传输的一部分发展史展现如下:
What is grpc-swift
OK,我们有gRPC了,是不是可以开始写iOS端的App,从「RPC后台」拿一些数据了?
上面提到,gRPC支持多种语言,其中就有Objective-C(如果暂时不理解「支持」的含义,后面会继续解释)。
但是,现在大家都用Swift开发iOS App,所以就有了grpc-swift了。
所以,总括来看,他们的关系如下图:
(对了,题外话:Bitcoin用的是JSON-RPC
为什么要用gRPC
OK,上面讲了各种概念。那么,为什么要用gRPC呢?
(注意,我这里的问题是「为什么要用gRPC」,而不是「为什么要用RPC」)
天下武功,唯快不破
这是一条受用千年的古训。
gRPC用了上面提到过的Protocol Buffers,在数据传输过程,数据包/payload
是基于二进制/binary
的。
所以,数据包的size,比JSON小很多(想象一个例子:一个55bytes,一个20bytes)。
另外,二进制形式的数据包,CPU可以更高效地进行「序列化」和「反序列化」。
所以,概括来说,用gRPC的小伙伴,是想榨出更多的性能。
当然,gRPC也不是万金油,也有自己的劣势:浏览器支持有限、二进制格式对人类不友好等等。
更多的优劣分析,可以参考:
What is gRPC: Main Concepts, Pros and Cons, Use Cases
至于你要不要用gRPC,请自行斟酌——跟我好像没有关系。
iOS App端如何实现和RPC服务器通信
好了,上面讲了一大堆屁话,终于到正题了。
要写一个iOS的App,和gRPC后台通信。首先,我们要有一个gRPC后台——好一句废话。
服务端跑起来
没有后台经验的小伙伴不需要菊花一紧,你只需要在你的终端敲入swift run HelloWorldServer
这行命令,然后再轻轻敲一下回车键,官方GitHub的HelloWorld后台,就会神奇般地跑起来了:
- 把grpc-swift项目clon下来
- cd到项目根目录
- 打开终端/
Termanil
,执行swift run HelloWorldServer
命令(成功后会看到终端的打印:server started on port 1234
)
这样,RPC后台就跑起来了。从这个后台能拿到什么数据?
首先这个后台有一个方法sayHello()
可供(App)客户端调用,然后,假如你调用这个方法并传入Antony
作为方法的参数(准确说应该是一个Rquest
对象),他会返回字符串Hello Antony!
(准确说应该是一个Response
对象)。如果不传参数,默认返回Hello stranger!
。
有没有很厉害?!
如果你迫不及待,没写好App,就想调sayHello()
方法试试看。可以:
- 再打开一个终端
- cd到项目根目录
- 执行
swift run HelloWorldClient
命令(成功后会看到打印:Greeter received: Hello stranger!
)
表示我们的客户端(是一个命令行工具)调用了sayHello()
并收到了后台服务端的数据了!
.proto
文件的撰写
在写App之前,还想介绍一下 .proto
文件。
上面介绍了,我们客户端这边,调用了sayHello()
方法,同样地,到时候我们的App,也会调用这个方法,获取数据,而这个方法自然是用Swift语言写的,我们需要自己写这个方法吗?答案是不需要。那这个方法从哪里来?
答案就是接下来介绍的 .proto
文件。我们利用Protocol Buffers这个接口描述语言,来把我们的数据传输过程中的「数据模型」和「方法」在 .proto
文件定义好,然后再通过相关指令,生成你的客户端需要的代码。比如iOS的Swift、Android的Kotlin等等。
(上面说过的「gRPC支持多种语言」,就是这个意思。)
下面是仓库中的helloworld.proto
文件
// Protocol Buffers有proto2版本,这里表明,我们用的是比较新的proto3版本
syntax = "proto3";
// 下面的option,是生成代码时候的一些配置
option java_multiple_files = true; // 生成的Java代码,是否分成多个文件
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW"; // 生成的Objective-C代码的前缀是什么
// 「包名」。想象一下,你在这里定义、最后生成的「类」和「方法」,有可能会和你原来App的「类」、「方法」重名。
// 这里加一个package的名称,避免「命名冲突」
package helloworld;
// 定义一个service
// 事实上你可以在同一个 .proto文件,定义多个serive(按我目前理解,这样做可以让不同功能的APIs,组织得更有条理?)
service Greeter {
// SayHello接口方法的具体定义
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 参数HelloRequest的定义
// 注意,这里的1,并不是给name赋值,而是标记tag,用于序列化和反序列化时的字段匹配
// 这里的message关键字,可以理解成和class类似
message HelloRequest {
string name = 1;
}
// 返回值类型HelloReply的定义
message HelloReply {
string message = 1;
}
// 如果有其他的数据模型和方法,继续添加就好。是不是挺简单的?
具体的语法介绍: Language Guide (proto3)
这里需要说明一下, .proto
文件,理论上是负责后台的工程师去撰写的。
可能比较nice一点的同事,会顺便生成swift文件给你,你直接用就可以了。没那么nice的,可能会把 .proto
文件丢给你,让你自己玩。
不过这里的最佳实践,我相信是前后端的工程师一起讨论 .proto
文件中API接口的撰写,毕竟前后端开发有差异,很难避免写出一些不符合对方预期的API接口。
有兴趣的前端小伙伴,也可以试试往helloworld.proto
文件加点方法,改点内容,重新生成代码,更新实现。感受一下后台的开发。
接口代码的生成
OK,现在我们有 .proto
文件了,假如我们碰到一位没那么nice的后台同事,把 .proto
文件直接丢过来,要怎么生成Swift代码?
gRPC Swift
提供了一个插件/plugin
,叫protoc
(名字算是起得够烂了?让人很confusing)。详见: protoc gRPC Swift plugin
(如果没有安装这个插件而运行生成代码的指令,报错command not found: protoc
)
插件的安装,如果是macOS(应该没有人用Windows做iOS开发的?),直接在终端执行命令brew install swift-protobuf grpc-swift
,用Homebrew来安装。
更详细的安装说明:Getting the protoc Plugins
(这里有个坑,一开始我搜到的是gRPC官网的安装教程Protocol Buffer Compiler Installation,这个不是针对Swift的,安装后生成代码的时候会提示protoc-gen-grpc-swift: program not found or is not executable
)
装好后,就可以用命令来生成Swift代码了。不过,先看看生成的代码文件长什么样:
可以看到,两个文件(命名还有点奇怪):
-
.grpc.swift
文件生成的是:API接口方法(对应上面的SayHello
方法)、Client
(App端用到)、Provider
(实现后台时用到——后台工程师用) -
.pb.swift
文件生成的是:模型类(对应上面的HelloRequest
,HelloReply
)
接着,就可以敲命令行生成代码了,个人感觉命令行还是有点复杂,敲敲打打半天,才搞明白,所以画图说明一下(以这个目录下的helloworld.proto
文件为例。先cd到仓库的根目录grpc-swift
):
执行上面命令后,如无意外,就会得到helloworld.grpc.swift
和helloworld.pb.swift
两个文件。
可参考: protoc gRPC Swift plugin——不过感觉还没我讲得清楚
App端请求数据
终于可以写App端的代码了!!!
新建一个iOS工程,获取gRPC Swift
:可以用Swift Package Manager;可以手动导入;也可以用CocoaPods。详情可以看Github仓库的README。
连接服务器,调用方法,获取数据
接着可以连接gRPC服务器了并获取数据了:
let group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
// 创建一个channel
// 通过host和port,就知道连接那个服务器了
let channel = try? GRPCChannelPool.with(target: .host("localhost", port: 1234),
transportSecurity: .plaintext,
eventLoopGroup: group)
// 创建Client对象
// Helloworld_GreeterClient是根据.proto文件生成的代码
let greeter = Helloworld_GreeterClient(channel: channel!)
// 创建Request对象,作为方法的参数传给服务器
let request = Helloworld_HelloRequest.with {
$0.name = "ANTONY"
}
// 传入参数,调用方法
let sayHello = greeter.sayHello(request)
do {
// 拿到方法的返回值(后台返回的数据)
let response = try sayHello.response.wait()
print("Greeter received: \(response.message)")
} catch {
print("Greeter failed: \(error)")
}
最后会看到Xcode控制台打印:Greeter received: Hello ANTONY!
。这样就完成gRPC「客户端」和「服务器」之间的数据传输了。
Are you kidding me? 就这几行代码?你写了3000字?
OK,别着急,后面再写进阶一点的内容。