写这篇笔记的目的

为了应对后续开发生涯中可能遇到的种种情况以及分布式计算的趋势(讲白了就是后续对工作会很有帮助)。如若总是依赖http-api/restful编写并提供外部调用接口,当接口数量不断上升,文档内容不断增加,这给设计者和使用者都带来非常不好的体验,而RPC在这体就现出了非常大的优势。我将自己的理解和体会以及学习的过程记录在这里,以便今后遇到问题能够从这儿获得些许的线索以及提供一个参考给同道中人。

前提

不妨思考这样一个情形:作为接口设计者,我早已经定义好的接口的请求方式(RESTFul)和返回结构(json),但是每个接口我还需要另外维护一个文档来说明各个接口的用法(请求参数)和解释返回的结果(字段描述)。而对于接口的调用者而言,不但要去文档中查找自己需要的接口并阅读说明,在实际调用中,还要以防接口提供者返回非既定结构的结果而导致的报错。

介绍

Remote Procedure Call(远程过程调用),简称RPC。它可以使得调用远程服务接口如同调用本地方法一样简单。虽然实质还是通过网络通信,但是相比http请求api的网络开销还是极小的,其原因简单来说,HTTP协议每次请求都需要建立TCP连接,就会涉及3次握手的网络开销问题以及冗余报文,而rpc直接使用TCP多路复用(gRPC基于HTTP/2)无需重复建立连接。就文档方面而言,编写一份开发文档足矣,因为接口的定义由接口定义语言(IDL)来完成,而阅读IDL便可理解所有接口,并且通过编译器可以将IDL编译成不同的语言实现源码(gRPC通过protoc编译器将protobuf编译)。其他优点例如:注册、监控、发布等的这里不做论述。

Go-Server

GRPC的官方资源:go get google.golang.org/grpc

GRPC的镜像资源:https://github.com/grpc/grpc

官方的相关示列可以在grpc/examples/中找到。

定义服务

使用protocol buffers去定义gRPC service和方法 request以及 response 的类型。

新增并编辑文件:something.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
syntax = "proto3";

// 声明包/作用域
package something;

// 定义一个服务,名为Wiwider
service Wiwider {
rpc FindUser (UserRequest) returns (UserReply) {} // 声明一个方法
rpc FindUsers (UserRequest) returns (UsersReply) {}
}

// 定义一个请求消息
message UserRequest {
string name = 1;
}

// 定义一个响应消息
message UserReply {
int64 id = 1;
string name = 2;
int32 age = 3;
sexes sex = 4;

enum sexes {
Male = 0;
Female = 1;
}
}

// 定义一个包含消息的消息
message UsersReply {
repeated UserReply users = 1;
}

生成代码

Go-代码

1
protoc --go_out=plugins=grpc:. something.proto

运行这个命令后,会在当前目录生成一个something.pb.go文件,内容包含:

  • 所有用于填充,序列化和获取我们请求和响应消息类型的 protocol buffer 代码
  • 一个为客户端调用定义在Wiwider服务的方法的接口类型(或者 存根 )
  • 一个为服务器使用定义在Wiwider服务的方法去实现的接口类型(或者 存根 )

PHP-代码

1
protoc --proto_path=./ --php_out=./ --grpc_out=./ --plugin=protoc-gen-grpc=/Users/anthony/git/grpc/bins/opt/grpc_php_plugin ./something.proto

其中/Users/anthony/git/grpc/bins/opt/grpc_php_plugin对应从git中获取的<grpc-git-path>/bins/opt/grpc_php_plugin

运行这个命令后,会在当前目录生成如下:

  • GPBMetadata/something.php
  • Something/UserReply/sexes.php
  • Something/UserReply.php
  • Something/UserReply_sexes.php
  • Something/UserRequest.php
  • Something/UsersReply.php
  • Something/WiwiderClient.php

创建服务器

首先我们需要实现服务定义的服务接口:

something/something.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package something

import (
"Wiwide/cmi"
"context"
)
type UserServer struct {

}

func (s *UserServer) FindUser(ctx context.Context, in *UserRequest) (*UserReply, error) {
name := in.GetName()
rs, err := cmi.Db("local").Table("users").Where("name", name).First() // 从数据库查询
if err != nil || len(rs) == 0 {
return &UserReply{},err
}
rId := rs["id"].(int64)
rName := rs["name"].(string)
rAge := int32(rs["age"].(int64))
rSex := UserReplySexes(rs["sex"].(int64))
rep := &UserReply{Id:rId,Name:rName,Age:rAge,Sex:rSex}
return rep, nil
}

func (s *UserServer) FindUsers(ctx context.Context, in *UserRequest) (*UsersReply, error) {
name := in.GetName()
rs, err := cmi.Db("local").Table("users").Where("name","like","%"+name+"%").Get() // 从数据库查询
if err != nil {
return &UsersReply{}, err
}
list := make([]*UserReply, 0)
for _, v := range rs {
rId := v["id"].(int64)
rName := v["name"].(string)
rAge := int32(v["age"].(int64))
rSex := UserReplySexes(v["sex"].(int64))
list = append(list, &UserReply{Id:rId,Name:rName,Age:rAge,Sex:rSex})
}
rep := &UsersReply{Users: list}
return rep, nil
}

然后运行一个gRPC服务器,注册我们的服务并监听来自客户端的请求:

server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"Wiwide/rpc/something"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"log"
"net"
)

var (
port = ":50051"
)
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
something.RegisterWiwiderServer(s, &something.UserServer{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

运行服务器

简单执行命令:go run server.go,然后等待客户端请求。

创建客户端

Go-Client

创建文件client.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"Wiwide/rpc/something"
"context"
"google.golang.org/grpc"
"log"
"os"
"time"
)

const(
address = "localhost:50051"
defaultName = "World"
)

func main(){
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
s := something.NewWiwiderClient(conn)
u, err := s.FindUser(ctx, &something.UserRequest{Name:name})
if err != nil {
log.Fatalf("could not Find: %v", err)
}
us, err := s.FindUsers(ctx, &something.UserRequest{Name:name})
if err != nil {
log.Fatalf("could not Find users: %v", err)
}
log.Printf("Find user: %v", u)
log.Printf("Find users: %v", us)
}

PHP-Client

PHP首先要安装grpc的php扩展,下载地址:http://pecl.php.net/package/gRPC

直接使用phpize安装:

1
2
3
4
5
6
7
8
tar -zxf grpc-1.17.0.tgz
cd grpc-1.17.0
phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make
make install
# 在php.ini中增加
extension = grpc.so

执行php -m | grep grpc 应该会输出”grpc”,就代表成功了。

创建客户端文件,这里需要用到composer获取两个包,其中composer.json内容为:

1
2
3
4
5
6
7
8
{
"name": "grpc/grpc-demo",
"description": "gRPC example for PHP",
"require": {
"grpc/grpc": "^v1.3.0",
"google/protobuf": "^v3.3.0"
}
}

something.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
require dirname(__FILE__).'/vendor/autoload.php';

include_once dirname(__FILE__).'/Something/UserReply.php';
include_once dirname(__FILE__).'/Something/UsersReply.php';
include_once dirname(__FILE__).'/Something/UserRequest.php';
include_once dirname(__FILE__).'/Something/WiwiderClient.php';
include_once dirname(__FILE__).'/GPBMetadata/Something.php';

$client = new Something\WiwiderClient('127.0.0.1:50051', ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
$req = new Something\UserRequest();
$name = !empty($argv[1]) ? $argv[1] : 'world';
$req->setName($name);
list($reply, $status) = $client->FindUser($req)->wait();
var_dump($reply->getId(), $reply->getName(), $reply->getAge(), $reply->getSex(), $status);

list($reply, $status) = $client->FindUsers($req)->wait();
$uers = $reply->getUsers();
$users = array();
foreach ($uers as $key => $value) {
array_push($users, array('id'=>$value->getId(),'name'=>$value->getName(),'age'=>$value->getAge(),'sex'=>$value->getSex()));
}
var_dump($users);

运行客户端

Go-Client

我们先编译一下:go build client.go,输出可执行文件client,然后直接运行:

  • ./client Anthony
  • ./client ny

分别输出如下:

1
2
2018/12/02 13:40:15 Find user: id:1 name:"Anthony" age:24 sex:Female 
2018/12/02 13:40:15 Find users: users:<id:1 name:"Anthony" age:24 sex:Female >
1
2
2018/12/02 13:40:25 Find user: 
2018/12/02 13:40:25 Find users: users:<id:1 name:"Anthony" age:24 sex:Female > users:<id:4 name:"funny" age:15 sex:2 >

PHP-Client

直接执行PHP文件:php something.php Anthonyphp something.php ny,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int(1)
string(7) "Anthony"
int(24)
int(1)
object(stdClass)#8 (3) {
["metadata"]=>
array(0) {
}
["code"]=>
int(0)
["details"]=>
string(0) ""
}
array(1) {
[0]=>
array(4) {
["id"]=>
int(1)
["name"]=>
string(7) "Anthony"
["age"]=>
int(24)
["sex"]=>
int(1)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int(0)
string(0) ""
int(0)
int(0)
object(stdClass)#8 (3) {
["metadata"]=>
array(0) {
}
["code"]=>
int(0)
["details"]=>
string(0) ""
}
array(2) {
[0]=>
array(4) {
["id"]=>
int(1)
["name"]=>
string(7) "Anthony"
["age"]=>
int(24)
["sex"]=>
int(1)
}
[1]=>
array(4) {
["id"]=>
int(4)
["name"]=>
string(5) "funny"
["age"]=>
int(15)
["sex"]=>
int(2)
}
}

结束

本次笔记接近结尾了,内容很简单,主要记录使用grpc的大体流程。为后续的测试做一点点的准备。

参考资料