Laravel做爲最受歡迎的php web框架一直廣受廣大互聯網公司的喜好。php
筆者也參與過一些由laravel開發的項目。雖然laravel的性能廣受詬病可是業界也有一些比較好的解決方案,好比堆機器,好比使用swoole進行加速。laravel
一個項目立項到開發上線,隨着時間和需求的不斷激增,會愈來愈複雜,變成一個大項目,若是前期項目架構沒設計的很差,代碼會愈來愈臃腫,難以維護,後期的每次產品迭代上線都會牽一髮而動全身。項目微服務化,鬆耦合模塊間的關係,是一個很好的選擇,隨然增長了維護成本,可是仍是很值得的。git
那麼有什麼辦法使一個laravel項目改形成微服務呢?github
最近研究thrift的時候發現thrift對php之城很是好,那麼可不可使用使用thrift做爲rpc框架,使用swoole來實現異步TCP服務,打造一個微服務框架呢。web
心動不如行動咱們開始嘗試一下吧。首先咱們建立一個laravel的項目,筆者使用的laravel官方提供的homestead的環境。apache
laravel new laravel-thrift-app
安裝laravel-s https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md瀏覽器
composer require "hhxsv5/laravel-s:~3.5.0" -vvv
laravel-s是一個由swoole寫的laravel擴展,賦予laravel更好的性能,具體使用方法參看官方文檔。服務器
在項目的根目錄下新建一個thrift的目錄,而後在該子目錄下建立 Thrift IDL 文件 user.thrift,用於定義和用戶相關的服務接口。swoole
1 namespace php App.Thrift.User 2 // 定義用戶接口 3 service User { 4 string getInfo(1:i32 id) 5 }
這裏咱們定義了一個接口,接着在項目根目錄下運行以下命令,根據上述 IDL 文件生成相關的服務代碼:架構
thrift -r --gen php:server -out ./ thrift/user.thrift
查看文件這時候咱們會發如今App\Thrift\User`目錄下生成對應的服務代碼。
經過 Composer 安裝 Thrift PHP 依賴包:
composer require apache/thrift
編寫服務代碼,在 app目錄下新建一個 Services/Server 子目錄,而後在該目錄下建立服務接口類 UserService,該類實現自 `App\Thrift\User\UserIf` 接口:
1 <?php 2 namespace App\Services\Server; 3 4 5 use App\Thrift\User\UserIf; 6 7 class UserService implements UserIf 8 { 9 public function getInfo($id) 10 { 11 return "chenSi".$id; 12 } 13 }
在 app 目錄下新建一個 Sockets目錄用於存放 Swoole 相關代碼,首先咱們建立一個 ServerTransport.php用來存放服務端代理類,並編寫代碼以下:
1 <?php 2 namespace App\Sockets; 3 4 5 use Thrift\Server\TServerTransport; 6 7 class ServerTransport extends TServerTransport 8 { 9 /** 10 * @var array 服務器選項 11 */ 12 public $options = [ 13 'dispatch_mode' => 1, //1: 輪循, 3: 爭搶 14 'open_length_check' => true, //打開包長檢測 15 'package_max_length' => 8192000, //最大的請求包長度,8M 16 'package_length_type' => 'N', //長度的類型,參見PHP的pack函數 17 'package_length_offset' => 0, //第N個字節是包長度的值 18 'package_body_offset' => 4, //從第幾個字節計算長度 19 ]; 20 21 /** 22 * @var SwooleServer 23 */ 24 public $server; 25 protected $host; 26 protected $port; 27 protected $sockType; 28 29 30 public function __construct($swoole, $host, $port = 9999, $sockType = SWOOLE_SOCK_TCP, $options = []) 31 { 32 $this->server = $swoole; 33 $this->host = $host; 34 $this->port = $port; 35 $this->sockType = $sockType; 36 $this->options = array_merge($this->options,$options); 37 38 } 39 40 41 public function listen() 42 { 43 $this->server =$this->server->addlistener($this->host,$this->port,$this->sockType); 44 $this->server->set($this->options); 45 return null; 46 } 47 48 49 public function close() 50 { 51 //$this->server->shutdown(); 52 return null; 53 } 54 55 56 protected function acceptImpl() 57 { 58 return null; 59 } 60 }
咱們在代理類的構造函數中初始化 Swoole TCP 服務器參數,因爲咱們使用的是laravel-s而後在該類中定義 listen 方法啓動這個swoole增長監聽的端口並監聽客戶端請求。
咱們在 app/Sockets目錄下建立 Transport.php文件用於存放基於 Swoole 的傳輸層實現代碼:
1 <?php 2 /** 3 * Created by PhpStorm. 4 * User: 74100 5 * Date: 2019/10/21 6 * Time: 2:22 7 */ 8 namespace App\Sockets; 9 10 use Swoole\Server as SwooleServer; 11 use Thrift\Exception\TTransportException; 12 use Thrift\Transport\TTransport; 13 14 class Transport extends TTransport 15 { 16 /** 17 * @var swoole服務器實例 18 */ 19 protected $server; 20 /** 21 * @var int 客戶端鏈接描述符 22 */ 23 protected $fd = -1; 24 /** 25 * @var string 數據 26 */ 27 protected $data = ''; 28 /** 29 * @var int 數據讀取指針 30 */ 31 protected $offset = 0; 32 33 /** 34 * SwooleTransport constructor. 35 * @param SwooleServer $server 36 * @param int $fd 37 * @param string $data 38 */ 39 public function __construct(SwooleServer $server, $fd, $data) 40 { 41 $this->server = $server; 42 $this->fd = $fd; 43 $this->data = $data; 44 } 45 46 /** 47 * Whether this transport is open. 48 * 49 * @return boolean true if open 50 */ 51 public function isOpen() 52 { 53 return $this->fd > -1; 54 } 55 56 /** 57 * Open the transport for reading/writing 58 * 59 * @throws TTransportException if cannot open 60 */ 61 public function open() 62 { 63 if ($this->isOpen()) { 64 throw new TTransportException('Swoole Transport already connected.', TTransportException::ALREADY_OPEN); 65 } 66 } 67 68 /** 69 * Close the transport. 70 * @throws TTransportException 71 */ 72 public function close() 73 { 74 if (!$this->isOpen()) { 75 throw new TTransportException('Swoole Transport not open.', TTransportException::NOT_OPEN); 76 } 77 $this->server->close($this->fd, true); 78 $this->fd = -1; 79 } 80 81 /** 82 * Read some data into the array. 83 * 84 * @param int $len How much to read 85 * @return string The data that has been read 86 * @throws TTransportException if cannot read any more data 87 */ 88 public function read($len) 89 { 90 if (strlen($this->data) - $this->offset < $len) { 91 throw new TTransportException('Swoole Transport[' . strlen($this->data) . '] read ' . $len . ' bytes failed.'); 92 } 93 $data = substr($this->data, $this->offset, $len); 94 $this->offset += $len; 95 return $data; 96 } 97 98 /** 99 * Writes the given data out. 100 * 101 * @param string $buf The data to write 102 * @throws TTransportException if writing fails 103 */ 104 public function write($buf) 105 { 106 if (!$this->isOpen()) { 107 throw new TTransportException('Swoole Transport not open.', TTransportException::NOT_OPEN); 108 } 109 $this->server->send($this->fd, $buf); 110 } 111 }
Transport類主要用於從傳輸層寫入或讀取數據,最後咱們建立 Server.php 文件,用於存放基於 Swoole 的 RPC 服務器類:
1 <?php 2 /** 3 * Created by PhpStorm. 4 * User: 74100 5 * Date: 2019/10/21 6 * Time: 2:24 7 */ 8 namespace App\Sockets; 9 10 use Swoole\Server as SwooleServer; 11 use Thrift\Server\TServer; 12 13 class Server extends TServer 14 { 15 public function serve() 16 { 17 18 $this->transport_->server->on('receive', [$this, 'handleReceive']); 19 $this->transport_->listen(); 20 21 } 22 23 public function stop() 24 { 25 $this->transport_->close(); 26 } 27 28 /** 29 * 處理RPC請求 30 * @param Server $server 31 * @param int $fd 32 * @param int $fromId 33 * @param string $data 34 */ 35 public function handleReceive(SwooleServer $server, $fd, $fromId, $data) 36 { 37 $transport = new Transport($server, $fd, $data); 38 $inputTransport = $this->inputTransportFactory_->getTransport($transport); 39 $outputTransport = $this->outputTransportFactory_->getTransport($transport); 40 $inputProtocol = $this->inputProtocolFactory_->getProtocol($inputTransport); 41 $outputProtocol = $this->outputProtocolFactory_->getProtocol($outputTransport); 42 $this->processor_->process($inputProtocol, $outputProtocol); 43 } 44 }
該類繼承自 Thrift\Server\TServer,在子類中須要實現 serve` 和 `stop`方法,分別定義服務器啓動和關閉邏輯,這裏咱們在 serve方法中定義了 Swoole TCP 服務器收到請求時的回調處理函數,其中 $this->transport 指向 App\Swoole\ServerTransport 實例,回調函數 handleReceive中咱們會將請求數據傳入傳輸層處理類 Transport進行初始化,而後再經過一系列轉化經過處理器對請求進行處理,該方法中 `$this` 指針指向的屬性都是在外部啓動 RPC 服務器時傳入的,後面咱們會看到。定義好請求回調後,便可經過 `$this->transport_->listen()` 啓動服務器並監聽請求。
最後咱們使用laravel-s的事件回調。
在laravel-s的配置文件新增Master進程啓動時的事件。
'event_handlers' => [ 'ServerStart' => \App\Events\ServerStartEvent::class, ],
編寫ServerStartEvent類。
1 <?php 2 namespace App\Events; 3 use App\Sockets\ServerTransport; 4 use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface; 5 use App\Services\Server\UserService; 6 use App\Sockets\TFramedTransportFactory; 7 use App\Thrift\User\UserProcessor; 8 use Thrift\Factory\TBinaryProtocolFactory; 9 use Swoole\Http\Server; 10 use App\Sockets\Server as TServer; 11 12 13 class ServerStartEvent implements ServerStartInterface 14 { 15 public function __construct() 16 { 17 } 18 public function handle(Server $server) 19 { 20 // 初始化thrift 21 $processor = new UserProcessor(new UserService()); 22 $tFactory = new TFramedTransportFactory(); 23 $pFactory = new TBinaryProtocolFactory(); 24 // 監聽本地 9999 端口,等待客戶端鏈接請求 25 $transport = new ServerTransport($server,'127.0.0.1', 9999); 26 $server = new TServer($processor, $transport, $tFactory, $tFactory, $pFactory, $pFactory); 27 $server->serve(); 28 } 29 }
這時候咱們服務端的代碼已經寫完。開始寫客戶端的代碼。
接下來,咱們來修改客戶端請求服務端遠程接口的代碼,在此以前在 app/Sockets目錄下新建一個 ClientTransport.php 來存放客戶端與服務端通訊的傳輸層實現代碼:
1 <?php 2 namespace App\Sockets; 3 use Swoole\Client; 4 use Thrift\Exception\TTransportException; 5 use Thrift\Transport\TTransport; 6 7 class ClientTransport extends TTransport 8 { 9 /** 10 * @var string 鏈接地址 11 */ 12 protected $host; 13 /** 14 * @var int 鏈接端口 15 */ 16 protected $port; 17 /** 18 * @var Client 19 */ 20 protected $client; 21 22 /** 23 * ClientTransport constructor. 24 * @param string $host 25 * @param int $port 26 */ 27 public function __construct($host, $port) 28 { 29 $this->host = $host; 30 $this->port = $port; 31 $this->client = new Client(SWOOLE_SOCK_TCP); 32 } 33 34 /** 35 * Whether this transport is open. 36 * 37 * @return boolean true if open 38 */ 39 public function isOpen() 40 { 41 return $this->client->sock > 0; 42 } 43 44 /** 45 * Open the transport for reading/writing 46 * 47 * @throws TTransportException if cannot open 48 */ 49 public function open() 50 { 51 if ($this->isOpen()) { 52 throw new TTransportException('ClientTransport already open.', TTransportException::ALREADY_OPEN); 53 } 54 if (!$this->client->connect($this->host, $this->port)) { 55 throw new TTransportException( 56 'ClientTransport could not open:' . $this->client->errCode, 57 TTransportException::UNKNOWN 58 ); 59 } 60 } 61 62 /** 63 * Close the transport. 64 * @throws TTransportException 65 */ 66 public function close() 67 { 68 if (!$this->isOpen()) { 69 throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN); 70 } 71 $this->client->close(); 72 } 73 74 /** 75 * Read some data into the array. 76 * 77 * @param int $len How much to read 78 * @return string The data that has been read 79 * @throws TTransportException if cannot read any more data 80 */ 81 public function read($len) 82 { 83 if (!$this->isOpen()) { 84 throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN); 85 } 86 return $this->client->recv($len, true); 87 } 88 89 /** 90 * Writes the given data out. 91 * 92 * @param string $buf The data to write 93 * @throws TTransportException if writing fails 94 */ 95 public function write($buf) 96 { 97 if (!$this->isOpen()) { 98 throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN); 99 } 100 $this->client->send($buf); 101 } 102 }
咱們在 app/Services/Client 目錄下建立 UserService.php,用於存放 RPC 客戶端鏈接與請求服務接口方法:
1 <?php 2 namespace App\Services\Client; 3 4 use App\Sockets\ClientTransport; 5 use App\Thrift\User\UserClient; 6 use Thrift\Exception\TException; 7 use Thrift\Protocol\TBinaryProtocol; 8 use Thrift\Protocol\TMultiplexedProtocol; 9 use Thrift\Transport\TBufferedTransport; 10 use Thrift\Transport\TFramedTransport; 11 use Thrift\Transport\TSocket; 12 13 class UserService 14 { 15 public function getUserInfoViaSwoole(int $id) 16 { 17 try { 18 // 創建與 SwooleServer 的鏈接 19 $socket = new ClientTransport("127.0.0.1", 9999); 20 21 $transport = new TFramedTransport($socket); 22 $protocol = new TBinaryProtocol($transport); 23 $client = new UserClient($protocol); 24 $transport->open(); 25 26 $result = $client->getInfo($id); 27 28 $transport->close(); 29 return $result; 30 } catch (TException $TException) { 31 dd($TException); 32 } 33 } 34 }
測試,新增一個路由。
1 Route::get('/user/{id}', function($id) { 2 $userService = new UserService(); 3 $user = $userService->getUserInfoViaSwoole($id); 4 return $user; 5 });
啓動laravel-s。
php bin/laravels start
在瀏覽器中輸入 http://192.168.10.100:5200/user/2 (192.168.10.100爲我homestead設置的地址,5200爲laravel-s設置的端口號)
這時候咱們就會發現瀏覽器上面出現chensi2幾個大字。一個由larave+thrift+swoole搭建的微服務框架就這樣完成了。端口號固定9999也可使用consul作服務發現。
固然了有興趣的能夠寫一個package本身去實現而不用laravels這個擴展。