PHP 原生 Socket 网络编程:守护进程
你好,我是一笑。
目前市面上有很多开源库其中包括 Swoole、Workman、ReactPHP、AmpPHP 都可以非常方便的帮助我们去实现 Socket 或者 WebSocket 编程,但是这些都是开箱即用的工具,在这个系列的文章中,我将从 0 到 1 实现一个网络在线聊天室。
PHP Cli 后台服务守护进程
Socket 编程与 Web 或者 API 编程最大的差异就是我们的程序是要一直跑在我们的服务器后端执行,在我们手动执行一些 PHP 脚本的时候,如果是长时间的耗时操作我们可能会用到 nohup 命令将我们的脚本脱离终端,避免终端异常关闭后导致脚本执行退出。因为在我们正常的情况下如果你是在前端执行你执行的程序是属于当前终端的,当你终端结束时程序会收到 SIGSUP
的信号,收到信号后默认行为就是将程序直接杀死掉。
所以一般服务端的程序一般都是在后端去执行,所谓的后端就是我们的应用程序没有界面,也没有任何输出的信息,只能通过日志输出到一个文件里,而不是在标准的输入、输出和标准错误中看到任何信息。
最简单实现守护进程的方式就是使用 fork 函数来派生子进程,具体的流程如下:
1. 设置文件创建屏蔽字
文件创建屏蔽字是指屏蔽掉文件创建时的对应位(umask() 控制系统文件和目录默认权限
)。由于使用fork系统调用新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的不便。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。
2. fork 子进程,父进程退出,子进程成为孤儿进程被 init 进程接管
fork 子进程的目的是为了执行我们真正的业务逻辑,而主进程在 fork 子进程后就退出,这样一来 子进程就会变成孤儿进程,孤儿进程会自动被系统的 init 进程接管,这个时候其实也就完成了后台运行,而后面的操作是为了让我们的程序更加健壮。
3. 调用 setsid 建立新的进程会话
虽然你的 主进程退出了,但是你的父子关系其实还是存在的,它有一个sessionid,当你没有做改变的时候,他的 sessionid 还会存在,标记着它在哪个 session 中,既然父进程已经退出了,我们被 init 进程接管了,那么就需要重新设置一个进程会话。
4. 将当前工作目录切换到根目录
在 linux 下是可以 mount 很多磁盘的,你可能是在系统磁盘下,也可能是在新 mount 的数据磁盘上,实际上我们应该与这个磁盘脱离关系,所以要切换一下工作目录。
5. 关闭标准输入、标准输出、标准错误将其重定向到 /dev/null
当Linux启动一个进程时,会自动打开三个的端口:标准输入(Standard Input)、标准输出(Standard Output)和标准错误(Standard Error)。进程通常会通过这三个端口进行输入和输出。
后台程序是应该没有标准输入和标准输出以及标准错误的,所以要将其重定向到 /dev/null中。
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
代码实现
<?php
interface Process
{
public function run(): void;
}
class WriteFileProcess implements Process
{
public function run(): void
{
while (true) {
//在这里写你的业务代码,比如每隔1秒钟,往当前目录下的test.txt文件里面添加当前时间
file_put_contents(dirname(__FILE__) . '/test.txt', date('Y-m-d H:i:s') . PHP_EOL, FILE_APPEND);
sleep(1);
}
}
}
class Daemon
{
private $pidFile;
protected $process;
function __construct(Process $process)
{
$this->pidFile = dirname(__FILE__) . '/daemontest.pid';
$this->process = $process;
}
private function startDeamon()
{
if (file_exists($this->pidFile)) {
echo "The file $this->pidFile exists.\n";
exit();
}
$pid = pcntl_fork();
if ($pid == -1) {
//这里是创建进程失败
die('could not fork');
} else if ($pid) {
//这里是父进程
echo 'start ok';
exit($pid);
} else {
umask(0);
//这里是子进程
file_put_contents($this->pidFile, getmypid());
// 建立新的会话
posix_setsid();
//设置工作目录
chdir('/');
//重定向标准输出,标准输入,标准错误到 /dev/null
fclose(STDIN);//0
fclose(STDOUT);//1
fclose(STDERR);//2
//当关掉以上标准输出,标准输入,标准错误之后,如果后面要对文件操作(比如打开一个文件,写入,创建)它返回的文件描述符从0开始,这样可能造成未知异常
//为了避免问题,我们使用输出从定向到 /dev/null 空设备文件解决这个问题,重新设置0,1,2 文件描述符用来代替标准输入,标准输出,标准错误,往 /dev/null 写入数据会被丢弃,这样就不会向终端输出数据了。
fopen("/dev/null", 'a');
fopen("/dev/null", 'a');
fopen("/dev/null", 'a');
return getmypid();
}
}
private function start()
{
$this->startDeamon();
$this->process->run();
}
private function stop()
{
if (file_exists($this->pidFile)) {
$pid = file_get_contents($this->pidFile);
posix_kill($pid, 9);
unlink($this->pidFile);
}
}
public function run($argv)
{
if ($argv[1] == 'start') {
$this->start();
} else if ($argv[1] == 'stop') {
$this->stop();
} else if ($argv[1] == 'restart') {
$this->stop();
$this->start();
} else {
echo 'param error';
}
}
}
$daemon = new Daemon(new WriteFileProcess());
$daemon->run($argv);
首先,我们定义了一个 process 的接口,这个接,而实现这个接口的 WriteFileProcess 就是我们需要执行的程序,在这段代码中,我们不断的向一个文件中写入当前时间,让程序一直保持运行状态,为了避免CPU 被这个程序沾满,我们写入的时间间隔为 1 秒。
Daemon 类中 startDeamon 方法就是我们实现守护进程的过程,在 Daemon 中还实现了另外两个方法 run
、start
、stop
。
run 可以看做我们的程序入口,他解析传递的命令,当为 start 的时候开始记录 pid,然后执行守护进程方法,执行我们要执行的业务逻辑。
# 执行程序
php Daemon.php start
# 关闭程序
php Daemon.php stop
# 重启程序
php Dameon.php restart
你可以执行一下看看执行效果。
参考文档
- [PHP 的标准输入与输出-php教程-PHP中文网](