PHP laravel+thrift+swoole打造微服务框架
laravel作为最受欢迎的php web框架一直广受广大互联网公司的喜爱。
笔者也参与过一些由laravel开发的项目。虽然laravel的性能广受诟病但是业界也有一些比较好的解决方案,比如堆机器,比如使用swoole进行加速。
一个项目立项到开发上线,随着时间和需求的不断激增,会越来越复杂,变成一个大项目,如果前期项目架构没设计的不好,代码会越来越臃肿,难以维护,后期的每次产品迭代上线都会牵一发而动全身。项目微服务化,松耦合模块间的关系,是一个很好的选择,随然增加了维护成本,但是还是很值得的。
那么有什么办法使一个laravel项目改造成微服务呢?
最近研究thrift的时候发现thrift对php之城非常好,那么可不可以使用使用thrift作为rpc框架,使用swoole来实现异步tcp服务,打造一个微服务框架呢。
心动不如行动我们开始尝试一下吧。首先我们创建一个laravel的项目,笔者使用的laravel官方提供的homestead的环境。
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,用于定义和用户相关的服务接口。
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这个扩展。
上一篇: 关于工作中.net转java遇到的一个远程调用传递重复参的问题。
下一篇: java嵌套接口
推荐阅读
-
PHP laravel+thrift+swoole打造微服务框架
-
微信小程序图片选择、上传到服务器、预览(PHP)实现实例
-
php打造属于自己的MVC框架
-
PHP开发之用微信远程遥控服务器
-
FF.PyAdmin 接口服务/后台管理微框架 (Flask+LayUI)
-
PHP从零开始打造自己的MVC框架之类的自动加载实现方法详解
-
PHP从零开始打造自己的MVC框架之入口文件实现方法详解
-
PHP从零开始打造自己的MVC框架之路由类实现方法分析
-
微信小程序项目开发实战:用WePY、mpvue、Taro打造高效的小程序》(笔记4)支持React.js语法的Taro框架
-
php使用workman框架实现socket服务以及连接客户端