xiaolingzi's blog

每天都在成长...

欢迎您:亲

PHP使用thrift实现RPC服务(服务端和客户端)

xiaolingzi 发表于 2017-06-21 15:04:53

一、简单说明

1.RPC服务是什么

RPC全称是Remote Procedure Call Protocol,即远程过程调用。他跟Web Service有类似的地方,但又有所不同。具体可以百度两种区别。这里不做深入讨论。

2.为什么选择thrift

thrift支持多语言,而且服务端和客户端也可以使用不同的语言,比较方便。其次它是Facebook开发,后交由Apache打理,也比较可靠。当然如果只是针对php还可以考虑yar、workerman-thrift-rpc或者swoole。


二、软件安装

软件的安装可以直接看官网的介绍。地址如下:

http://thrift.apache.org/docs/install/

碰到的问题这里简单说明一下,以centos为例。里面有些软件的安装有最低版本要求,安装的时候需要注意,但官方文档里面说到的软件版本不一定是当前最新的版本,大家可以去对应软件官网下载最新版本。

1.QT错误

(1)错误信息

./configure: line 17658: syntax error near unexpected token `QT,'
./configure: line 17658: `    PKG_CHECK_MODULES(QT, QtCore >= 4.3, QtNetwork >= 4.3, have_qt=yes, have_qt=no)'

在一台centos 6.2的系统里通过GitHub的源码进行安装时报该错误,而在另一台centos 7的系统里安装时则没问题,具体原因不详。

(2)解决办法

直接在官网下载release版本安装则没有报该错误。下载地址如下:

http://thrift.apache.org/download

2.libevent相关错误

(1)错误信息

报libevent相关的未定义引用的错误,比如其中一个:

undefined reference to `evutil_make_socket_closeonexec'

(2)解决办法

出现该错误的原因是,我原来通过yum已经安装过了libevent,版本比较旧,然后又通过源码编译安装了更新的版本,从而导致的部分冲突。通过yum卸载原来旧版本可以解决该问题。还有记得见c++,g++,gcc链接到/usr/bin下。

安装完之后,在源码目录可以找到对应的类库。

比如php的位置如下:

thrift-0.10.0/lib/php/lib


三、thrift脚本编写

编写服务代码之前,首先需要编写thrift脚本来定义服务、接口和类型,然后通过thrift工具生成对应语言的代码。

官方每种语言都有例子说明,php例子的地址如下:

http://thrift.apache.org/tutorial/php

这里我们做一个简单点的说明。

1.数据类型

thrift支持的数据类型如下:

bool        布尔型
i8 (byte)   8位整型
i16         16位整型
i32         32位整型
i64         64位整型
double      64位浮点型
string      字符串
binary      字节流
map<t1,t2>  键值对字典
list<t1>    有序列表,值可重复
set<t1>     无序集合,值唯一不重复
struct      结构体

2.脚本编写

(1)最简单的服务

service TestService
{
    string getUserName(1:i32 userId)
}

将上面的脚本保存test.thrift,我们就完成了一个最简单的服务脚本,服务名称叫TestService,里面定义了一个getUserName的接口,接收一个整型的userId变量,返回字符串类型的用户名。需要注意的是,接口函数的参数类型前面需要序号,比如第一个参数就是1加冒号(1:i32 userId),第二个第三个类推。

(2)自定义对象类型

struct User{
    1:i32 userId=0,
    2:string userName
}
service TestService
{
    string getUserName(1:i32 userId)
    User getUserInfo(1:i32 userId)
}

我对之前的脚本进行改造,添加了User结构,并定义userId和userName两个属性。然后再服务中添加了getUserInfo接口,该方法接收userId参数返回User对象。

(3)异步方法

struct User{
    1:i32 userId=0,
    2:string userName
}
service TestService
{
    string getUserName(1:i32 userId)
    User getUserInfo(1:i32 userId)
    oneway void test(1:i32 id)
}

我们继续对之前的脚本进行改造,添加test接口。接口方法前面加了oneway定义,通过这种方式就可以将该方法定义为异步执行。需要注意的是异步方法的必须是无返回类型(void)。

注意:测试异步执行只在客户端使用socket连接的方式时有效,http方式时还是阻塞了。

(4)命名空间

namespace php ThriftGen.Test.Service
struct User{
    1:i32 userId=0,
    2:string userName
}
service TestService
{
    string getUserName(1:i32 userId)
    User getUserInfo(1:i32 userId)
    oneway void test(1:i32 id)
}

以上脚本我们通过 namespace php ThriftGen.Test.Service 添加的命名空间的定义。我们知道很多语言都有命名空间的概念,通过该定义可以让生成的代码加上定义的命名空间。这里我们添加的是php的命名空间,其他语言就将php改成对应的语言即可。

需要注意的是PHP的命名空间是以斜杠(\)分级的,比如php代码里面应该是ThriftGen\Test\Service这样的,但在thrift脚本定义时需要统一使用点连接而不是斜杠。

(5)文件包含

当我们定义的对象类型很多时,我们不希望所有脚本写在一个文件里面,为了进行模块化处理,我们需要将脚本分别写在多个文件里面,这样我们就用到文件包含功能了。

thrift脚本文件可以通过 include "xxx.thrift"的方式包含其他脚本文件。我们可以将上面的对象类型定义和服务接口进行分离,变成两个文件。

# test.thrift
namespace php ThriftGen.Test.Service
include "entity.thrift"
service TestService
{
    string getUserName(1:i32 userId)
    entity.User getUserInfo(1:i32 userId)
    oneway void test(1:i32 id)
}
     
# entity.thrift
namespace php ThriftGen.Test.Entity
struct User{
    1:i32 userId=0,
    2:string userName
}

文件分离后,如果我们需要引用被包含文件里定义的资源属性时需要在前面加上文件名,比如getUserInfo方法返回的User的类型,要改为entity.User。

3.代码生成

代码生成命令如下:

thrift -r --gen php:server ./test.thrift

执行该命令之后就会在该目录下生成gen-php目录,目录里面就是生成的代码,目录结构按命名空间来分。比如上面两个文件的命名空间分别为ThriftGen.Test.Service和ThriftGen.Test.Entity,那么生成的代码分别在gen-php/ThriftGenTest/Service和gen-php/ThriftGenTest/Entity下,其中Service目录下包含TestService.php和Types.php文件,Entity脚本没有定义服务,所以该目录下只有Types.php.其中TestService.php的代码主要是定义接口和processor等,Types.php文件定义我们脚本中定义的类型。

需要注意的是通过thrift --gen php ./test.thrift也一样可以生成代码,但是不加:server生成的代码里面没有processor,所以服务端代码用时需要加上server,只是生成给客户端用时可以不加。

如果需要生成到指定目录时,可以通过-out命令指定,比如:

thrift -r --gen php:server -out ./gen ./test.thrift

以上生成的代码就会保存在指定的gen的目录下,但需要提前先建好该目录。

还需要注意的时,加上-r参数才会生成被包含的文件,而且生成的文件是以命名空间进行文件存在,没有命名空间都生成在根目录(gen-php)下,所以如果使用了包含文件,一定要加上不同的命名空间,否则Types.php文件会被覆盖,因为不同脚本文件都会生成Types.php文件,而且没有命名空间或者相同命名空间的话都是存在同一个目录下。


四、服务端实现

thrift客户端请求服务端可以通过http方式也可以通过socket的方式,相应的服务也有http方式和socket方式的写法。代码是参考官方列子来写的。

1. http方式

http方式需要通过nginx或者apache的web服务器来作为载体,或者切换到目录运行 php -S localhost:9091 启动,不过还是推荐使用nginx之类的。

<?php
namespace Test\Server;
     
error_reporting(E_ALL);
     
require_once __DIR__ . '/Thrift/ClassLoader/ThriftClassLoader.php';
     
use Thrift\ClassLoader\ThriftClassLoader;
     
$genDir = __DIR__ .'/gen-php';
     
//注册以自动加载类,目录记得改为自己实际的目录,如果不想用自动加载类也可以将TestService.php和Types.php直接包含进来
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__);
$loader->registerDefinition('ThriftGen', $genDir);
$loader->register();
     
/*
 * This is not a stand-alone server.  It should be run as a normal
 * php web script (like through Apache's mod_php) or as a cgi script
 * (like with the included runserver.py).  You can connect to it with
 * THttpClient in any language that supports it.  The PHP tutorial client
 * will work if you pass it the argument "--http".
 */
     
if(php_sapi_name() == 'cli')
{
    ini_set("display_errors", "stderr");
}
     
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TPhpStream;
use Thrift\Transport\TBufferedTransport;
use ThriftGen\Test\Service\TestServiceIf;
use ThriftGen\Test\Service\TestServiceProcessor;
use ThriftGen\Test\Entity\User;
     
class TestHandler implements TestServiceIf
{
    public function getUserName($userId)
    {
        return "test name";
    }
    public function getUserInfo($userId)
    {
        $user = new User();
        $user->userId=$userId;
        $user->userName="test name";
        return $user;
    }
    public function test($id)
    {
        echo "Start...\n";
        //休眠5秒模拟处理,http方式还是阻塞效果,socket方式才异步
        sleep(5);
        echo "End!!!\n";
    }
}
;
     
header('Content-Type', 'application/x-thrift');
if(php_sapi_name() == 'cli')
{
    echo "\r\n";
}
     
$handler = new TestHandler();
$processor = new TestServiceProcessor($handler);
     
$transport = new TBufferedTransport(new TPhpStream(TPhpStream::MODE_R | TPhpStream::MODE_W));
$protocol = new TBinaryProtocol($transport, true, true);
     
$transport->open();
$processor->process($protocol, $protocol);
$transport->close();

2.socket方式

看官方提供的类库,socket支持TSimpleServer和TForkingServer两种模式(这里说的是php,其他语言还有其他模式),前者单进程阻塞式,后者会fork新进程处理,建议使用后者。socket方式不需要web服务器载体,直接运行该php文件即可。

<?php
namespace Test\SocketServer;
     
error_reporting(E_ALL);
     
require_once __DIR__ . '/Thrift/ClassLoader/ThriftClassLoader.php';
     
use Thrift\ClassLoader\ThriftClassLoader;
     
$genDir = __DIR__ .'/gen-php';
     
//注册以自动加载类,目录记得改为自己实际的目录,如果不想用自动加载类也可以将TestService.php和Types.php直接包含进来
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__);
$loader->registerDefinition('ThriftGen', $genDir);
$loader->register();
     
if(php_sapi_name() == 'cli')
{
    ini_set("display_errors", "stderr");
}
     
use Thrift\Factory\TBinaryProtocolFactory;
use Thrift\Factory\TTransportFactory;
use Thrift\Server\TServerSocket;
use Thrift\Server\TSimpleServer;
use Thrift\Server\TForkingServer;
use ThriftGen\Test\Service\TestServiceIf;
use ThriftGen\Test\Service\TestServiceProcessor;
use ThriftGen\Test\Entity\User;
     
class TestHandler implements TestServiceIf
{
    public function getUserName($userId)
    {
        return "test name";
    }
    public function getUserInfo($userId)
    {
        $user = new User();
        $user->userId=$userId;
        $user->userName="test name";
        return $user;
    }
    public function test($id)
    {
        echo "Start...\n";
        //休眠5秒模拟处理,http方式还是阻塞效果,socket方式才异步
        sleep(5);
        echo "End!!!\n";
    }
}
;
     
$serverTransport = new TServerSocket("0.0.0.0",9090);
$clientTransport = new TTransportFactory();
$binaryProtocol = new TBinaryProtocolFactory();
$handler = new TestHandler();
$processor = new TestServiceProcessor($handler);
$server = new TForkingServer(
    $processor,
    $serverTransport,
    $clientTransport,
    $clientTransport,
    $binaryProtocol,
    $binaryProtocol
);
     
$server->serve();


五、客户端调用实现

客户端也是参考官方代码来写,支持http方式和socket方式,http方式需要加--http参数

<?php
namespace Test\Client;
     
error_reporting(E_ALL);
     
require_once __DIR__ . '/Thrift/ClassLoader/ThriftClassLoader.php';
     
use Thrift\ClassLoader\ThriftClassLoader;
     
$genDir = __DIR__ . '/gen-php';
     
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__ );
$loader->registerDefinition('ThriftGen', $genDir );
$loader->register();
     
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\THttpClient;
use Thrift\Transport\TBufferedTransport;
use Thrift\Exception\TException;
use ThriftGen\Test\Service\TestServiceClient;
     
try
{
    if(array_search('--http', $argv))
    {
        $socket = new THttpClient('192.168.2.11', 9091, '/Server.php');
    }
    else
    {
        $socket = new TSocket('192.168.2.11', 9090);
    }
    $transport = new TBufferedTransport($socket, 1024, 1024);
    $protocol = new TBinaryProtocol($transport);
    $client = new TestServiceClient($protocol);
         
    $transport->open();
         
    $result = $client->getUserName(5028);
    var_dump($result);
    $result = $client->getUserInfo(5028);
    var_dump($result);
    $client->test(5028);
    echo "Finished\n";
         
    $transport->close();
}
catch(TException $tx)
{
    print 'TException: ' . $tx->getMessage() . "\n";
}
     
?>

至此一个简单的RPC服务就完成了。


      

转载请注明出处:http://www.xxling.com/article/3110.aspx

  • 分类: PHP
  • 阅读: (3657)
  • 评论: (0)
拍砖 取消
请输入昵称
请输入邮箱
*
 选择评论类型
300字以内  请输入评论内容