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

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 

composer require "hhxsv5/laravel-s:~3.5.0" -vvv 

laravel-s是一个由swoole写的laravel扩展,赋予laravel更好的性能,具体使用方法参看官方文档。

在项目的根目录下新建一个thrift的目录,然后在该子目录下创建 Thrift IDL 文件 user.thrift,用于定义和用户相关的服务接口。

 namespace php App.Thrift.User  
 // 定义用户接口 
 service User {     
 string getInfo(1:i32 id)
  }

这里我们定义了一个接口,接着在项目根目录下运行如下命令,根据上述 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` 接口:

<?php
namespace App\Services\Server;


use App\Thrift\User\UserIf;

class UserService implements UserIf
{
    public function getInfo($id)
    {
        return "chenSi".$id;
    }
}

在 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 的传输层实现代码:

<?php
/**
 * Created by PhpStorm.
 * User: 74100
 * Date: 2019/10/21
 * Time: 2:22
 */
namespace App\Sockets;

use Swoole\Server as SwooleServer;
use Thrift\Exception\TTransportException;
use Thrift\Transport\TTransport;

class Transport extends TTransport
{
    /**
     * @var swoole服务器实例
     */
    protected $server;
    /**
     * @var int 客户端连接描述符
     */
    protected $fd = -1;
    /**
     * @var string 数据
     */
    protected $data = '';
    /**
     * @var int 数据读取指针
     */
    protected $offset = 0;

    /**
     * SwooleTransport constructor.
     * @param SwooleServer $server
     * @param int $fd
     * @param string $data
     */
    public function __construct(SwooleServer $server, $fd, $data)
    {
        $this->server = $server;
        $this->fd = $fd;
        $this->data = $data;
    }

    /**
     * Whether this transport is open.
     *
     * @return boolean true if open
     */
    public function isOpen()
    {
        return $this->fd > -1;
    }

    /**
     * Open the transport for reading/writing
     *
     * @throws TTransportException if cannot open
     */
    public function open()
    {
        if ($this->isOpen()) {
            throw new TTransportException('Swoole Transport already connected.', TTransportException::ALREADY_OPEN);
        }
    }

    /**
     * Close the transport.
     * @throws TTransportException
     */
    public function close()
    {
        if (!$this->isOpen()) {
            throw new TTransportException('Swoole Transport not open.', TTransportException::NOT_OPEN);
        }
        $this->server->close($this->fd, true);
        $this->fd = -1;
    }

    /**
     * Read some data into the array.
     *
     * @param int $len How much to read
     * @return string The data that has been read
     * @throws TTransportException if cannot read any more data
     */
    public function read($len)
    {
        if (strlen($this->data) - $this->offset < $len) {
            throw new TTransportException('Swoole Transport[' . strlen($this->data) . '] read ' . $len . ' bytes failed.');
        }
        $data = substr($this->data, $this->offset, $len);
        $this->offset += $len;
        return $data;
    }

    /**
     * Writes the given data out.
     *
     * @param string $buf The data to write
     * @throws TTransportException if writing fails
     */
    public function write($buf)
    {
        if (!$this->isOpen()) {
            throw new TTransportException('Swoole Transport not open.', TTransportException::NOT_OPEN);
        }
        $this->server->send($this->fd, $buf);
    }
}

Transport类主要用于从传输层写入或读取数据,最后我们创建 Server.php 文件,用于存放基于 Swoole 的 RPC 服务器类:

<?php
/**
 * Created by PhpStorm.
 * User: 74100
 * Date: 2019/10/21
 * Time: 2:24
 */
namespace App\Sockets;

use Swoole\Server as SwooleServer;
use Thrift\Server\TServer;

class Server extends TServer
{
    public function serve()
    {

        $this->transport_->server->on('receive', [$this, 'handleReceive']);
        $this->transport_->listen();

    }

    public function stop()
    {
        $this->transport_->close();
    }

    /**
     * 处理RPC请求
     * @param Server $server
     * @param int $fd
     * @param int $fromId
     * @param string $data
     */
    public function handleReceive(SwooleServer $server, $fd, $fromId, $data)
    {
        $transport = new Transport($server, $fd, $data);
        $inputTransport = $this->inputTransportFactory_->getTransport($transport);
        $outputTransport = $this->outputTransportFactory_->getTransport($transport);
        $inputProtocol = $this->inputProtocolFactory_->getProtocol($inputTransport);
        $outputProtocol = $this->outputProtocolFactory_->getProtocol($outputTransport);
        $this->processor_->process($inputProtocol, $outputProtocol);
    }
}

该类继承自 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类。

<?php
namespace App\Events;
use App\Sockets\ServerTransport;
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
use App\Services\Server\UserService;
use App\Sockets\TFramedTransportFactory;
use App\Thrift\User\UserProcessor;
use Thrift\Factory\TBinaryProtocolFactory;
use Swoole\Http\Server;
use App\Sockets\Server as TServer;


class ServerStartEvent implements ServerStartInterface
{
    public function __construct()
    {
    }
    public function handle(Server $server)
    {
        // 初始化thrift
        $processor = new UserProcessor(new UserService());
        $tFactory = new TFramedTransportFactory();
        $pFactory = new TBinaryProtocolFactory();
        // 监听本地 9999 端口,等待客户端连接请求
        $transport = new ServerTransport($server,'127.0.0.1', 9999);
        $server = new TServer($processor, $transport, $tFactory, $tFactory, $pFactory, $pFactory);
        $server->serve();
    }
}

这时候我们服务端的代码已经写完。开始写客户端的代码。

接下来,我们来修改客户端请求服务端远程接口的代码,在此之前在 app/Sockets目录下新建一个 ClientTransport.php 来存放客户端与服务端通信的传输层实现代码:

<?php
namespace App\Sockets;
    use Swoole\Client;
    use Thrift\Exception\TTransportException;
    use Thrift\Transport\TTransport;

    class ClientTransport extends TTransport
    {
        /**
         * @var string 连接地址
         */
        protected $host;
        /**
         * @var int 连接端口
         */
        protected $port;
        /**
         * @var Client
         */
        protected $client;

        /**
         * ClientTransport constructor.
         * @param string $host
         * @param int $port
         */
        public function __construct($host, $port)
        {
            $this->host = $host;
            $this->port = $port;
            $this->client = new Client(SWOOLE_SOCK_TCP);
        }

        /**
         * Whether this transport is open.
         *
         * @return boolean true if open
         */
        public function isOpen()
        {
            return $this->client->sock > 0;
        }

        /**
         * Open the transport for reading/writing
         *
         * @throws TTransportException if cannot open
         */
        public function open()
        {
            if ($this->isOpen()) {
                throw new TTransportException('ClientTransport already open.', TTransportException::ALREADY_OPEN);
            }
            if (!$this->client->connect($this->host, $this->port)) {
                throw new TTransportException(
                    'ClientTransport could not open:' . $this->client->errCode,
                    TTransportException::UNKNOWN
                );
            }
        }

        /**
         * Close the transport.
         * @throws TTransportException
         */
        public function close()
        {
            if (!$this->isOpen()) {
                throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN);
            }
            $this->client->close();
        }

        /**
         * Read some data into the array.
         *
         * @param int $len How much to read
         * @return string The data that has been read
         * @throws TTransportException if cannot read any more data
         */
        public function read($len)
        {
            if (!$this->isOpen()) {
                throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN);
            }
            return $this->client->recv($len, true);
        }

        /**
         * Writes the given data out.
         *
         * @param string $buf The data to write
         * @throws TTransportException if writing fails
         */
        public function write($buf)
        {
            if (!$this->isOpen()) {
                throw new TTransportException('ClientTransport not open.', TTransportException::NOT_OPEN);
            }
            $this->client->send($buf);
        }
    }

我们在 app/Services/Client 目录下创建 UserService.php,用于存放 RPC 客户端连接与请求服务接口方法:

<?php
    namespace App\Services\Client;

    use App\Sockets\ClientTransport;
    use App\Thrift\User\UserClient;
    use Thrift\Exception\TException;
    use Thrift\Protocol\TBinaryProtocol;
    use Thrift\Protocol\TMultiplexedProtocol;
    use Thrift\Transport\TBufferedTransport;
    use Thrift\Transport\TFramedTransport;
    use Thrift\Transport\TSocket;

    class UserService
    {
        public function getUserInfoViaSwoole(int $id)
        {
            try {
                // 建立与 SwooleServer 的连接
                $socket = new ClientTransport("127.0.0.1", 9999);

                $transport = new TFramedTransport($socket);
                $protocol = new TBinaryProtocol($transport);
                $client = new UserClient($protocol);
                $transport->open();

                $result = $client->getInfo($id);

                $transport->close();
                return $result;
            } catch (TException $TException) {
                dd($TException);
            }
        }
    }

测试,新增一个路由。

 Route::get('/user/{id}', function($id) {
     $userService = new UserService();
     $user = $userService->getUserInfoViaSwoole($id);
     return $user;
 });

启动laravel-s。

php bin/laravels start 

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

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

 

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

摘自:https://www.cnblogs.com/a609251438/p/11811665.html

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