EasySwoole3源码分析

萌新小黄带你EasySwoole3源码分析

自序

感谢您花时间看这篇自序,我并不是一位'高手',也不会什么'绝世武功'.只是单纯的为了监督自己学习,并把学习结果分享出来.可能这些文章读起来并不是那么易懂,我感触很深在读别人一些技术文章时候总半途而废,看到一半没懂意思了,跟不上思路了.

我会尽力去讲解清楚每一个知识点.关于如何更好的去读懂本系列,我将会在下文中介绍.

[...未完待续]

框架下载及PhpStorm配置

本次源码分析基于 3.3.4 版本.

框架下载

官方文档: https://www.easyswoole.com/Preface/Introduction.html

环境要求:

PHP >= 7.1
Swoole >= 4.4.12
需要 pcntl 扩展
使用 Linux / FreeBSD / MacOS 这三类操作系统

Composer安装方式:

composer require easyswoole/easyswoole=3.x
php vendor/easyswoole/easyswoole/bin/easyswoole install

我本地使用windows编辑器.然后将代码同步到我远程的Linux服务器上. 下面演示一波.

先在服务器上安装框架. 安装完成后打包下载到本地然后用PhpStorm打开.

1.选择 Tools => Deployment => Configuration

2.点击'+'号, 填写服务器信息. Host填入你的IP, Username 填入用户名 , Password填入密码, Rootpath 选择你的项目路径.

3.还需要将Mappings下的Deployment path 填入 /

4.最后一步,选择 Tools => Deployment => Options 将此项设置为 Always

配置完成后,当你按下Ctrl+s的时候你的本地代码将和服务器进行同步. 在开发thinkphp Laravel的时候到这就可以了.但是基于Cli的框架我们需要手动重启.(当然也有热更新的插件), 还需要多几步

依旧 选择 Tools => Deployment => Start SSH session 点击我们配置好的

下面显示出我们远程的服务器就代表ok.接着我们 打开 Settings 找到 Languages 下的 PHP 配置 CLI Interpreter

最后配置我们的重启脚本, 第一次启动我们需要在命令行里手动输入 php easyswoole start. 最后我们点击运行键会帮我们重启,方便调试.

到此环境配置完成.

EasySwoole命令行Command分析

和阅读其他FPM框架源码一样需要从入口文件跟入. 在EasySwoole中启动方式是 php easyswoole start easyswoole其实就是php文件,只是省略了后缀

在开头定义了几个常量

// 判断是否在PHAR环境下运行, 当前返回False
defined('IN_PHAR') or define('IN_PHAR', boolval(\Phar::running(false)));
// 获取运行目录 /www/wwwroot/es_study
defined('RUNNING_ROOT') or define('RUNNING_ROOT', realpath(getcwd()));
// 获取easyswoole目录 /www/wwwroot/es_study
defined('EASYSWOOLE_ROOT') or define('EASYSWOOLE_ROOT', IN_PHAR ? \Phar::running() : realpath(getcwd()));

然后引入Composer自动加载

// 引用 Composer 自动加载
$file = EASYSWOOLE_ROOT.'/vendor/autoload.php';
if (file_exists($file)) {
    require $file;
}else{
    die("include composer autoload.php fail\n");
}

这里还有引入了一个bootstrap.php默认不存在需要自己新建,它允许用户在框架初始化之前执行自定义事件

if(file_exists(EASYSWOOLE_ROOT.'/bootstrap.php')){
    require_once EASYSWOOLE_ROOT.'/bootstrap.php';
}

最后接收Cli参数, 并弹出第一位. 只需要获取到 第二位之后的即可.

# $argv的内容
array(2) {
  [0]=>
  string(32) "/www/wwwroot/es_study/easyswoole"
  [1]=>
  string(7) "start"
}
$args = $argv;
//trim first command
array_shift($args);

当我们以 php easyswoole start 启动框架的时候,会将 cli参数 传入 CommandRunner::getInstance()->run($args);

类调用使用了 getInstance 方法,是一个单例模式. CommandRunner 也就是我们今天的主题了,继续跟入.

该类的文件在 vendor/easyswoole/easyswoole/src/Command 下. 顺便看下其他文件.可以知道DefaultCommand里放着的是命令参数.还实现了一个CommandContainer容器.

Utility类主要实现了一些辅助功能.

`easySwooleLog` 在终端打印出easySwoole的LOGO

`displayItem` 终端显示一些文本

`releaseResource` 复制文件

`opCacheClear` 清除opcache缓存和Apuc缓存

继续看我们的CommandRunner类.

class CommandRunner
{
    // 引用单例
    use Singleton;   
}

这里通过一个trait实现了一个单例的方法.

该类的构造方法注册了框架自带的命令

function __construct()
    {
        // 注册命令
        CommandContainer::getInstance()->set(new Help());
        CommandContainer::getInstance()->set(new Install());
        CommandContainer::getInstance()->set(new Start());
        CommandContainer::getInstance()->set(new Stop());
        CommandContainer::getInstance()->set(new Reload());
        CommandContainer::getInstance()->set(new PhpUnit());
        CommandContainer::getInstance()->set(new Restart());
        CommandContainer::getInstance()->set(new Config());
    }

该类还剩一个run方法

    // CommandRunner::getInstance()->run($args);
    function run(array $args):?string
    {
        // 弹出元素 `start`
        $command = array_shift($args);
        if(empty($command)){ // 如果为空就显示帮助信息
            $command = 'help';
        }else if($command != 'install'){ // 判断是否为安装模式,在composer下载完成后需要install才能正常使用
            //预先加载配置
            if(in_array('produce',$args)){
                Core::getInstance()->setIsDev(false); // 如果命令行参数中带有produce则是生产环境
            }
            Core::getInstance()->initialize(); // 初始化操作
        }
        if(!CommandContainer::getInstance()->get($command)){ // 判断是否有此命令,没有则为help帮助信息
            $command = 'help';
        }
        return CommandContainer::getInstance()->hook($command,$args); // 通过容器调用相应的类进行操作
    }
}

Core类这里是框架核心类. 我们之后在做具体介绍. 现在接着分析CommandContainer类.

container属性里面存着命令参数和对应类的实例化. CommandRunner类的构造方法里set出具体命令实例化对象.

然后通过CommandContainer::getInstance()->hook($command,$args)hook方法调用具体的类.

调用的所有命令类.需要实现CommandInterface接口.

interface CommandInterface
{
    public function commandName():string; // 命令名
    public function exec(array $args):?string ; // 操作
    public function help(array $args):?string ; // 帮助信息
}

那我们如何实现一个自定义的命令呢?

1.编一个类实现CommandInterface里的所有方法

2.在根目录的bootstrap里set出你新建的类

\EasySwoole\EasySwoole\Command\CommandContainer::getInstance()->set(new \App\Command\Test());

大功告成,你也可以使用自己的方法了. 例如php easyswoole test

框架自带的命令也不一一分析了.后面我们以start命令做深入讲解.毕竟这是我们的启动命令.

EasySwoole单例实现原理分析

在前文的分析中发现很多类使用了getInstance()方法获取实例.本篇将对该方法进行刨析

直接跟踪进去

namespace EasySwoole\Component;


trait Singleton
{
    private static $instance;

    static function getInstance(...$args)
    {
        if(!isset(self::$instance)){
            self::$instance = new static(...$args);
        }
        return self::$instance;
    }
}

使用了Trait,专门为了单继承语言而准备的一种代码复用机制.使用方式通过use关键字在类中声明.注意的是Trait不能直接实例化.

下面以框架中CommandRunner类举例.这也是我们框架第一个实例化的类.

use EasySwoole\Component\Singleton;

class CommandRunner
{
    // 引用单例
    use Singleton;
}

use了Trait类相当于复用里面代码,等同于下面这段代码

use EasySwoole\Component\Singleton;

class CommandRunner
{
    // 引用单例
    private static $instance;

    static function getInstance(...$args)
    {
        if(!isset(self::$instance)){
            self::$instance = new static(...$args);
        }
        return self::$instance;
    }
}

关于Trait的更多介绍可以看下 PHP官方文档

这里使用了一个静态方法getInstance 用于获取该类实例化对象.存在直接返回,不存在先实例化保存在self::$instance中再返回

// 判断该类是否实例化过
if(!isset(self::$instance)){
    // 第一次使用时,去实例化对象(延迟实例化,单例模式中的懒汉模式)
    self::$instance = new static(...$args);
}
// 返回该实例
return self::$instance;

需要注意的是这里是通过new static()来实例化,那么它和 new self() 有什么区别呢 下面通过一个简单的Demo解释下,一目了然不用过多解释

class A {
  public static function get_self() {
    return new self();
  }
 
  public static function get_static() {
    return new static();
  }
}
 
class B extends A {}
 
echo get_class(B::get_self()); // A
echo get_class(B::get_static()); // B
echo get_class(A::get_static()); // A

通过static可以实现后期静态绑定(用于在继承范围内引用静态调用的类) 官方文档

另一个区别是 static:: 只能用于静态属性.

关于EasySwoole的单例实现就介绍到此.实现比较简单,像__construct私有化、clone方法拦截也没有处理.毕竟约定大于实现.

EasySwoole配置类源码剖析

Config类在vendor\easyswoole\src\Config.php里.本篇将对它的实现进行刨析.

我们在需要用到Config的地方 $config = Config::getInstance() 获取实例.

通过 $config->setConf()$config->getConf() 读写配置项,那这些内容存在在哪里呢? 我们可以看下它的构造方法.

class Config
{
    private $conf; // 实现AbstractConfig的类 (框架提供的 TableConfig)

    use Singleton; // 引入单例

    public function __construct(?AbstractConfig $config = null)
    {
        if($config == null){
            $config = new TableConfig(); // 默认传入 TableConfig 类(基于Swoole Table)
        }
        $this->conf = $config;
    }
}

默认为 TableConfig 类.这个类继承了AbstractConfig,如果你想编写自己的存储方式,可以编写一个类继承这个抽象类,实现里面所有方法.

abstract class AbstractConfig
{
    abstract function getConf($key = null); // 获取key
    abstract function setConf($key,$val):bool ; // 设置key
    abstract function load(array $array):bool ; // 批量读入key
    abstract function merge(array $array):bool ; // 合并value
    abstract function clear():bool ; // 清除所有配置项
}

框架中还提供了一个类 SplArrayConfig, 它有一个弊端就是动态生成的配置项只在当前进程有效(进程隔离问题),我们重点分析TableConfig的实现

主要分析 setConfgetConf 这两个方法.


function setConf($key, $val): bool
{
    if (strpos($key, ".") > 0) {
        $temp = explode(".", $key);
        $key = array_shift($temp);
        $data = $this->getConf($key);
        if (is_array($data)) {
            $data = new SplArray($data);
        } else {
            $data = new SplArray();
        }
        $data->set(implode('.', $temp), $val);
        return $this->table->set($key, [
            'data' => serialize($data->getArrayCopy())
        ]);
    } else {
        // 如果没有设置多级配置项直接序列化后写入SwooleTable
        return $this->table->set($key, [
            'data' => serialize($val)
        ]);
    }
}

我们可以通过setConfig('a.b.c', '1') 设置多级配置项.当遇到多级配置项时,会实例化一个SplArray

它是一个数组类库提供了很多操作数组的方法

举个例子:

$arr = new \EasySwoole\Spl\SplArray([
    'a' => [
        'b' => [
            'c' => [
                'd' => '123'
            ]
        ]
    ]
]);

var_dump($arr->get('a.b'));

结果:

array(1) {
  ["c"]=>
  array(1) {
    ["d"]=>
    string(3) "123"
  }
}

下面回归我们的setConfig方法,如果发现多级配置项则获取该多级配置项下其他数据(通过strpos函数判断key中是否有.)

$temp = explode(".", $key); // key名转为一个数组
$key = array_shift($temp); // 弹出第一个元素, 主域名作为SwooleTable里的key
$data = $this->getConf($key); // 获取主域名下的数据
if (is_array($data)) { // 如果有数据则读入SplArray
    $data = new SplArray($data);
} else {
   $data = new SplArray();
}
$data->set(implode('.', $temp), $val); // 将新增的元素插入SplArray

return $this->table->set($key, [
    'data' => serialize($data->getArrayCopy()) // 写入SwooleTable
]);

setConfig方法我们分析完毕了,接着再看看getConfig

function getConf($key = null)
{
    if ($key == null) { // 如果为没有传入数据则打印所有存入的配置项
        $data = [];
        foreach ($this->table as $key => $item) {
            $data[$key] = unserialize($item['data']);
        }
        return $data;
    }
    if (strpos($key, ".") > 0) { // 判断是否为多级配置项
        $temp = explode(".", $key); // 将key转为数组
        $data = $this->table->get(array_shift($temp)); // 在setConfig中,我们只将主域名作为Key存入SwooleTable
        if ($data) {
            $data = unserialize($data['data']);
            /*
             * 数组才有意义进行二次搜索
             */
            if (is_array($data)) {
                $data = new SplArray($data); // 将取出的数组交给SplArray解析
                return $data->get(implode('.', $temp));
            }
        }
    } else {
        $data = $this->table->get($key); // 非多级配置项直接取出反序列化即可
        if ($data) {
            return unserialize($data['data']);
        }
    }
    return null;
}

至此,我们的SetConfiggetConfig方法已经分析结束.

总结:

如果是多级配置项,只将主域名作为Key存入SwooleTable,读取的时候将下面的子配置项传入框架内的SplArray类.

$config = \EasySwoole\EasySwoole\Config::getInstance();
$config->setConf('a.b.c.d.e', 123);
var_dump($config->getTest()->get('a'));

$config->getTest() 是我获取SwooleTable对象的.为了查看下它存入的数据结构.

array(1) {
  ["data"]=>
  string(62) "a:1:{s:1:"b";a:1:{s:1:"c";a:1:{s:1:"d";a:1:{s:1:"e";i:123;}}}}"
}

可以通过unserialize来转成数组.

EasySwoole核心Core分析(一):initialize方法

上篇我们分析了Command的原理,这次接着流程走到Core类.

CommandRunner的run方法中执行了 Core::getInstance()->initialize(); 初始化操作.我们本篇就对该方法一探究竟.

__construct构造方法中定义了几个常量

defined('SWOOLE_VERSION') or define('SWOOLE_VERSION',intval(phpversion('swoole')));
defined('EASYSWOOLE_ROOT') or define('EASYSWOOLE_ROOT', realpath(getcwd()));
defined('EASYSWOOLE_SERVER') or define('EASYSWOOLE_SERVER',1);
defined('EASYSWOOLE_WEB_SERVER') or define('EASYSWOOLE_WEB_SERVER',2);
defined('EASYSWOOLE_WEB_SOCKET_SERVER') or define('EASYSWOOLE_WEB_SOCKET_SERVER',3);
defined('EASYSWOOLE_REDIS_SERVER') or define('EASYSWOOLE_REDIS_SERVER',4);

initialize方法中,首先检测EasySwooleEvent类是否存在

//检查全局文件是否存在.
$file = EASYSWOOLE_ROOT . '/EasySwooleEvent.php';
if(file_exists($file)){
    require_once $file;
    try{
        // 判断载入的类是否实现了Event这个接口
        $ref = new \ReflectionClass('EasySwoole\EasySwoole\EasySwooleEvent');
        if(!$ref->implementsInterface(Event::class)){
            die('global file for EasySwooleEvent is not compatible for EasySwoole\EasySwoole\EasySwooleEvent');
        }
        unset($ref);
    }catch (\Throwable $throwable){
        die($throwable->getMessage());
    }
}else{
    die('global event file missing');
}

然后去初始化一些操作

//先加载配置文件
$this->loadEnv();
//执行框架初始化事件
EasySwooleEvent::initialize();
//临时文件和Log目录初始化
$this->sysDirectoryInit();
//注册错误回调
$this->registerErrorHandler();

加载配置就是将根目录下produce.phpdev.php保存进Config里.之后可以通过 Config::getInstance()->getConf()获取.(Config的实现会在之后文章中细讲,现在只需要知道他可以存取数据,是一个KV存储系统)

EasySwooleEvent::initialize()这一步则是调用之前引用进来的全局类的initialize方法,上篇我们说到可以将自己自定义Command命令声明在这.

$this->sysDirectoryInit() 初始化 临时文件 和 日志 目录.在配置项中框架默认为null,

'TEMP_DIR' => null,
'LOG_DIR' => null

方法中判断为null时,临时文件和日志放在根目录下的Temp和Log目录中.

$tempDir = Config::getInstance()->getConf('TEMP_DIR');
if(empty($tempDir)){
    $tempDir = EASYSWOOLE_ROOT.'/Temp';
    Config::getInstance()->setConf('TEMP_DIR',$tempDir); // 保存新的临时目录
}else{
    $tempDir = rtrim($tempDir,'/');
}

if(!is_dir($tempDir)){
    File::createDirectory($tempDir); // 创建目录
}
defined('EASYSWOOLE_TEMP_DIR') or define('EASYSWOOLE_TEMP_DIR',$tempDir);

同理 日志目录的处理方式也是一样.不多赘述了.最后设置默认文件地址 pid.pidswoole.log

初始化的最后最后一步就是注册全局异常处理registerErrorHandler

首先显示所有错误

ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);

然后初始化日志系统

$logger = Di::getInstance()->get(SysConst::LOGGER_HANDLER); // SysConst 里定义了框架的一些常量
// 判断该类是否实现LoggerInterface接口
if(!$logger instanceof LoggerInterface){
	// 没有实现或者未定义则使用框架默认的日志系统
	$logger = new DefaultLogger(EASYSWOOLE_LOG_DIR);
}
Logger::getInstance($logger); // 实例化Logger类

Logger提供了logconsole方法.日志操作类需要实现LoggerInterface接口

namespace EasySwoole\Log;
interface LoggerInterface
{
    const LOG_LEVEL_INFO = 1;
    const LOG_LEVEL_NOTICE = 2;
    const LOG_LEVEL_WARNING = 3;
    const LOG_LEVEL_ERROR = 4;

    function log(?string $msg,int $logLevel = self::LOG_LEVEL_INFO,string $category = 'DEBUG'):string ;
    function console(?string $msg,int $logLevel = self::LOG_LEVEL_INFO,string $category = 'DEBUG');
}

log方法的本质就是file_put_contents将日志写入文件

function log(?string $msg,int $logLevel = self::LOG_LEVEL_INFO,string $category = 'DEBUG'):string
{
    $date = date('Y-m-d H:i:s');
    $levelStr = $this->levelMap($logLevel);
    $filePath = $this->logDir."/log.log";
    $str = "[{$date}][{$category}][{$levelStr}] : [{$msg}]\n";
    file_put_contents($filePath,"{$str}",FILE_APPEND|LOCK_EX);
    return $str;
}

console是将日志输出在终端

function console(?string $msg,int $logLevel = self::LOG_LEVEL_INFO,string $category = 'DEBUG')
{
    $date = date('Y-m-d H:i:s');
    $levelStr = $this->levelMap($logLevel);
    $temp =  $this->colorString("[{$date}][{$category}][{$levelStr}] : [{$msg}]",$logLevel)."\n";
    fwrite(STDOUT,$temp);
}

同理我们如果想实现一些个性化的日志需求可以自定义一个日志类,然后在EasySwooleEventinitialize方法中加上Di::getInstance()->set(SysConst::LOGGER_HANDLER, 自定义的日志类)实现自己的日志系统

接着又初始化了Trigger

//初始化追追踪器
$trigger = Di::getInstance()->get(SysConst::TRIGGER_HANDLER);
// 同样判断是否实现TriggerInterface接口
if(!$trigger instanceof TriggerInterface){
    // 使用默认的触发器(用于主动触发错误或者异常而不中断程序继续执行)
    $trigger = new DefaultTrigger(Logger::getInstance());
}
Trigger::getInstance($trigger); // 实例化Trigger类

最后注册 set_error_handlerregister_shutdown_function 方法

set_error_handler 设定用户自定义的错误处理函数. 方便用户自己定义来处理运行中的错误.

register_shutdown_function 脚本执行完成时回调

//在没有配置自定义错误处理器的情况下,转化为trigger处理
$errorHandler = Di::getInstance()->get(SysConst::ERROR_HANDLER);
//判断是否可调用
if(!is_callable($errorHandler)){
    $errorHandler = function($errorCode, $description, $file = null, $line = null){
        $l = new Location();
        $l->setFile($file);
        $l->setLine($line);
        // Trigger功能: 主动触发错误或者异常而不中断程序继续执行
        Trigger::getInstance()->error($description,$errorCode,$l);
    };
}
// 注册全局异常处理
set_error_handler($errorHandler);

$func = Di::getInstance()->get(SysConst::SHUTDOWN_FUNCTION);
if(!is_callable($func)){
    $func = function (){
        // 判断是否有异常
        $error = error_get_last();
        if(!empty($error)){
            $l = new Location();
            $l->setFile($error['file']);
            $l->setLine($error['line']);
            // Trigger功能: 主动触发错误或者异常而不中断程序继续执行
            Trigger::getInstance()->error($error['message'],$error['type'],$l);
        }
    };
}
// 终止前回调该方法
register_shutdown_function($func);

Location只负责记录两个参数 linefile.顾名思义他记录着某段函数的位置.

到此我们的initialize方法分析完毕.当你阅读完本篇后可能会对Trigger产生困惑.这是啥? 不要急,马上我们专门介绍一下Trigger.

EasySwoole核心Core分析(二):createServer&start方法

我们之前分析了CommandRunner类,它会去执行一些具体的命令.当我们输入php easyswoole start的时候,将会去执行EasySwoole\EasySwoole\Command\DefaultCommand\Start类中的exec方法.

public function exec(array $args): ?string
{
    // 清除Opcahce缓存和apc缓存
    Utility::opCacheClear();
    $mode = 'develop';
    // 判断是否为dev环境
    if (!Core::getInstance()->isDev()) {
        $mode = 'produce';
    }
    // 获取config实例
    $conf = Config::getInstance();
    // 判断是否为守护进程
    if (in_array("d", $args) || in_array("daemonize", $args)) {
        $conf->setConf("MAIN_SERVER.SETTING.daemonize", true);
    }
    // 创建服务
    Core::getInstance()->createServer();
    ...
    // 将服务器信息打印在终端
    echo $response;
    // 启动服务
    Core::getInstance()->start();
    return null;
}

我们之前都是在初始化日志系统、注册注册错误回调,还没有创建Swoole相关服务,那么创建Swoole实例的部分在哪呢?

Core::getInstance()->createServer() 顾名思义创建服务

function createServer()
{
    $conf = Config::getInstance()->getConf('MAIN_SERVER');
    // 创建Swoole服务
    ServerManager::getInstance()->createSwooleServer(
        $conf['PORT'], $conf['SERVER_TYPE'], $conf['LISTEN_ADDRESS'], $conf['SETTING'], $conf['RUN_MODEL'], $conf['SOCK_TYPE']
    );
    // 注册默认回调事件
    $this->registerDefaultCallBack(ServerManager::getInstance()->getSwooleServer(), $conf['SERVER_TYPE']);
    // hook 全局的mainServerCreate事件
    EasySwooleEvent::mainServerCreate(ServerManager::getInstance()->getMainEventRegister());
    // 注册crontab、Task进程
    $this->extraHandler();
    return $this;
}

ServerManager::getInstance()->createSwooleServer() 这里又使用ServerManager类的方法,这篇里我们只需要知道它能创建一个Swoole服务即可(后续会专门介绍内部实现)

$this->registerDefaultCallBack() 该方法用于注册主服务的事件,我们知道Swoole的Server有多个事件,我们需要去设置相应的回调.

private function registerDefaultCallBack(\swoole_server $server, int $serverType)
    {
        // 判断是否为HTTP服务或WebSocket服务, 绑定OnRequest事件
        if (in_array($serverType, [EASYSWOOLE_WEB_SERVER, EASYSWOOLE_WEB_SOCKET_SERVER], true)) {
            // 获取命名空间
            $namespace = Di::getInstance()->get(SysConst::HTTP_CONTROLLER_NAMESPACE);
            if (empty($namespace)) {
                $namespace = 'App\\HttpController\\';
            }
            // 获取控制器最大深度
            $depth = intval(Di::getInstance()->get(SysConst::HTTP_CONTROLLER_MAX_DEPTH));
            $depth = $depth > 5 ? $depth : 5;
            // 获取最大数量
            $max = intval(Di::getInstance()->get(SysConst::HTTP_CONTROLLER_POOL_MAX_NUM));
            z($max);
            if ($max == 0) {
                $max = 500;
            }
            // 获取等待时间
            $waitTime = intval(Di::getInstance()->get(SysConst::HTTP_CONTROLLER_POOL_WAIT_TIME));
            if ($waitTime == 0) {
                $waitTime = 5;
            }
            // 初始化路由分发器
            $dispatcher = new Dispatcher($namespace, $depth, $max);
            // 设置等待时间
            $dispatcher->setControllerPoolWaitTime($waitTime);
            // 获取HTTP异常全局处理方式
            $httpExceptionHandler = Di::getInstance()->get(SysConst::HTTP_EXCEPTION_HANDLER);
            if (!is_callable($httpExceptionHandler)) {
                // 不存在全局异常捕获时,将把错误信息输出到页面
                $httpExceptionHandler = function ($throwable, $request, $response) {
                    $response->withStatus(Status::CODE_INTERNAL_SERVER_ERROR);
                    $response->write(nl2br($throwable->getMessage() . "\n" . $throwable->getTraceAsString()));
                    Trigger::getInstance()->throwable($throwable);
                };
                // 注入进容器
                Di::getInstance()->set(SysConst::HTTP_EXCEPTION_HANDLER, $httpExceptionHandler);
            }
            // 为分发器也设置异常处理方法
            $dispatcher->setHttpExceptionHandler($httpExceptionHandler);

            // 为创建的Server注册Request事件, EventHelper类封装了Swoole的Server的on、add、set方法
            EventHelper::on($server, EventRegister::onRequest, function (\swoole_http_request $request, \swoole_http_response $response) use ($dispatcher) {
                $request_psr = new Request($request); // 生成PSR请求规范
                $response_psr = new Response($response); // 生成PSR响应规范
                try {
                    // 如果为true则分发路由, 这里的onRequest和Swoole的事件是两码事,不要弄混淆了
                    if (EasySwooleEvent::onRequest($request_psr, $response_psr)) {
                        // 分发路由, 执行HTTP控制器里__hook方法,将响应结果存入Response对象
                        $dispatcher->dispatch($request_psr, $response_psr);
                    }
                } catch (\Throwable $throwable) {
                    call_user_func(Di::getInstance()->get(SysConst::HTTP_EXCEPTION_HANDLER), $throwable, $request_psr, $response_psr);
                } finally {
                    try {
                        // 处理全局Http afterRequest事件
                        EasySwooleEvent::afterRequest($request_psr, $response_psr);
                    } catch (\Throwable $throwable) {
                        call_user_func(Di::getInstance()->get(SysConst::HTTP_EXCEPTION_HANDLER), $throwable, $request_psr, $response_psr);
                    }
                }
                // 响应数据
                $response_psr->__response();
            });
        }

        // 获取主服务的事件对象
        $register = ServerManager::getInstance()->getMainEventRegister();
        //注册进程启动事件
        EventHelper::registerWithAdd($register, EventRegister::onWorkerStart, function (\swoole_server $server, $workerId) {
            // 为Work进程设置名字
            if (!in_array(PHP_OS, ['Darwin', 'CYGWIN', 'WINNT'])) {
                $name = Config::getInstance()->getConf('SERVER_NAME');
                if (($workerId < Config::getInstance()->getConf('MAIN_SERVER.SETTING.worker_num')) && $workerId >= 0) {
                    $type = 'Worker';
                    cli_set_process_title("{$name}.{$type}.{$workerId}");
                }
            }
        });
        // 注册进程退出事件
        EventHelper::registerWithAdd($register, $register::onWorkerExit, function () {
            // 清除当前工作进程内的所有定时器
            Timer::clearAll();
        });
    }

EasySwoole里提供了一个EventRegister类,里面声明了Swoole Server的事件.

class EventRegister extends MultiContainer
{
    const onStart = 'start';
    const onShutdown = 'shutdown';
    const onWorkerStart = 'workerStart';
    const onWorkerStop = 'workerStop';
    const onWorkerExit = 'workerExit';
    const onTimer = 'timer';
    const onConnect = 'connect';
    const onReceive = 'receive';
    const onPacket = 'packet';
    const onClose = 'close';
    const onBufferFull = 'bufferFull';
    const onBufferEmpty = 'bufferEmpty';
    const onTask = 'task';
    const onFinish = 'finish';
    const onPipeMessage = 'pipeMessage';
    const onWorkerError = 'workerError';
    const onManagerStart = 'managerStart';
    const onManagerStop = 'managerStop';
    const onRequest = 'request';
    const onHandShake = 'handShake';
    const onMessage = 'message';
    const onOpen = 'open';
    
    ...
}

registerDefaultCallBack方法里还会初始化一个Dispatcher类,该类用于分发请求到相应的控制器.(路由组件基于fastRoute).这里我们只需要知道它是干啥的就可以了.

默认只针对HTTP服务或WebSocket服务创建OnRequset事件,最后还设置了OnWorkStartOnWorkExit事件,其他服务或者是需要处理其他事件,可以在根目录的EasySwooleEvent类中mainServerCreate方法创建,例如是一个websocket服务需要自己注册一个OnMessage事件,例如:

public static function mainServerCreate(EventRegister $register)
{
    $register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) {
        $server->push($frame->fd, "over");
    });
}

最后在$this->extraHandler方法里注册crontab和Task进程,到此createServer方法我们分析完毕.

我们的Start方法调用ServerManager::getInstance()->start()启动整个服务.到这里我们Swoole实例已经跑起来了.

如果没有仔细跟读源码,会对 DI,ServerManager,Dispatcher,Crontab,TaskManager的实现很模糊,甚至会读不懂上下文.在后续的分析中,会专门分析其具体实现.循序渐进而不是一篇到底.这样可能读起来更困难.

EasySwoole容器DI组件源码剖析

EasySwoole的DI容器实现相对简单,我们可以通过以下方法setget操作类或闭包,甚至是一段字符串.

class Test
{
    public function testA() {
        return "test";
    }
}

Di::getInstance()->set('DI_NUMBER', 5000); // 将5000这个整数存入容器中
Di::getInstance()->set('DI_CLASS', Test::class); // 把类存入容器
Di::getInstance()->set('DI_CALLABLE', function($a) {
    return $a;
}); // 将这个可执行的闭包存入容器


var_dump(Di::getInstance()->get('DI_NUMBER')); // 5000
var_dump(Di::getInstance()->get('DI_CLASS')->testA()); // test 
var_dump(Di::getInstance()->get('DI_CALLABLE')("this is callable")); // this is callable

下面我们来看看底层如何实现的,类在Vendor/easyswoole/component/src/Di.php

class Di
{
    use Singleton;
    private $container = array(); // 会将set进的数据保存在容器里.
    ...
}

需要注意的是Di容器在Swoole服务创建后修改里面的数据会仅针对当前进程有效.如果需要跨进程可以利用SwooleTable.

来接着看下set方法,仅单纯将数据存入container数组中.$obj没有做任何限制.

public function set($key, $obj,...$arg):void
{
    $this->container[$key] = array(
        "obj"=>$obj,
        "params"=>$arg,
    );
}

下面看看get方法.如果存入的obj是对象或者闭包直接返回,存入的是类的话会去尝试实例化它,并存入容器方便下次直接返回对象.

function get($key)
{
    // 首先判断是否存在key
    if (isset($this->container[$key])) {
        $obj = $this->container[$key]['obj'];
        $params = $this->container[$key]['params'];
        
        // 判断obj是否为对象或者可调用的方法
        if (is_object($obj) || is_callable($obj)) {
            return $obj;
        } else if (is_string($obj) && class_exists($obj)) {
            try {
                // 如果是一个类,则去实例化它
                $this->container[$key]['obj'] = new $obj(...$params);
                return $this->container[$key]['obj'];
            } catch (\Throwable $throwable) {
                throw $throwable;
            }
        } else {
            return $obj;
        }
    } else {
        return null;
    }
}

剩下还有两个cleardelete方法.就不用我过多介绍了.

function delete($key): void
{
    unset($this->container[$key]);
}

function clear(): void
{
    $this->container = array();
}

总体EasySwoole的容器类实现较为轻量,使用起来足够简单.