乐趣区

关于php:Swoft的HttpServer启动及请求工作流程五补充startstoprestartreload命令

本章节补充一下命令行启动、敞开、重启和重载 Http 服务的实现

Swoft\Http\Server\Command\HttpServerCommand
地位/vendor/swoft/http-server/src/Commond/HttpServerCommand.php

开启办法:

public function start(): void
{
     // 创立服务, 实质上是获取 httpServer 的单例
     // 并给 httpServer 绑定以后执行的脚本和命令等参数
     // 后附 createServer 的实现
     $server = $this->createServer();
     
     // 打印服务根本信息, 设置是否后盾运行参数
     // 后附实现代码
     $this->showServerInfoPanel($server);
     
     // 启动服务, 这一步调用的后续就到了本系列的第一章,Server 的启动
     // Start the server
     $server->start();}

createServer 的实现:

private function createServer(): HttpServer
{
     // 通过命令行的 input 获取执行脚本
     $script = input()->getScriptFile();
     
     // 获取以后执行的命令
     $command = $this->getFullCommand();
     
     // 获取 httpServer 的单例 bean 对象
     /** @var HttpServer $server */
     $server = bean('httpServer');
     
     // 将执行脚本和执行命令设置给 httpServer
     $server->setScriptFile(Swoft::app()->getPath($script));
     $server->setFullCommand($command);
     return $server;
}

showServerInfoPanel 的实现:

protected function showServerInfoPanel(Server $server): void
{
     // 打印 swoft 的 banner 图, 也就是咱们在命令行看到的
     // 后附打印成果
     $this->showSwoftBanner();
     
     // 如果服务曾经是运行状态, 则打印错误信息并返回
     // Check if it has started
     if ($server->isRunning()) {$masterPid = $server->getPid();
         output()->writeln("<error>The server have been running!(PID: {$masterPid})</error>");
         return; 
     }
     
     // 配置启动选项, 实际上就是设置是否后盾运行
     // 后附实现代码
     // Startup config
     $this->configStartOption($server);
     
     // 获取 server 的类型, 因为咱们此处拿的是 httpServer
     // 所以此处的返回是 HTTP
     // Server startup parameters
     $sType = $server->getServerType();
     
     // 面板信息
     // Main server info
     $panel = [
        // 依据传人的 server 参数, 获取监听的地址和端口、服务模式、woerker 数量、taskWorker 数量等根底设置信息
        $sType => $this->buildMainServerInfo($server),
     ];
     
     // 将 bean.php 中配置的须要监听的 rpc、tcp 等更多的 listener
     // 信息增加到面板信息中
     // Port listeners
     $panel = $this->appendPortsToPanel($server, $panel);
     
     // 题目信息
     $title = sprintf('SERVER INFORMATION(v%s)', Swoft::VERSION);
     
     // 将配置好的面板信息输入到管制台上
     // 后附效果图
     // Show server info
     Show::panel($panel, $title, ['titleStyle' => 'cyan',]);
     
     // 输入服务启动胜利音讯, 实际上此处还未真正的调用服务的 start 办法
     $bgMsg = '!';
     if ($server->isDaemonize()) {$bgMsg = '(Run in background)!';
     }
     output()->writef("<success>$sType Server Start Success{$bgMsg}</success>");
}

swoftBanner 效果图:

     ____            _____    ____                                   __     ___   ___
    / __/    _____  / _/ /_  / __/______ ___ _  ___ _    _____  ____/ /__  |_  | / _ \
   _\ \| |/|/ / _ \/ _/ __/ / _// __/ _ `/  '\/ -_) |/|/ / _ \/ __/'_/ / __/_/ // /
  /___/|__,__/\___/_/ \__/ /_/ /_/  \_,_/_/_/_/\__/|__,__/\___/_/ /_/\_\ /____(_)___/
  

configStartOption 的实现:

protected function configStartOption(Server $server): void
{
     // 获取命令中是否后盾运行的参数
     $asDaemon = input()->getSameOpt(['d', 'daemon'], false);
     
     // 如果是后盾运行, 着将 httpServer 设置为后盾运行
     if ($asDaemon) {$server->setDaemonize();
     }
}

Show::panel 效果图:

                          SERVER INFORMATION(v2.0.10)
  ********************************************************************************
  * HTTP     | Listen: 0.0.0.0:18306, Mode: Process, Worker: 6, Task worker: 12
  ********************************************************************************

listener 中的俄罗斯套娃:
在写这里的时候发现一个乏味的事件,swoft 的 bean.php 中有一段对于 httpServer 的 listener 默认配置, 外面蕴含了 rpc 和 tcp 的 bean 对象. 当关上 tcp 配置正文的时候一切正常, 然而关上 rpc 配置的时候, 服务会启动失败. 给出的错误信息是内存申请超限:

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in /Volumes/Samsung_T5/hmqr/phpProgram/fulian/AuthService/vendor/swoft/framework/src/BeanHandler.php on line 81

关上 bean.php 看到对于 tcpServer、rpcServer、httpServer 的配置:

'tcpServer' => [
     'port' => 18309,
     'debug' => 1,
],
'rpcServer' => [
     'class' => ServiceServer::class,
     'listener' => ['http' => bean('httpServer'),
     ]
],
'httpServer' => [
     'class' => HttpServer::class,
     'port' => 18306,
     'listener' => ['rpc' => bean('rpcServer'),
        //             'tcp' => bean('tcpServer'),
     ],
     'process' => [// 'monitor' => bean(AppProcessMonitorProcess::class)
         // 'crontab' => bean(CrontabProcess::class) 
     ],
     'on' => [// SwooleEvent::TASK   => bean(SyncTaskListener::class),  // Enable sync task
         SwooleEvent::TASK => bean(TaskListener::class), // Enable task must task and finish event
         SwooleEvent::FINISH => bean(FinishListener::class)
     ],
     /* @see HttpServer::$setting */
     'setting' => [
         'task_worker_num' => 12,
         'task_enable_coroutine' => true,
         'worker_num' => 6,
         // static handle
         // 'enable_static_handler'    => true, // 'document_root'            => dirname(__DIR__) . '/public', 
     ]
 ],

发现在 httpServer 的 listener 中用到了 tcpServer 的 bean 对象, 在 rpcServer 的 listener 中又援用了 httpServer, 这不是俄罗斯套娃么? 猜想此次内存申请超限就是这个套娃引起的, 待后续验证……

敞开服务:

public function stop(): void
{
     // 此处和 start 中一样, 是获取 httpServer 的 bean 对象
     $server = $this->createServer();
     
     // 如果服务为运行, 则打印错误信息并返回
     // Check if it has started
     if (!$server->isRunning()) {output()->writeln('<error>The HTTP server is not running! cannot stop.</error>');
        return; 
     }
     
     // 执行敞开服务逻辑, 后附代码
     // Do stopping.
     $server->stop();}

httpServer 的 stop 办法继承于 Server 类:

public function stop(): bool
{
     // 获取过程 ID
     $pid = $this->getPid();
     if ($pid < 1) {return false;}
     
     // 给 master 过程发送 SIGTERM 信号
     // 通过信号的形式让 master 过程执行本人和其它子过程的敞开工作
     // 如果敞开胜利则删除本次执行的 pid 文件和 command 文件
     // 并返回删除后果
     // SIGTERM = 15
     if (ServerHelper::killAndWait($pid, 15, $this->pidName, 30)) {$rmPidOk = ServerHelper::removePidFile(alias($this->pidFile));
         $rmCmdOk = ServerHelper::removePidFile(alias($this->commandFile));
         return $rmPidOk && $rmCmdOk;
     }
     
     // 未能敞开过程, 则返回 false
     return false;
}

重启服务:
首先判断是否运行, 如果运行则先敞开.
之后先设置服务启动形式为后盾模式, 而后调用后面的开启办法.
代码如下:

public function restart(): void
{$server = $this->createServer();
     // Restart server
     $this->restartServer($server);
}
protected function restartServer(Server $server): void
{
     // If it's has started, stop old server.
     if ($server->isRunning()) {$success = $server->stop();
        if (!$success) {output()->error('Stop the old server failed!');
             return;
         }
     }
     output()->writef('<success>Swoft Server Restart Success!</success>');
     // Restart server
     $server->startWithDaemonize();}
public function startWithDaemonize(): void
{
     // Restart default is daemon
     $this->setDaemonize();
     // Start server
     $this->start();}

补充零碎判断服务是否运行的办法:

public function isRunning(): bool
{
     // 获取 master 过程的 pid 文件
     $pidFile = alias($this->pidFile);
     
     // 不存在则认为服务没有启动
     // Is pid file exist ?
     if (!file_exists($pidFile)) {return false;}
     
     // 如果 master 过程 id 文件内没有内容, 则认为服务没有启动
     // Get pid file content and parse the content
     $content = (string)file_get_contents($pidFile);
     if (!$content = trim($content, ',')) {return false;}
     
     // 过程 id 文件中的 id 以逗号分隔, 别离是 masterId 和 managerId
     // 如果内容中没有逗号则认为过程 ID 文件有效, 服务没有运行
     // Content is valid
     if (strpos($content, ',') === false) {return false;}
     
     // 拆分成 masterId 和 managerId
     // Parse and record PIDs
     [$masterPID, $managerPID] = explode(',', $content, 2);
     
     // Format type
     $masterPID = (int)$masterPID;
     $managerPID = (int)$managerPID;
     
     
     $this->pidMap['masterPid']  = $masterPID;
     $this->pidMap['managerPid'] = $managerPID;
     
     // 如果 masterId 大于 1 并且 过程仍旧存活 则示意服务正在运行
     // 给 master 过程发送信号量 0 来检测过程是否存活
     // 跳过 pid 为 1 的状况是为了解决服务在 docker 上跑的状况?
     // Notice: skip pid 1, resolve start server on docker.
     return $masterPID > 1 && Process::kill($masterPID, 0);
}

重载办法:

public function reload(): void
{$server = $this->createServer();
     // Reload server
     $this->reloadServer($server);
}
protected function reloadServer(Server $server): void
{$script = input()->getScriptFile();
     // Check if it has started
     if (!$server->isRunning()) {output()->writeln('<error>The server is not running! cannot reload</error>');
         return; 
     }
     output()->writef('<info>Server %s is reloading</info>', $script);
     
     // 命令参数中带了 t 选项 则打印提示信息
     if ($reloadTask = input()->hasOpt('t')) {Show::notice('Will only reload task worker');
     }
     
     // 开始重载
     if (!$server->reload($reloadTask)) {Show::error('The swoole server worker process reload fail!');
         return; 
     }
     output()->writef('<success>Server %s reload success</success>', $script);
}

public function reload(bool $onlyTaskWorker = false): bool
{if (($pid = $this->pidMap['masterPid']) < 1) {return false;}
     // SIGUSR1(10):
     //  Send a signal to the management process that will smoothly restart all worker processes 
     // 给 master 过程发送信号, 让 master 过程平滑的重启所有 worker 过程
     // SIGUSR2(12): //  Send a signal to the management process, only restart the task process 
     // 12 示意只重启 taskworker 过程
     $signal = $onlyTaskWorker ? 12 : 10;
     
     // 给 master 过程发送信号
     return ServerHelper::sendSignal($pid, $signal);
}

总结:

1. 控制台打印一个服务启动胜利时, 往往是在这个服务 start 前, 也就是服务此时并没有真正的启动胜利.
2. 能够批改 swoftBanner 等办法以达到输入一些花里胡哨的货色的成果, 不倡议这么做.
3. 除启动办法外, 其它办法都是依附向 master 过程发送对应信号量来达到敞开、重启、重载的成果.
4.swoft 与大多数须要后盾运行的程序一样, 采纳 pid 文件的形式来保留 master 过程的信息.
5. 禁止俄罗斯套娃.
退出移动版