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 的传输层实现代码:
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 来存放客户端与服务端通信的传输层实现代码:
我们在 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。
在浏览器中输入 http://192.168.10.100:5200/user/2 (192.168.10.100为我homestead设置的地址,5200为laravel-s设置的端口号)
这时候我们就会发现浏览器上面出现chensi2几个大字。一个由larave+thrift+swoole搭建的微服务框架就这样完成了。端口号固定9999也可以使用consul做服务发现。
当然了有兴趣的可以写一个package自己去实现而不用laravels这个扩展。