一、安裝篇
博主注:截至2017-10-10,官網上thrift最新版0.10.0一直無法成功編譯。所以,請選擇0.9.3版本,避免走各種彎路:
wget http://apache.fayea.com/thrift/0.9.3/thrift-0.9.3.tar.gz
1、安裝開發平台工具
yum -y groupinstall "Development Tools"
2、安裝autoconf
wget http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz
tar xvf autoconf-2.69.tar.gz
cd autoconf-2.69
./configure --prefix=/usr/local
make
make install
3、安裝automake
wget http://ftp.gnu.org/gnu/automake/automake-1.14.tar.gz
tar xvf automake-1.14.tar.gz
cd automake-1.14
./configure --prefix=/usr/local
make
make install
4、安裝bison
wget http://ftp.gnu.org/gnu/bison/bison-2.5.1.tar.gz
tar xvf bison-2.5.1.tar.gz
cd bison-2.5.1
./configure --prefix=/usr/local
make
make install
5、安裝C++庫依賴包
yum -y install libevent-devel zlib-devel openssl-devel
6、安裝boost
wget http://sourceforge.net/projects/boost/files/boost/1.53.0/boost_1_53_0.tar.gz
tar xvf boost_1_53_0.tar.gz
cd boost_1_53_0
./bootstrap.sh
./b2
7、安裝其它依賴包
yum install gcc gcc-c++ bzip2 bzip2-devel bzip2-libs python-devel -y
8、安裝thrift(基本的編譯套路)
cd thrift
./bootstrap.sh
./configure --with-lua=no --prefix=/alidata/server/thrift
make
make install
二、Hello World篇
使用套路總結:
1、用IDL語法,定義自己的數據結構與服務接口:
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/tutorial.thrift
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/shared.thrift
將這兩個文件下載到本地,分別保存為tutorial.thrift和shared.thrift,並將這兩個文件傳到與thrift同目錄下。
附:
IDL詳細教程:http://thrift.apache.org/docs/idl
IDL示例(夠用了):https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/tutorial.thrift
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/shared.thrift
2、用thrift將上面的thrift的IDL文件,轉成對應語言的類:
thrift -gen php:server tutorial.thrift
上面這一句是對應服務端的轉換,客戶端的轉換不用:server;但是服務端方式生成的類文件可以供客戶端使用。
3、轉換完成后,在本目錄下會得到一個gen-php目錄,里面有三個文件tutorial.php、shared.php和Types.php。
tutorial.php里面有一個CalculatorIf接口,這個類是我們要實現的服務端業務接口。
shared.php被引用在tutorial.php文件中。
Types.php里面是自定義數據類型。
4、實現server端,即需要創建一個Handler類實再業務接口CalculatorIf
注意:這里的lib文件在thrift源碼包lib里,我們移到自己的php需要的路徑中。代碼中require_once部分、$GEN_DIR部分、Thrift命名空間注冊部分的路徑需要根據實際情況進行更改
<?php namespace tutorial\php; error_reporting(E_ALL); require_once __DIR__.'/../../lib/php/lib/Thrift/ClassLoader/ThriftClassLoader.php'; use Thrift\ClassLoader\ThriftClassLoader; $GEN_DIR = realpath(dirname(__FILE__).'/..').'/gen-php'; $loader = new ThriftClassLoader(); $loader->registerNamespace('Thrift', __DIR__ . '/../../lib/php/lib'); $loader->registerDefinition('shared', $GEN_DIR); $loader->registerDefinition('tutorial', $GEN_DIR); $loader->register(); if (php_sapi_name() == 'cli') { ini_set("display_errors", "stderr"); } use Thrift\Protocol\TBinaryProtocol; use Thrift\Transport\TPhpStream; use Thrift\Transport\TBufferedTransport; class CalculatorHandler implements \tutorial\CalculatorIf { protected $log = array(); public function ping() { error_log("ping()"); } public function add($num1, $num2) { error_log("add({$num1}, {$num2})"); return $num1 + $num2; } public function calculate($logid, \tutorial\Work $w) { error_log("calculate({$logid}, {{$w->op}, {$w->num1}, {$w->num2}})"); switch ($w->op) { case \tutorial\Operation::ADD: $val = $w->num1 + $w->num2; break; case \tutorial\Operation::SUBTRACT: $val = $w->num1 - $w->num2; break; case \tutorial\Operation::MULTIPLY: $val = $w->num1 * $w->num2; break; case \tutorial\Operation::DIVIDE: if ($w->num2 == 0) { $io = new \tutorial\InvalidOperation(); $io->whatOp = $w->op; $io->why = "Cannot divide by 0"; throw $io; } $val = $w->num1 / $w->num2; break; default: $io = new \tutorial\InvalidOperation(); $io->whatOp = $w->op; $io->why = "Invalid Operation"; throw $io; } $log = new \shared\SharedStruct(); $log->key = $logid; $log->value = (string)$val; $this->log[$logid] = $log; return $val; } public function getStruct($key) { error_log("getStruct({$key})"); // This actually doesn't work because the PHP interpreter is // restarted for every request. //return $this->log[$key]; return new \shared\SharedStruct(array("key" => $key, "value" => "PHP is stateless!")); } public function zip() { error_log("zip()"); } }; header('Content-Type', 'application/x-thrift'); if (php_sapi_name() == 'cli') { echo "\r\n"; } $handler = new CalculatorHandler(); $processor = new \tutorial\CalculatorProcessor($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();
5、實現client端:
同樣需要注意:這里的lib文件在thrift源碼包lib里,我們移到自己的php需要的路徑中。代碼中require_once部分、$GEN_DIR部分、Thrift命名空間注冊部分的路徑需要根據實際情況進行更改
<?php namespace tutorial\php; error_reporting(E_ALL); require_once __DIR__.'/../../lib/php/lib/Thrift/ClassLoader/ThriftClassLoader.php'; use Thrift\ClassLoader\ThriftClassLoader; $GEN_DIR = realpath(dirname(__FILE__).'/..').'/gen-php'; $loader = new ThriftClassLoader(); $loader->registerNamespace('Thrift', __DIR__ . '/../../lib/php/lib'); $loader->registerDefinition('shared', $GEN_DIR); $loader->registerDefinition('tutorial', $GEN_DIR); $loader->register(); use Thrift\Protocol\TBinaryProtocol; use Thrift\Transport\TSocket; use Thrift\Transport\THttpClient; use Thrift\Transport\TBufferedTransport; use Thrift\Exception\TException; try { if (array_search('--http', $argv)) { $socket = new THttpClient('localhost', 8080, '/php/PhpServer.php'); } else { $socket = new TSocket('localhost', 9090); } $transport = new TBufferedTransport($socket, 1024, 1024); $protocol = new TBinaryProtocol($transport); $client = new \tutorial\CalculatorClient($protocol); $transport->open(); $client->ping(); print "ping()\n"; $sum = $client->add(1,1); print "1+1=$sum\n"; $work = new \tutorial\Work(); $work->op = \tutorial\Operation::DIVIDE; $work->num1 = 1; $work->num2 = 0; try { $client->calculate(1, $work); print "Whoa! We can divide by zero?\n"; } catch (\tutorial\InvalidOperation $io) { print "InvalidOperation: $io->why\n"; } $work->op = \tutorial\Operation::SUBTRACT; $work->num1 = 15; $work->num2 = 10; $diff = $client->calculate(1, $work); print "15-10=$diff\n"; $log = $client->getStruct(1); print "Log: $log->value\n"; $transport->close(); } catch (TException $tx) { print 'TException: '.$tx->getMessage()."\n"; } ?>
6、因為php的server端代碼沒有進行端口偵聽的網絡服務功能,所以要依靠python來提供這個功能。我們把下面的腳本命名為s.py,需要與server.php放在同一個目錄下(/alidata/www/thrift)
#!/bin/py #coding=utf-8 import os import BaseHTTPServer import CGIHTTPServer # chdir(2) into the tutorial directory. os.chdir('/alidata/www/thrift') # 指定目錄 ,如果目錄錯誤 請求會失敗 class Handler(CGIHTTPServer.CGIHTTPRequestHandler): cgi_directories = ['/'] BaseHTTPServer.HTTPServer(('', 8080), Handler).serve_forever()
7、到現在為止,hello world級別的工程已經完成。
運行服務端s.py
運行客戶端php client.php --http
便可以看到響應的實際效果
三、生產使用篇
hello world級別的工程,只有一個進程進行偵聽和服務,在多核CPU的時代,只有一個進程不但無法充分利用CPU的性能,也無法為用戶提供最高效的服務。
方案一:用php自己實現網絡服務方案(這是一個牛人寫的多進程服務demo,完全可以參照) http://blog.csdn.net/flynetcn/article/details/47837975
<?php /** * 多進程形式的server. * @package thrift.server * @author flynetcn */ namespace Thrift\Server; use Thrift\Server\TServer; use Thrift\Transport\TTransport; use Thrift\Exception\TException; use Thrift\Exception\TTransportException; class TMultiProcessServer extends TServer { /** * 捕獲的信號編號 */ static $catchQuitSignal = 0; /** * worker進程數量 */ private $workProcessNum = 4; /** * 每個worker進程處理的最大請求數 */ private $maxWorkRequestNum = 2000; /** * 當前worker進程已處理的請求數 */ private $currentWorkRequestNum = 0; /** * 當前連接調用次數 */ private $currentConnectCallNum = 0; /** * 發送超時 */ private $sendTimeoutSec = 1; /** * 接收超時 */ private $recvTimeoutSec = 1; /** * 當前進程pid */ private $pid = 0; /** * Flag for the main serving loop */ private $stop_ = false; /** * List of children. */ protected $childrens = array(); /** * 服務器日志文件 */ protected static $logFiles; protected static $pidFile; /** * run */ public function serve($daemon=false, array $config=array()) { if (isset($config['workProcessNum'])) { $this->workProcessNum = intval($config['workProcessNum']); } if ($this->workProcessNum < 1) { self::log(1, "child workProcessNum can not be less than 1"); throw new TException('child workProcessNum can not be less than 1'); } if (isset($config['maxWorkRequestNum'])) { $this->maxWorkRequestNum = intval($config['maxWorkRequestNum']); } if ($this->maxWorkRequestNum < 1) { self::log(1, "child maxWorkRequestNum can not be less than 1"); throw new TException('child maxWorkRequestNum can not be less than 1'); } if (isset($config['sendTimeoutSec'])) { $this->sendTimeoutSec = intval($config['sendTimeoutSec']); } if (isset($config['recvTimeoutSec'])) { $this->recvTimeoutSec = intval($config['recvTimeoutSec']); } if ($daemon) { $this->daemon(); $this->registerSignalHandler(); self::$logFiles = isset($config['logFiles']) && is_array($config['logFiles']) ? $config['logFiles'] : array(); self::$pidFile = isset($config['pidFile']) ? $config['pidFile'] : ''; declare(ticks=3); } $this->pid = posix_getpid(); self::createPidFile($this->pid); self::log(0, "manage process({$this->pid}) has started"); $this->transport_->listen(); while (!$this->stop_) { while ($this->workProcessNum > 0) { try { $pid = pcntl_fork(); if ($pid > 0) { $this->handleParent($pid, $this->workProcessNum); } else if ($pid === 0) { $this->pid = posix_getpid(); $this->handleChild($this->workProcessNum); } else { self::log(1, "Failed to fork"); throw new TException('Failed to fork'); } $this->workProcessNum--; } catch (Exception $e) { } } $this->collectChildren(); sleep(2); if (\Thrift\Server\TMultiProcessServer::$catchQuitSignal) { $this->stop(); } } } public function getCurrentWorkRequestNum() { return $this->currentWorkRequestNum; } public function getCurrentConnectCallNum() { return $this->currentConnectCallNum; } /** * Code run by the parent * * @param int $pid * @param int $num 進程編號 * @return void */ private function handleParent($pid, $num) { $this->childrens[$pid] = $num; } /** * Code run by the child. * * @param int $num 進程編號 * @return void */ private function handleChild($num) { self::log(0, "child process($this->pid) has started"); $this->childrens = array(); while (!$this->stop_) { try { $transport = $this->transport_->accept(); if ($transport != null) { $transport->setSendTimeout($this->sendTimeoutSec * 1000); $transport->setRecvTimeout($this->recvTimeoutSec * 1000); $this->currentWorkRequestNum++; $this->currentConnectCallNum = 0; $inputTransport = $this->inputTransportFactory_->getTransport($transport); $outputTransport = $this->outputTransportFactory_->getTransport($transport); $inputProtocol = $this->inputProtocolFactory_->getProtocol($inputTransport); $outputProtocol = $this->outputProtocolFactory_->getProtocol($outputTransport); while ($this->processor_->process($inputProtocol, $outputProtocol)) { $this->currentConnectCallNum++; } @$transport->close(); } } catch (TTransportException $e) { } catch (Exception $e) { self::log(1, $e->getMessage().'('.$e->getCode().')'); } if (\Thrift\Server\TMultiProcessServer::$catchQuitSignal) { $this->stop(); } if ($this->currentWorkRequestNum >= $this->maxWorkRequestNum) { self::log(0, "child process($this->pid) has processe {$this->currentWorkRequestNum} requests will be exit"); $this->stop(); break; } } exit(0); } /** * Collects any children we may have * * @return void */ private function collectChildren() { foreach ($this->childrens as $pid => $num) { if (pcntl_waitpid($pid, $status, WNOHANG) > 0) { unset($this->childrens[$pid]); $this->workProcessNum++; } } } /** * @return void */ public function stop() { $this->transport_->close(); $this->stop_ = true; foreach ($this->childrens as $pid => $num) { if (!posix_kill($pid, SIGTERM)) { } } } /** * 附加信號處理 */ public static function sig_handler($signo) { switch ($signo) { case SIGTERM: case SIGHUP: case SIGQUIT: case SIGTSTP: $pid = posix_getpid(); self::log(0, "process($pid) catch signo: $signo"); \Thrift\Server\TMultiProcessServer::$catchQuitSignal = $signo; break; default: } } /** * 附加信號處理 */ private function registerSignalHandler() { pcntl_signal(SIGTERM, '\Thrift\Server\TMultiProcessServer::sig_handler'); pcntl_signal(SIGHUP, '\Thrift\Server\TMultiProcessServer::sig_handler'); pcntl_signal(SIGQUIT, '\Thrift\Server\TMultiProcessServer::sig_handler'); pcntl_signal(SIGTSTP, '\Thrift\Server\TMultiProcessServer::sig_handler'); declare(ticks=3); } /** * 附加守護進程方式 */ private function daemon() { if (!function_exists('posix_setsid')) { return; } if (($pid1 = pcntl_fork()) != 0) { exit; } posix_setsid(); if (($pid2 = pcntl_fork()) != 0) { exit; } } public static function log($type, $msg) { static $fds; $msg = date('Y-m-d H:i:s')." $type {$msg}\n"; if (isset(self::$logFiles[$type]) && self::$logFiles[$type]) { if (file_exists(self::$logFiles[$type])) { if (empty($fds[$type])) { $fds[$type] = fopen(self::$logFiles[$type], 'a'); } if (!$fds[$type]) { $fds[$type] = fopen('php://stdout', 'w'); fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING fopen(".self::$logFiles[$type].") failed\n"); } } else { if (!is_dir(dirname(self::$logFiles[$type])) && !mkdir(dirname(self::$logFiles[$type]), 0755, true)) { $fds[$type] = fopen('php://stdout', 'w'); fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING mkdir(".self::$logFiles[$type].") failed\n"); } elseif (!($fds[$type] = fopen(self::$logFiles[$type], 'a'))) { $fds[$type] = fopen('php://stdout', 'w'); fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING fopen(".self::$logFiles[$type].") failed\n"); } } } else { $fds[$type] = fopen('php://stdout', 'w'); } $ret = fwrite($fds[$type], $msg); if (!$ret && self::$logFiles[$type]) { fclose($fds[$type]); $fds[$type] = fopen(self::$logFiles[$type], 'a'); $ret = fwrite($fds[$type], $msg); } return true; } public static function createPidFile($pid=0) { if (!$pid) { $pid = posix_getpid(); } if (file_exists(self::$pidFile)) { $fd = fopen(self::$pidFile, 'w'); if (!$fd) { self::log(1, "fopen(".self::$pidFile.") failed"); return false; } } else { if (!is_dir(dirname(self::$pidFile)) && !mkdir(dirname(self::$pidFile), 0755, true)) { self::log(1, "mkdir(".self::$pidFile.") failed"); return false; } elseif (!($fd = fopen(self::$pidFile, 'w'))) { self::log(1, "fopen(".self::$pidFile.") failed"); return false; } } if (!fwrite($fd, "$pid")) { self::log(1, "fwrite(".self::$pidFile.",$pid) failed"); return false; } fclose($fd); return true; } }
方案二:方案一固然不錯,但是因為沒有實踐的檢驗,所以用起來還是有一點擔心的。所以,個人建議采用workman的thrift方案,請參考:http://www.workerman.net/workerman-thrift
1、因為要使用多進程,所以需要先安裝pcntl擴展。(安裝過程很簡單,不是重點,不詳細描述過程)
2、workman-thrift下載解壓后,目錄中Applications/ThriftRpc/Services存放的自己的服務端服務,即業務接口If、自定義數據類型Type和業務接口的實現。
3、根目錄下的start.php加載並運行了Applications下的各個服務的start.php文件
4、解壓后的目錄中Applications/ThriftRpc是一個HelloWorld服務的demo。
5、現在,看Applications/ThriftRpc/start.php中的關鍵代碼:
$worker = new ThriftWorker('tcp://0.0.0.0:9090'); $worker->count = 16; $worker->class = 'HelloWorld';
這里,設置了偵聽的IP與端口、設置了服務進程數量與服務的類
6、查看Applications/ThriftPrc/ThriftWorker.php中的關鍵代碼:
// 載入該服務下的所有文件 foreach(glob(THRIFT_ROOT . '/Services/'.$this->class.'/*.php') as $php_file) { require_once $php_file; }
// 檢查類是否存在 $processor_class_name = "\\Services\\".$this->class."\\".$this->class.'Processor'; if(!class_exists($processor_class_name)) { ThriftWorker::log("Class $processor_class_name not found" ); return; } // 檢查類是否存在 $handler_class_name ="\\Services\\".$this->class."\\".$this->class.'Handler'; if(!class_exists($handler_class_name)) { ThriftWorker::log("Class $handler_class_name not found" ); return; }
根據第5步指定的class,在ThriftWorker中會載入對應路徑下的所有php文件進行檢查,並檢查對應的Processor和Handler類是否存在。最終創建Handler和Processor
7、運行根目錄下的php start.php start,可以看到
恭喜,服務端運行成功。
8、關於客戶端
復制 http://www.workerman.net/workerman-thrift 上的客戶端代碼,即可正常運行。
<?php // 引入客戶端文件 require_once __DIR__.'/Applications/ThriftRpc/Clients/ThriftClient.php'; use ThriftClient\ThriftClient; // 傳入配置,一般在某統一入口文件中調用一次該配置接口即可 thriftClient::config( array( 'HelloWorld' => array( 'addresses' => array( '127.0.0.1:9090', '127.0.0.2:9191', ), 'thrift_protocol' => 'TBinaryProtocol',//不配置默認是TBinaryProtocol,對應服務端HelloWorld.conf配置中的thrift_protocol 'thrift_transport' => 'TBufferedTransport',//不配置默認是TBufferedTransport,對應服務端HelloWorld.conf配置中的thrift_transport ), 'DInfoCenter' => array( 'addresses' => array( '127.0.0.1:9090', ), 'thrift_protocol' => 'TBinaryProtocol',//不配置默認是TBinaryProtocol,對應服務端HelloWorld.conf配置中的thrift_protocol 'thrift_transport' => 'TBufferedTransport',//不配置默認是TBufferedTransport,對應服務端HelloWorld.conf配置中的thrift_transport ), ) ); // ========= 以上在WEB入口文件中調用一次即可 =========== // ========= 以下是開發過程中的調用示例 ========== // 初始化一個HelloWorld的實例 $client = ThriftClient::instance('HelloWorld'); // --------同步調用實例---------- var_export($client->sayHello('JIM'); //var_export($client->recv_request("req",strval(time()))); // --------異步調用示例----------- /* // 異步調用 之 發送請求給服務端(注意:異步發送請求格式統一為 asend_XXX($arg),既在原有方法名前面增加'asend_'前綴) $client->asend_sayHello("JERRY"); $client->asend_sayHello("KID"); // 這里是其它業務邏輯 sleep(1); // 異步調用 之 接收服務端的回應(注意:異步接收請求格式統一為 arecv_XXX($arg),既在原有方法名前面增加'arecv_'前綴) var_export($client->arecv_sayHello("KID")); var_export($client->arecv_sayHello("JERRY")); */
9、截止這里,已經可以成功運行workman-thrift的hello world工程
10、關於定義自己的服務:
到這里根據了解,我們可以很容易自己的的服務。比如,我們想定義自己的服務,DInfoCenter,這個過程中有三個一定需要注意的地方:
第一點:目錄名稱需要改成DInfoCenter
第二點:三個文件的namespace聲明(第一行),一定要改為:namespace Services\DInfoCenter;
第三點:默認生成的DInfoCenter.php中,下面很多類以及函數的調用,都帶有命名空間(\DInfoCenter),需要把這一段刪掉
到這里,就大致完成了,最后將start.php中的class賦值成DInfoCenter,就全部完成了。
第一篇無錯版的Thrift php教程,到此完成。只要你按上面的步驟,就能實現thrift php的服務端與客戶端。
這是第一篇,但不會是最后一篇關於thrift php的文章,關於服務端使用TMultiplexedProcessor與workman-thrift進行整合,請拭目以待:)