本章节补充一下命令行启动、关闭、重启和重载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("The server have been running!(PID: {$masterPid}) ");
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("$sType Server Start Success{$bgMsg} ");
}
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('The HTTP server is not running! cannot stop. ');
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('Swoft Server Restart 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('The server is not running! cannot reload ');
return;
}
output()->writef('Server %s is reloading ', $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('Server %s reload 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.禁止俄罗斯套娃.