欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

PHP laravel+thrift+swoole打造微服务框架

程序员文章站 2023-11-10 21:56:40
Laravel作为最受欢迎的php web框架一直广受广大互联网公司的喜爱。 笔者也参与过一些由laravel开发的项目。虽然laravel的性能广受诟病但是业界也有一些比较好的解决方案,比如堆机器,比如使用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

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 

 

在浏览器中输入 (192.168.10.100为我homestead设置的地址,5200为laravel-s设置的端口号)

这时候我们就会发现浏览器上面出现chensi2几个大字。一个由larave+thrift+swoole搭建的微服务框架就这样完成了。端口号固定9999也可以使用consul做服务发现。

 

当然了有兴趣的可以写一个package自己去实现而不用laravels这个扩展。