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,用于定义和用户相关的服务接口。

 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用来存放服务端代理类,并编写代码如下:

 <?php
namespace App\Sockets; use Thrift\Server\TServerTransport; class ServerTransport extends TServerTransport
{
/**
* @var array 服务器选项
*/
public $options = [
'dispatch_mode' => 1, //1: 轮循, 3: 争抢
'open_length_check' => true, //打开包长检测
'package_max_length' => 8192000, //最大的请求包长度,8M
'package_length_type' => 'N', //长度的类型,参见PHP的pack函数
'package_length_offset' => 0, //第N个字节是包长度的值
'package_body_offset' => 4, //从第几个字节计算长度
]; /**
* @var SwooleServer
*/
public $server;
protected $host;
protected $port;
protected $sockType; public function __construct($swoole, $host, $port = 9999, $sockType = SWOOLE_SOCK_TCP, $options = [])
{
$this->server = $swoole;
$this->host = $host;
$this->port = $port;
$this->sockType = $sockType;
$this->options = array_merge($this->options,$options); } public function listen()
{
$this->server =$this->server->addlistener($this->host,$this->port,$this->sockType);
$this->server->set($this->options);
return null;
} public function close()
{
//$this->server->shutdown();
return null;
} protected function acceptImpl()
{
return null;
}
}

我们在代理类的构造函数中初始化 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 

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

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

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

  • 多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要的加群(点击→)677079770

PHP laravel+thrift+swoole打造微服务框架的更多相关文章

  1. 基于thrift的微服务框架

    前一阵开源过一个基于spring-boot的rest微服务框架,今天再来一篇基于thrift的微服务加框,thrift是啥就不多了,大家自行百度或参考我之前介绍thrift的文章, thrift不仅支 ...

  2. Thrift 个人实战--Thrift RPC服务框架日志的优化

    前言: Thrift作为Facebook开源的RPC框架, 通过IDL中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码. 不过Thrift的实现, 简单使用离实际生产环境还 ...

  3. laravel 整合 swoole ,并简单 ab 测试对比性能以及在 PHPstorm 中利用debug调试配置swoole服务中的PHP代码

    安装PHP 的 swoole 扩展 及 安装 laravel,就不描述了 整合 laravel 和 swoole 用了这个轮子,侵入性很小,一行代码搞定,推荐一下,今天刚用,不能预测未来是否会遇见坑 ...

  4. RPC服务框架探索之Thrift

    前言架构服务化后,需要实现一套方便调用各服务的框架,现在开源如日中天,优先会寻找开源实现,如果没有合适自家公司业务的,才会考虑从零开发,尤其是一切以KPI为准绳的公司,谁会跟钱过不去?N个月之前,公司 ...

  5. 美团大众点评服务框架Pigeon

    服务框架Pigeon架构 • Pigeon提供jar包接入 ,线上运行在tomcat里 • Monitor-CAT ,负责调用链路分析.异常监控告警等 • 配置中心-Lion ,负责一些开关配置读取 ...

  6. 分布式服务框架 dubbo/dubbox 入门示例

    dubbo是一个分布式的服务架构,可直接用于生产环境作为SOA服务框架. 官网首页:http://dubbo.io/ ,官方用户指南 http://dubbo.io/User+Guide-zh.htm ...

  7. .Net下几个服务框架介绍

    简介 在公司的服务多了以后,为了调用上的方便,同时为了以后的服务治理,一般都会使用一些服务框架,这里主要介绍我知道的几个服务框架,简析一下这些服务框架的基本概念. 可投入生产环境使用的 以下两个服务框 ...

  8. Laravel 学习笔记 —— 神奇的服务容器 [转]

    容器,字面上理解就是装东西的东西.常见的变量.对象属性等都可以算是容器.一个容器能够装什么,全部取决于你对该容器的定义.当然,有这样一种容器,它存放的不是文本.数值,而是对象.对象的描述(类.接口)或 ...

  9. 高性能的分布式服务框架 Dubbo

    我思故我在,提问启迪思考! 1. 什么是Dubbo? 官网:http://dubbo.io/,DUBBO是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及作为SOA服务治理的 ...

随机推荐

  1. Ubuntu 重装vmtool

    1. 虚拟机菜单 ->  更新虚拟机  : 2. 弹出的窗口中: 3. 拷贝红色的文件到可读写的目录: 4. 解压,运行解压出来的绿色脚本文件,一路回车:

  2. PHP array_unique

    1.函数的作用:移除数组中重复的值 2.函数的参数: @params array $array @params int $sort_flag SORT_REGULAR : 通常方法比较(不改变类型) ...

  3. 冰释前嫌——转入Android Studio与连接手机无法识别问题

    前言:曾有段时间被AS+gradle虽紧密结合却依然搞不定联网依赖的模样弄的头疼,尝试了各种改代理.改配置均无果,于是坚守Eclipse进行开发学习,结果一方面受制于gradle Android项目的 ...

  4. 玩转OneNET物联网平台之MQTT服务① —— OneNetMqtt全方位调试

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  5. ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库

        在网络篇①中,博主主要讲解了Arduino上开发ESP8266的插件库 Arduino Core For ESP8266.但是,并没有讲到关于这个模块的工作模式,所以本篇讲着重讲解ESP826 ...

  6. 4、OGNL与值栈

    一.OGNL 1.什么是OGNL 对象导航图语言(Object Graph Navigation Language),简称OGNL,是应用于Java中的一个开源的表达式语言(Expression La ...

  7. 【MongoDB详细使用教程】四、python操作MongoDB

    目录 1.安装pymongo 2.连接数据库 3.操作数据库 3.1.查 3.2.增 3.3.改 3.4.删 使用第三方库pymongo来实现python对MongoDB的操作 pymongo官方文档 ...

  8. 百万年薪python之路 -- 内置函数练习

    1.整理今天笔记,课上代码最少敲3遍. 2.用列表推导式做下列小题 过滤掉长度小于3的字符串列表,并将剩下的转换成大写字母 lst = [["a","b"],[ ...

  9. linq一般用法

    最一般的用法 var rows = from c in dataTrue.AsEnumerable() from t in dataPre.AsEnumerable() ].ToString().St ...

  10. 自定义表头Datagrid

    自定义的一个表头 <bp:BasePage x:Class="NetReform.Pages.RealProbabiTableCompare" xmlns="htt ...