每个公司,随着业务持续不断地增长,作为单体项目本身,都会变得越发臃肿,不论是部署,开发,排查问题都变得越来越蛋疼。这个时候,我们会想到的方法就是讲一些业务服务逐步先服务化,在之后是微服务架构,甚至到最终是服务网格的模式。
当然本期的主题就是在拆分为多个服务时,我们应该怎么处理我们的相互调用,我们最先考虑的就是http调用,但是对于这样的调用存在的问题就是我们在拆分服务的时候,希望不用过多修改代码,不用关心调用里需要处理的细节,这就引出了RPC,全称远程过程调用,虽然是两个服务之间的调用,但是在代码层面上就是一个普通的函数调用,里面的调用细节已经封装好了,不用你关心,听着就好爽,至于RPC相关的知识,我推荐的是极客时间的<<趣谈网络协议>>相关章节,讲的很好也很透彻,这是链接:RPC综述。
既然选择了RPC,我们就会考虑一些比较成熟的框架(当然不是自己搞了。。。。因为实践就是最好的方案)。网上其实有许多不错的方案,包括dubbo,dubbox, grpc, thrift等。不过本次的文章主要讲的是多种语言之间的调用,所以我们的重点在于grpc和Thrift,grpc主要使用的是pb,而Thrift则较为灵活,支持多种压缩,在传输协议上grpc是http2.0,而Thrift则是同时支持http和tcp。当然两者更详细的对比大家可以看下这篇文章grpc 对比 thrift,在我编写实践中,发现thrift编写简单直观,虽然在代码生成上略多一些,但我还是选择了thrift,当然grpc也是一个非常不错的框架,另外实名反对上面那篇文章讲的thrift的文档偏少,其实主要是中文的文档偏少,但是其实因为却是简单方便,不需要过多文档。
实战准备
1.安装最基本的thrift工具,因为我本身使用的是mac,所以安装起来还是挺方便的
brew install thrift
其他平台的安装方式可以查看各平台安装thrift
通过在命令行调用 thrift -version确定安装成功
17:43:46 › thrift -version
Thrift version 0.11.0
2.本次实战讲解用的是php和go,php作为我们的client端,go就作为我们的server端
thrift 的构成
Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过Thrift提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。
thrift大致采用的结构就如下图所示:
Transport 就是网络传输部分,通过抽象化传输部分,应用可以根据自己的需要来选择自己传输方式,甚至可以使用自己写的传输方式。
Protocol 简单来说就是我们的编解码方式,选择一种特定的方式来转化成我们的传输数据,也可以自定义方式。
Processor 顾名思义,从我们的输入protocol 获取数据,然后传递到我们实例化的handler上进行处理,然后在把处理的结果返回到输出protocol上。
Server 则是我们服务器运行的方式,因为Thrift采用的是C/S架构,这样我们的server可以选择方式来处理远程客户端,比如单线程,事件驱动。
想要对thrift有更深入的了解,或者想对thrift文件编写和使用有更深了解的同学可以看这遍大牛写的文章,讲地很透彻。thrift使用指南
代码实现
我们的IDL文件作为实例,比较简单,client端和server端都只有唯一文件,但是namespace 应该以client语言为准 (我实践的结果。。。)
//example.thrift
namespace php example
struct Data {
1:string text
}
service format_data {
Data do_format(1:Data data)
}
go server的实现
1.在根目录里创建example.thrift 文件
2.执行命令
thrift -r --gen go example.thrift
-
根目录会多出一个gen-go 的目录,里面是thrift的实例化代码,都是自动生成的。
- 引入thrift-go 的库
go get git.apache.org/thrift.git/lib/go/thrift
如果因为不可抗拒原因无法下载,可以使用源码安装(当然我还是建议go get获取,比较方便,可以在网上查下如何go get 能成功的办法,在这里就不做讲解了)
通过源码安装:
* 在 $GOPATH 的 src 目录下创建多层级目录:git.apache.org/thrift.git/lib/go
* 从 github 上下载 [thrift 0.10.0](https://github.com/apache/thrift/archive/0.10.0.zip) 的源码 ,解压后进入 thrift-0.10.0/lib/go 目录下,将 thrift 目录 copy 到刚创建的 $GOPATH/src/git.apache.org/thrift.git/lib/go 目录下
* 在任意目录下执行 $ go install git.apache.org/thrift.git/lib/go/thrift 就完成了 golang 的 thrift 库的安装
5.接下来,我们编写函数的实例化代码,(为了方便,我把所有代码写在main.go 里)
type FormatDataImpl struct {}
func (fdi *FormatDataImpl) DoFormat(ctx context.Context,data *example.Data) (r *example.Data, err error) {
var rData example.Data
rData.Text = strings.ToUpper(data.Text)
fmt.Println(data, rData)
return &rData, nil
}
上面的代码其实比较简单,我们定义了一个类型,并且有一个方法DoFormat,该方法实例化了我们定义的方法,由于我使用的go版本是1.10,所以方法的参数会增加ctx context.Context,如果低版本应该和你定义的是保持一致的。
6.接下来定义我们的server
handler := &FormatDataImpl{}
processor := example.NewFormatDataProcessor(handler)
serverTransport, err := thrift.NewTServerSocket(HOST + ":" + PORT)
if err != nil {
log.Fatalln("Error:", err)
}
transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory)
fmt.Println("Running at:", HOST + ":" + PORT)
server.Serve()
之前我讲了thrift的大致结构,所以我们的server也是这样生成的,我相信你肯定看懂了,嘿嘿。
这是一个采用阻塞式socker,以frame为单位进行传输,采用二进制格式的简单的单线程服务模型。
想对各层次具有哪些格式和类型,可以查阅这篇文章Thrift 架构总结
7.所以我们完整的main.go 就如下
package main
import (
"context"
"fmt"
"git.apache.org/thrift.git/lib/go/thrift"
"log"
"strings"
"thrift/gen-go/example"
)
type FormatDataImpl struct {}
func (fdi *FormatDataImpl) DoFormat(ctx context.Context,data *example.Data) (r *example.Data, err error) {
var rData example.Data
rData.Text = strings.ToUpper(data.Text)
fmt.Println(data, rData)
return &rData, nil
}
const (
HOST = "localhost"
PORT = "8080"
)
func main() {
handler := &FormatDataImpl{}
processor := example.NewFormatDataProcessor(handler)
serverTransport, err := thrift.NewTServerSocket(HOST + ":" + PORT)
if err != nil {
log.Fatalln("Error:", err)
}
transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory)
fmt.Println("Running at:", HOST + ":" + PORT)
server.Serve()
}
-
编译运行,在go1.10可能会出现如下报错
这是因为生成的代码有问题,没给Flush函数传入context造成的,比较蛋疼,我们可以先手动给它传入了
oprot.Flush(ctx)
9.接下来我们正常的编译运行,如下图
10.ok 现在我们的server端OK了
php client的实现 (基于php7)
1.准备工作,由于官方的包没有做成composer包,所以我们测试的话可以按照官方的方法直接copy代码放入我们的项目,但我的建议,如果在正式环境中,最好把代码打成一个composer包,方便管理使用。
thrift的php官方代码仓库在github,我们在根目录创建lib,然后在lib下创建Thrift这个目录,然后把链接点开,把lib下面的所有代码都拷贝到Thrift这个目录下面,结果大概如下图。
2.接下来就是生成我们的php代码,依然用的还是那个thrift文件
比较简单的生成方式是
thrift -r --gen php thrift/example.thrift
这样生成的方式会将所有类都糅合进一个文件,所以我们也可以这样
thrift -r --gen php:psr4,validate,json thrift/example.thrift
- psr4 代码代码生成会遵循psr4标准,每个类一个文件
- validate 会生成校验代码,对参数进行校验
- json 生成的代码会实例化 JsonSerializable接口
当然这样生成的代码是client端的,代码量其实不是那么多,如果要生成server端代码,还需要加上server 参数
thrift --gen php:server,psr4,validate,json
3.接下来是我们如何引入thrift-php 的类,我们因为是直接拷贝的代码,所以需要手动引入,
require_once __DIR__.'/lib/Thrift/ClassLoader/ThriftClassLoader.php';
use Thrift\ClassLoader\ThriftClassLoader;
$GEN_DIR = realpath(dirname(__FILE__)).'/gen-php';
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift',__DIR__.'/lib');
$loader->registerNamespace('example',$GEN_DIR);
$loader->register();
需要注意的一点,如果没有加上psr4,我们生成的代码都在一个类,需要引入我们的 example的命名空间需要换成
$loader->registerDefinition('example',$GEN_DIR);
4.接下来是我们的调用
$socket = new TSocket('localhost',8080);
$transport = new TFramedTransport($socket);
$protocol = new TBinaryProtocol($transport);
$client = new format_dataClient($protocol);
$transport->open();
$data = new Data();
$data->text = 'World!';
$res = $client->do_format($data);
var_dump($res);
$transport->close();
为什么这样写我相信大家应该都懂了,😄
5.我们完整的client.php 代码如下
<?php
error_reporting(E_ALL);
require_once __DIR__.'/lib/Thrift/ClassLoader/ThriftClassLoader.php';
use Thrift\ClassLoader\ThriftClassLoader;
$GEN_DIR = realpath(dirname(__FILE__)).'/gen-php';
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift',__DIR__.'/lib');
$loader->registerNamespace('example',$GEN_DIR);
$loader->register();
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TFramedTransport;
use Thrift\Transport\TSocket;
use example\Data;
use example\format_dataClient;
try {
$socket = new TSocket('localhost',8080);
$transport = new TFramedTransport($socket);
$protocol = new TBinaryProtocol($transport);
$client = new format_dataClient($protocol);
$transport->open();
$data = new Data();
$data->text = 'World!';
$res = $client->do_format($data);
var_dump($res);
$transport->close();
} catch (\Exception $e) {
print 'TException:'.$e->getMessage().PHP_EOL;
}
- 接下来执行我们的client,结果就如下
13:02:54 › php Client.php
object(example\Data)#9 (1) {
["text"]=>
string(6) "WORLD!"
}
7.然后看下我们server端的输出
API server listening at: 127.0.0.1:60656
Running at: localhost:8080
Data({Text:World!}) {WORLD!}
- 这就是我们一次成功的rpc调用(当然是基于我们简单又灵活的thrift了),希望通过这样一个简单的过程,大家对rpc有了更深的了解,能够应用到日常生活中去。
参考文章
Thrift IDL基本语法
thrift go 的完整调用简单实现
走进 thrift server无废话 Thrift 之 Hello World( PHP 版).
Thrift使用指南
Thrift 的原理和使用